diff --git a/apps/docs/package.json b/apps/docs/package.json index f4791bf2d..ed37911e4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -56,5 +56,10 @@ "engines": { "node": ">=18.0" }, + "pnpm": { + "overrides": { + "minimatch": ">=10.2.3" + } + }, "license": "MIT" } diff --git a/apps/ui-sharethrift/.storybook/preview-head.html b/apps/ui-sharethrift/.storybook/preview-head.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/ui-sharethrift/.storybook/preview.js b/apps/ui-sharethrift/.storybook/preview.js deleted file mode 100644 index 26d23134b..000000000 --- a/apps/ui-sharethrift/.storybook/preview.js +++ /dev/null @@ -1,12 +0,0 @@ -import "@sthrift/ui-components/src/styles/theme.css"; - -// Remove Storybook's default 1rem padding from .sb-show-main.sb-main-padded -const style = document.createElement("style"); -style.innerHTML = ` - .sb-show-main.sb-main-padded { - padding: 0 !important; - } -`; -document.head.appendChild(style); - -export const parameters = {}; diff --git a/apps/ui-sharethrift/.storybook/preview.tsx b/apps/ui-sharethrift/.storybook/preview.tsx index 096a31dcc..b88142076 100644 --- a/apps/ui-sharethrift/.storybook/preview.tsx +++ b/apps/ui-sharethrift/.storybook/preview.tsx @@ -1,4 +1,8 @@ import type { Preview } from '@storybook/react-vite'; +import "@sthrift/ui-components/src/styles/theme.css"; +import '../src/index.css'; +import '../src/App.css' +import '@ant-design/v5-patch-for-react-19'; const preview: Preview = { parameters: { @@ -11,7 +15,32 @@ const preview: Preview = { a11y: { test: 'todo', }, + options: { + storySort: { + order: [ + 'Pages', + ['Home - Unauthenticated', + 'Login', + 'Signup', ['Select Account Type', 'Account Setup', 'Profile Setup', 'Terms', 'Payment'], + 'Home - Authenticated', + 'My Listings', + 'My Reservations', + 'Messages', + 'Account', ['Profile', 'Settings']], + 'Components', + 'Containers' + ], + }, + }, }, }; +// Remove Storybook's default 1rem padding from .sb-show-main.sb-main-padded +const style = document.createElement("style"); +style.innerHTML = ` + .sb-show-main.sb-main-padded { + padding: 0 !important; + } +`; +document.head.appendChild(style); export default preview; diff --git a/apps/ui-sharethrift/package.json b/apps/ui-sharethrift/package.json index 9f3b2420c..033fcfabb 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -37,11 +37,11 @@ "@chromatic-com/storybook": "^4.1.3", "@eslint/js": "^9.30.1", "@graphql-typed-document-node/core": "^3.2.0", - "@storybook/addon-a11y": "^10.1.11", - "@storybook/addon-docs": "^10.1.11", - "@storybook/addon-vitest": "^10.1.11", - "@storybook/react": "^10.1.11", - "@storybook/react-vite": "^10.1.11", + "@storybook/addon-a11y": "catalog:", + "@storybook/addon-docs": "catalog:", + "@storybook/addon-vitest": "catalog:", + "@storybook/react": "catalog:", + "@storybook/react-vite": "catalog:", "@testing-library/jest-dom": "^6.9.1", "@types/lodash": "^4.17.20", "@types/react": "^19.1.9", @@ -53,7 +53,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", - "storybook": "^10.2.10", + "storybook": "catalog:", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "catalog:", diff --git a/apps/ui-sharethrift/src/app.container.stories.tsx b/apps/ui-sharethrift/src/app.container.stories.tsx index af7fc542c..2232355a9 100644 --- a/apps/ui-sharethrift/src/app.container.stories.tsx +++ b/apps/ui-sharethrift/src/app.container.stories.tsx @@ -3,10 +3,10 @@ import { AppContainer } from './app.container.tsx'; import { withMockApolloClient, withMockRouter, - MockUnauthWrapper, } from './test-utils/storybook-decorators.tsx'; import { AppContainerCurrentUserDocument, ListingsPageContainerGetListingsDocument, UseUserIsAdminDocument } from './generated.tsx'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { MockUnauthWrapper } from './test-utils/storybook-mock-auth-wrappers.tsx'; diff --git a/apps/ui-sharethrift/src/app.stories.tsx b/apps/ui-sharethrift/src/app.stories.tsx index 1dcb0d663..c0286215a 100644 --- a/apps/ui-sharethrift/src/app.stories.tsx +++ b/apps/ui-sharethrift/src/app.stories.tsx @@ -1,9 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; import { MemoryRouter } from 'react-router-dom'; -import { AuthProvider } from 'react-oidc-context'; -import { MockedProvider } from '@apollo/client/testing/react'; import { App } from './app.tsx'; +import { withAuthDecorator } from './test-utils/storybook-decorators.tsx'; const mockEnv = { VITE_FUNCTION_ENDPOINT: 'https://mock-functions.example.com', @@ -50,6 +49,7 @@ const meta: Meta = { hasCompletedOnboarding: false, isAuthenticated: false, }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags parameters: { layout: 'fullscreen', docs: { @@ -59,21 +59,7 @@ const meta: Meta = { }, }, }, - decorators: [ - (Story) => ( - - - - - - ), - ], + decorators: [withAuthDecorator], } satisfies Meta; export default meta; diff --git a/apps/ui-sharethrift/src/app.tsx b/apps/ui-sharethrift/src/app.tsx index 4918de849..02ee36861 100644 --- a/apps/ui-sharethrift/src/app.tsx +++ b/apps/ui-sharethrift/src/app.tsx @@ -1,8 +1,8 @@ +import type React from 'react'; import { Route, Routes } from 'react-router-dom'; import { AppRoutes } from './components/layouts/app/index.tsx'; -import type React from 'react'; -import SignupRoutes from './components/layouts/signup/index.tsx'; -import { LoginSelection } from './components/shared/login-selection.tsx'; +import { LoginSelection } from './components/layouts/login/login-selection.tsx'; +import { SignupRoutes } from './components/layouts/signup/index.tsx'; import { AuthRedirectAdmin } from './components/shared/auth-redirect-admin.tsx'; import { AuthRedirectUser } from './components/shared/auth-redirect-user.tsx'; import { RequireAuth } from './components/shared/require-auth.tsx'; diff --git a/apps/ui-sharethrift/src/components/layouts/app/app-routes.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/app-routes.stories.tsx index 8e205847f..28436d0cc 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/app-routes.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/app-routes.stories.tsx @@ -1,41 +1,26 @@ -import type { Meta, StoryFn } from "@storybook/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, within } from 'storybook/test'; import { AppRoutes } from "./index.tsx"; -import { ListingsPageContainerGetListingsDocument, UseUserIsAdminDocument } from "../../../generated.tsx"; -import { withMockApolloClient, withMockRouter } from "../../../test-utils/storybook-decorators.tsx"; -import { expect } from 'storybook/test'; +import { withMockApolloClient,withMockRouter } from "../../../test-utils/storybook-decorators.tsx"; +import { ListingsPageContainerGetListingsDocument } from "../../../generated.tsx"; const meta: Meta = { - title: "Layouts/App Routes", + title: "Pages/Home - Authenticated", component: AppRoutes, decorators: [ withMockApolloClient, - withMockRouter("/"), + withMockRouter("/", true), ], }; export default meta; +type Story = StoryObj; -const Template: StoryFn = () => ; - -export const DefaultView: StoryFn = Template.bind({}); +export const DefaultView: Story = {}; DefaultView.play = async ({ canvasElement }) => { - // Component renders with lazy-loaded routes - expect(canvasElement).toBeTruthy(); -}; - -/** - * Tests that routes render correctly with lazy loading and Suspense. - * Verifies the lazy() import mechanism and Suspense wrapper are working for all route components. - */ -export const LazyLoadedRoutes: StoryFn = Template.bind({}); -LazyLoadedRoutes.play = async ({ canvasElement }) => { - // Component should render (Suspense wrapper is present) - expect(canvasElement).toBeTruthy(); - - // Verify the component has rendered content - const textContent = canvasElement.textContent || ''; - expect(textContent.length).toBeGreaterThan(0); + const canvas = within(canvasElement); + await expect(canvas.getByRole('main')).toBeInTheDocument(); }; DefaultView.parameters = { @@ -148,7 +133,58 @@ DefaultView.parameters = { }, { request: { - query: UseUserIsAdminDocument, + query: { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "useUserIsAdmin" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "currentUser" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "InlineFragment", + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "PersonalUser" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "userIsAdmin" } }, + ], + }, + }, + { + kind: "InlineFragment", + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "AdminUser" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "userIsAdmin" } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, }, result: { data: { @@ -162,4 +198,4 @@ DefaultView.parameters = { }, ], }, -}; +}; \ No newline at end of file diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/components/profile-view.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/components/profile-view.stories.tsx index 7109ce2e3..e6352d9fb 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/components/profile-view.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/components/profile-view.stories.tsx @@ -73,9 +73,9 @@ export const Default: Story = { listings: [], isOwnProfile: true, onEditSettings: () => console.log('Edit settings clicked'), - onListingClick: (_id: string) => console.log('Listing clicked'), + onListingClick: () => console.log('Listing clicked'), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -134,7 +134,7 @@ export const EmptyListingsOwnProfile: Story = { onEditSettings: fn(), onListingClick: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getByText('No listings yet')).toBeInTheDocument(); await expect(canvas.getByRole('button', { name: /Create Your First Listing/i })).toBeInTheDocument(); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/profile-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/profile-page.stories.tsx index 5e57823cd..4a60b81e4 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/profile-page.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/profile-page.stories.tsx @@ -7,52 +7,60 @@ import { import { HomeAccountProfileViewContainerCurrentUserDocument, HomeAccountProfileViewContainerUserListingsDocument, - UseUserIsAdminDocument, type ItemListing, type PersonalUser, } from '../../../../../../../../generated.tsx'; -import { expect } from 'storybook/test'; +import { expect, within } from 'storybook/test'; +import { UserIsAdminMockRequest } from '../../../../../../../../test-utils/storybook-mock-helpers.ts'; const mockUserSarah: PersonalUser = { + __typename: 'PersonalUser', id: '507f1f77bcf86cd799439099', + userIsAdmin: false, userType: 'personal-user', + createdAt: '2024-08-01T00:00:00Z', + account: { + __typename: 'PersonalUserAccount', accountType: 'verified-personal', - - username: 'sarah_williams', email: 'sarah.williams@example.com', + username: 'sarah_williams', profile: { + __typename: 'PersonalUserAccountProfile', firstName: 'Sarah', lastName: 'Williams', location: { + __typename: 'PersonalUserAccountProfileLocation', city: 'Philadelphia', state: 'PA', }, }, }, - - createdAt: '2024-08-01T00:00:00Z', - updatedAt: '2024-08-15T12:00:00Z', }; const mockUserAlex: PersonalUser = { + __typename: 'PersonalUser', id: '507f1f77bcf86cd799439102', + userIsAdmin: false, userType: 'personal-user', + createdAt: '2025-10-01T08:00:00Z', + account: { + __typename: 'PersonalUserAccount', + username: 'new_user', + email: 'new.user@example.com', + accountType: 'non-verified-personal', profile: { + __typename: 'PersonalUserAccountProfile', firstName: 'Alex', lastName: '', location: { + __typename: 'PersonalUserAccountProfileLocation', city: 'Boston', state: 'MA', }, }, - username: 'new_user', - email: 'new.user@example.com', - accountType: 'non-verified-personal', }, - createdAt: '2025-10-01T08:00:00Z', - updatedAt: '2025-10-01T08:00:00Z', }; const mockTwoListings: ItemListing[] = [ @@ -87,19 +95,6 @@ const mockTwoListings: ItemListing[] = [ }, ]; -const userIsAdminMockRequest = (userId: string) => { - return { - request: { - query: UseUserIsAdminDocument, - }, - result: { - data: { - currentUser: {id: userId, userIsAdmin: false }, - }, - }, - }; -}; - const meta: Meta = { title: 'Pages/Account/Profile', component: AppRoutes, @@ -114,10 +109,6 @@ export default meta; type Story = StoryObj; export const DefaultView: Story = { - play: async ({ canvasElement }) => { - // Component renders with lazy-loaded content - expect(canvasElement).toBeTruthy(); - }, parameters: { apolloClient: { mocks: [ @@ -125,6 +116,7 @@ export const DefaultView: Story = { request: { query: HomeAccountProfileViewContainerCurrentUserDocument, }, + delay: 100, // give this a slight delay to have cache merge work properly for the other query (UseUserIsAdminDocument) overriding this user data result: { data: { currentUser: mockUserSarah, @@ -148,10 +140,14 @@ export const DefaultView: Story = { }, }, }, - userIsAdminMockRequest(mockUserSarah.id), + UserIsAdminMockRequest(mockUserSarah.id), ], }, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('main')).toBeInTheDocument(); + }, }; export const NoListings: Story = { @@ -162,6 +158,7 @@ export const NoListings: Story = { request: { query: HomeAccountProfileViewContainerCurrentUserDocument, }, + delay: 100, // give this a slight delay to have cache merge work properly for the other query (UseUserIsAdminDocument) overriding this user data result: { data: { currentUser: mockUserAlex, @@ -175,11 +172,11 @@ export const NoListings: Story = { }, result: { data: { - myListingsAll: { items: [], total: 0, page: 1, pageSize: 100 }, + myListingsAll: { items: [], total: 0, page: 1, pageSize: 100 }, }, }, }, - userIsAdminMockRequest(mockUserAlex.id), + UserIsAdminMockRequest(mockUserAlex.id), ], }, }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/components/settings-view.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/components/settings-view.container.stories.tsx index b833603ae..dcdc6c8b1 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/components/settings-view.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/components/settings-view.container.stories.tsx @@ -79,6 +79,7 @@ const mockAdminUser = { const meta: Meta = { title: 'Containers/SettingsViewContainer', component: SettingsViewContainer, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. parameters: { a11y: { disable: true }, layout: 'fullscreen', @@ -146,7 +147,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingText = canvas.queryByText(/Loading/i); expect(loadingText || canvasElement).toBeTruthy(); @@ -216,7 +217,7 @@ export const UserNotFound: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const notFoundText = canvas.queryByText(/User not found/i); expect(notFoundText || canvasElement).toBeTruthy(); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-page.stories.tsx index cecc46654..a289efed3 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-page.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-page.stories.tsx @@ -1,141 +1,82 @@ -import type { Meta, StoryFn } from "@storybook/react"; -import { AppRoutes } from "../../../../../index.tsx"; -import { HomeAccountSettingsViewContainerCurrentUserDocument } from "../../../../../../../../generated.tsx"; +import type { Meta, StoryObj } from '@storybook/react'; + +import { expect, within } from 'storybook/test'; +import { AppRoutes } from '../../../../..'; import { withMockApolloClient, withMockRouter, } from '../../../../../../../../test-utils/storybook-decorators.tsx'; -import { expect } from 'storybook/test'; +import { HomeAccountSettingsViewContainerCurrentUserDocument } from '../../../../../../../../generated.tsx'; +import { UserIsAdminMockRequest } from '../../../../../../../../test-utils/storybook-mock-helpers.ts'; const meta: Meta = { - title: "Pages/Account/Settings", + title: 'Pages/Account/Settings', component: AppRoutes, - decorators: [ - withMockApolloClient, - withMockRouter("/account/settings"), - ], + decorators: [withMockApolloClient, withMockRouter('/account/settings')], }; export default meta; -const Template: StoryFn = () => ; - -export const DefaultView: StoryFn = Template.bind({}); +type Story = StoryObj; +export const DefaultView: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAccountSettingsViewContainerCurrentUserDocument, + }, + delay: 100, + result: { + data: { + currentUser: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439099', + userType: 'personal-user', + createdAt: '2024-08-01T00:00:00Z', + updatedAt: '2025-08-08T12:00:00Z', + account: { + __typename: 'PersonalUserAccount', + accountType: 'personal', + email: 'patrick.g@example.com', + username: 'patrick_g', + profile: { + __typename: 'PersonalUserAccountProfile', -DefaultView.play = async ({ canvasElement }) => { - // Component renders with lazy-loaded content - expect(canvasElement).toBeTruthy(); + firstName: 'Patrick', + lastName: 'Garcia', + aboutMe: + 'Enthusiastic thrift shopper and vintage lover. Always on the hunt for unique finds and sustainable fashion.', + location: { + __typename: 'PersonalUserAccountProfileLocation', + address1: '123 Main Street', + address2: 'Apt 4B', + city: 'Philadelphia', + state: 'PA', + country: 'United States', + zipCode: '19101', + }, + billing: { + __typename: 'PersonalUserAccountProfileBilling', + subscription: { + __typename: + 'PersonalUserAccountProfileBillingSubscription', + subscriptionId: 'sub_123456789', + }, + cybersourceCustomerId: 'cust_abc123', + }, + }, + }, + }, + }, + }, + }, + UserIsAdminMockRequest('507f1f77bcf86cd799439099'), + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('main')).toBeInTheDocument(); + }, }; - -DefaultView.parameters = { - apolloClient: { - mocks: [ - { - request: { - query: HomeAccountSettingsViewContainerCurrentUserDocument, - }, - result: { - data: { - currentPersonalUserAndCreateIfNotExists: { - __typename: "PersonalUser", - id: "507f1f77bcf86cd799439099", - userType: "personal-user", - createdAt: "2024-08-01T00:00:00Z", - updatedAt: "2025-08-08T12:00:00Z", - account: { - __typename: "PersonalUserAccount", - accountType: "personal", - email: "patrick.g@example.com", - username: "patrick_g", - profile: { - __typename: "PersonalUserAccountProfile", - firstName: "Patrick", - lastName: "Garcia", - location: { - __typename: "PersonalUserAccountProfileLocation", - address1: "123 Main Street", - address2: "Apt 4B", - city: "Philadelphia", - state: "PA", - country: "United States", - zipCode: "19101", - }, - billing: { - __typename: "PersonalUserAccountProfileBilling", - subscriptionId: "sub_123456789", - cybersourceCustomerId: "cust_abc123", - }, - }, - }, - }, - }, - }, - }, - { - request: { - query: { - kind: "Document", - definitions: [ - { - kind: "OperationDefinition", - operation: "query", - name: { kind: "Name", value: "useUserIsAdmin" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { - kind: "Field", - name: { kind: "Name", value: "currentUser" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { - kind: "InlineFragment", - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "PersonalUser" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - { kind: "Field", name: { kind: "Name", value: "userIsAdmin" } }, - ], - }, - }, - { - kind: "InlineFragment", - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "AdminUser" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - { kind: "Field", name: { kind: "Name", value: "userIsAdmin" } }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - result: { - data: { - currentUser: { - __typename: "PersonalUser", - id: "507f1f77bcf86cd799439099", - userIsAdmin: false, - }, - }, - }, - }, - ], - }, -}; \ No newline at end of file diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-view.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-view.stories.tsx index 51c1cdf14..a4463d649 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-view.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/settings-view.stories.tsx @@ -53,7 +53,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); const canvas = within(canvasElement); expect(canvas.getByText('John')).toBeInTheDocument(); @@ -201,7 +201,7 @@ export const UserWithoutBilling: Story = { billing: undefined, }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const notProvidedTexts = canvas.getAllByText('Not provided'); expect(notProvidedTexts.length).toBeGreaterThan(0); @@ -224,7 +224,7 @@ export const MinimalUser: Story = { }, }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const notProvidedTexts = canvas.getAllByText('Not provided'); expect(notProvidedTexts.length).toBeGreaterThan(0); @@ -235,7 +235,7 @@ export const SavingState: Story = { args: { isSavingSection: true, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-filter.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-filter.stories.tsx index d96618d77..8f6f861b1 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-filter.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-filter.stories.tsx @@ -3,7 +3,7 @@ import { expect, within, fn, userEvent } from 'storybook/test'; import { StatusFilter } from './admin-listings-table.status-filter.tsx'; const meta: Meta = { - title: 'Admin/ListingsTable/StatusFilter', + title: 'Components/AdminListingsTable/StatusFilter', component: StatusFilter, parameters: { layout: 'centered', diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-tag.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-tag.stories.tsx index 04d2aec67..0cf3cd82e 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-tag.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-status-tag.stories.tsx @@ -3,7 +3,7 @@ import { expect, within } from 'storybook/test'; import { StatusTag } from './admin-listings-table.status-tag.tsx'; const meta: Meta = { - title: 'Admin/ListingsTable/StatusTag', + title: 'Components/AdminListingsTable/StatusTag', component: StatusTag, parameters: { layout: 'centered', @@ -23,7 +23,7 @@ export const Blocked: Story = { args: { status: 'Blocked', }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const tag = canvas.getByText('Blocked'); expect(tag).toBeTruthy(); @@ -34,7 +34,7 @@ export const UndefinedStatus: Story = { args: { status: undefined, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const tag = canvas.getByText('N/A'); expect(tag).toBeTruthy(); @@ -45,7 +45,7 @@ export const CustomStatus: Story = { args: { status: 'Active', }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const tag = canvas.getByText('Active'); expect(tag).toBeTruthy(); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-title-filter.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-title-filter.stories.tsx index a8fa2396f..5e4794b82 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-title-filter.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-title-filter.stories.tsx @@ -3,7 +3,7 @@ import { expect, within, fn, userEvent } from 'storybook/test'; import { TitleFilter } from './admin-listings-table.title-filter.tsx'; const meta: Meta = { - title: 'Admin/ListingsTable/TitleFilter', + title: 'Components/AdminListingsTable/TitleFilter', component: TitleFilter, parameters: { layout: 'centered', @@ -25,7 +25,7 @@ export const Empty: Story = { searchText: '', selectedKeys: [], }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByPlaceholderText('Search listings'); expect(input).toBeTruthy(); @@ -37,7 +37,7 @@ export const WithSearchText: Story = { searchText: 'bicycle', selectedKeys: [], }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByPlaceholderText('Search listings'); expect(input).toBeTruthy(); @@ -49,7 +49,7 @@ export const WithSelectedKeys: Story = { searchText: '', selectedKeys: ['tent'], }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const input = canvas.getByPlaceholderText('Search listings'); expect((input as HTMLInputElement).value).toBe('tent'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-utils.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-utils.stories.tsx index 4e31a51ff..190f53832 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-utils.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-utils.stories.tsx @@ -42,13 +42,14 @@ const meta: Meta = { parameters: { layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; type Story = StoryObj; export const Default: Story = { - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { // Test valid date formatting with ISO strings (includes timezone) expect(formatDate('2024-01-15T10:30:00Z')).toBe('2024-01-15'); expect(formatDate('2024-12-25T12:00:00Z')).toBe('2024-12-25'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-view-listing.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-view-listing.stories.tsx index 359ed8a89..81a53a6d0 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-view-listing.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table-view-listing.stories.tsx @@ -14,6 +14,7 @@ import { const meta: Meta = { title: 'Containers/AdminViewListing', component: AdminViewListing, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags parameters: { layout: 'fullscreen', apolloClient: { diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table.container.stories.tsx index 25b5cf0bb..9e2f008c3 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-listings-table/admin-listings-table.container.stories.tsx @@ -14,6 +14,7 @@ import { const meta: Meta = { title: 'Containers/AdminListingsTableContainer', component: AdminListings, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags parameters: { a11y: { disable: true }, layout: 'fullscreen', @@ -214,7 +215,7 @@ export const LoadingState: Story = { ], }, }, - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + play: ({ canvasElement }: { canvasElement: HTMLElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-card.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-card.stories.tsx index 157239ff4..bfb0b757b 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-card.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-card.stories.tsx @@ -51,7 +51,7 @@ const mockUserNoDate: AdminUserData = { }; const meta: Meta = { - title: 'Admin/UsersTable/AdminUsersCard', + title: 'Components/AdminUsers/AdminUsersCard', component: AdminUsersCard, parameters: { layout: 'centered', @@ -68,7 +68,7 @@ export const ActiveUser: Story = { args: { user: mockActiveUser, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText('johndoe')).toBeTruthy(); expect(canvas.getByText('Active')).toBeTruthy(); @@ -82,7 +82,7 @@ export const BlockedUser: Story = { args: { user: mockBlockedUser, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText('blockeduser')).toBeTruthy(); expect(canvas.getByText('Blocked')).toBeTruthy(); @@ -95,7 +95,7 @@ export const UserWithReports: Story = { args: { user: mockUserWithReports, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText('reporteduser')).toBeTruthy(); expect(canvas.getByText('View Report (5)')).toBeTruthy(); @@ -106,7 +106,7 @@ export const UserWithNoDate: Story = { args: { user: mockUserNoDate, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText('newuser')).toBeTruthy(); expect(canvasElement.textContent).toContain('N/A'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-table.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-table.container.stories.tsx index 0943723b7..02c64597a 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-table.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users-table.container.stories.tsx @@ -50,6 +50,7 @@ const mockUsers = [ const meta: Meta = { title: 'Containers/AdminUsersTableContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags component: AdminUsersTableContainer, args: { currentPage: 1, @@ -126,7 +127,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + play: ({ canvasElement }: { canvasElement: HTMLElement }) => { const canvas = within(canvasElement); expect(canvasElement).toBeTruthy(); const johnDoe = canvas.queryByText(/john_doe/i); @@ -161,7 +162,7 @@ export const Empty: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -181,7 +182,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -238,7 +239,7 @@ export const WithBlockedUser: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvasElement).toBeTruthy(); const blockedText = canvas.queryByText(/Blocked/i); @@ -281,7 +282,7 @@ export const BlockUserError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -327,7 +328,7 @@ export const ManyUsers: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -365,7 +366,7 @@ export const UnblockUserError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -385,7 +386,7 @@ export const WithError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1153,7 +1154,7 @@ export const BlockUserMutationNetworkError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1191,7 +1192,7 @@ export const UnblockUserMutationNetworkError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1226,7 +1227,7 @@ export const SortingWithArrayField: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1261,7 +1262,7 @@ export const SortingWithNullField: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1296,7 +1297,7 @@ export const SearchWithPageReset: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1331,7 +1332,7 @@ export const StatusFilterWithPageReset: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1366,7 +1367,7 @@ export const TableChangeWithSorter: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -2077,7 +2078,7 @@ export const StatusFilterActive: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); // Status filter interaction would be tested here }, @@ -2113,7 +2114,7 @@ export const StatusFilterBlocked: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); // Status filter interaction would be tested here }, @@ -2404,7 +2405,7 @@ export const HandleStatusFilterFunction: Story = { args: { onPageChange: fn(), }, - play: async ({ canvasElement, args }) => { + play: ({ canvasElement, args }) => { expect(canvasElement).toBeTruthy(); // Verify handleStatusFilter function is called and resets page // Note: The actual function call happens during component interaction diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users.stories.tsx index 66faaf806..7f1135c39 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/components/admin-users-table/admin-users.stories.tsx @@ -53,6 +53,8 @@ const meta: Meta = { }, ], total: 2, + page: 1, + pageSize: 50, }, }, }, @@ -238,6 +240,8 @@ export const UnblockUserModal: Story = { }, ], total: 1, + page: 1, + pageSize: 50, }, }, }, @@ -445,6 +449,8 @@ export const UnblockModalConfirm: Story = { }, ], total: 1, + page: 1, + pageSize: 50, }, }, }, @@ -523,6 +529,8 @@ export const WithNullDateRender: Story = { }, ], total: 1, + page: 1, + pageSize: 50, }, }, }, @@ -572,6 +580,8 @@ export const WithInvalidDateRender: Story = { }, ], total: 1, + page: 1, + pageSize: 50, }, }, }, @@ -632,7 +642,9 @@ export const MultipleUsers: Story = { status: 'verified-personal', isBlocked: i % 3 === 0, })), - total: 100, + total: 100, + page: 1, + pageSize: 50, }, }, }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.container.stories.tsx index a995abe87..0c7ea3348 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.container.stories.tsx @@ -1,30 +1,44 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect } from 'storybook/test'; -import { AdminDashboardMain } from './admin-dashboard-main.tsx'; -import { withMockApolloClient,withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; +import { AppRoutes } from '../../..'; +import { withMockApolloClient,withMockRouter } from '../../../../../../test-utils/storybook-decorators'; import { AdminListingsTableContainerAdminListingsDocument,AdminUsersTableContainerAllUsersDocument } from '../../../../../../generated.tsx'; +import { UserIsAdminMockRequest } from '../../../../../../test-utils/storybook-mock-helpers.ts'; +import { expect, within } from 'storybook/test'; -const meta: Meta = { - title: 'Pages/AdminDashboardMain', - component: AdminDashboardMain, + + +const meta: Meta = { + title: 'Pages/Admin Dashboard', + component: AppRoutes, parameters: { a11y: { disable: true }, layout: 'fullscreen', + }, + decorators: [withMockApolloClient, withMockRouter('/admin-dashboard')], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + parameters: { apolloClient: { mocks: [ { request: { query: AdminListingsTableContainerAdminListingsDocument, + variables: { page: 1, pageSize: 6, statusFilters: ['Blocked'] }, }, - variableMatcher: () => true, + maxUsageCount: Number.POSITIVE_INFINITY, result: { data: { adminListings: { items: [ { id: 'listing-1', + __typename: 'ListingAll', title: 'Test Listing', - images: ['https://example.com/image.jpg'], + images: ['https://example.com/image.jpg'], createdAt: '2024-01-01T00:00:00Z', sharingPeriodStart: '2024-01-15', sharingPeriodEnd: '2024-02-15', @@ -32,6 +46,8 @@ const meta: Meta = { }, ], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -39,39 +55,57 @@ const meta: Meta = { { request: { query: AdminUsersTableContainerAllUsersDocument, + variables: { + page: 1, + pageSize: 50, + searchText: '', + statusFilters: [], + }, }, - variableMatcher: () => true, + maxUsageCount: Number.POSITIVE_INFINITY, result: { data: { allUsers: { items: [ { + __typename: 'PersonalUser', id: 'user-1', email: 'test@example.com', firstName: 'John', lastName: 'Doe', roleNames: ['User'], isBlocked: false, + createdAt: '2024-01-01T00:00:00Z', + userType: 'personal-user', + account: { + username: 'johndoe', + email: 'test@example.com', + profile: { + firstName: 'John', + lastName: 'Doe', + }, + }, }, ], total: 1, + page: 1, + pageSize: 50, }, }, }, }, + UserIsAdminMockRequest('admin-user', true), ], }, }, - decorators: [withMockApolloClient, withMockRouter('/account/admin-dashboard')], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - play: ({ canvasElement }: { canvasElement: HTMLElement }) => { + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + // make sure that everything rendered already and wait for async queries expect(canvasElement).toBeTruthy(); - const tabs = canvasElement.querySelectorAll('[role="tab"]'); - expect(tabs.length).toBeGreaterThan(0); + const canvas = within(canvasElement); + // wait for the admin dashboard H1 heading to appear after Apollo mocks resolve + const adminDashboardText = await canvas.findByRole('heading', { + name: /Admin Dashboard/i, + }); + expect(adminDashboardText).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.stories.tsx index 62fd0bb3e..264081eb1 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/admin-dashboard/pages/admin-dashboard-main.stories.tsx @@ -13,6 +13,7 @@ const meta = { }, }, }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags } satisfies Meta; export default meta; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing-form.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing-form.stories.tsx index a08dda702..2ecb1b6fc 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing-form.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing-form.stories.tsx @@ -3,7 +3,7 @@ import { Form } from 'antd'; import { ListingForm } from './create-listing-form'; const meta: Meta = { - title: 'CreateListing/ListingForm', + title: 'Components/CreateListing/ListingForm', component: ListingForm, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.container.stories.tsx index 57eac0d29..43ff6b9d6 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.container.stories.tsx @@ -10,6 +10,8 @@ import { HomeCreateListingContainerCreateItemListingDocument } from '../../../.. const meta: Meta = { title: 'Containers/CreateListingContainer', component: CreateListingContainer, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. + parameters: { layout: 'fullscreen', a11y: { disable: true }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.stories.tsx index 3e609e5b6..2d3eaf7a0 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/components/create-listing.stories.tsx @@ -57,7 +57,7 @@ export const Default: Story = { onImageAdd: fn(), onImageRemove: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const header = canvas.queryByText(/Create a Listing/i); @@ -78,7 +78,7 @@ export const WithImages: Story = { onCancel: fn(), onImageRemove: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -88,7 +88,7 @@ export const FormInteraction: Story = { onSubmit: fn(), onCancel: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const titleInput = canvas.queryByLabelText(/Title/i); @@ -102,7 +102,7 @@ export const ClickBackButton: Story = { args: { onCancel: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const backButton = canvas.queryByRole('button', { name: /Back/i }); @@ -118,7 +118,7 @@ export const ClickSaveAsDraft: Story = { onSubmit: fn(), uploadedImages: [], }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const draftButton = canvas.queryByRole('button', { name: /Save as Draft/i }); @@ -134,7 +134,7 @@ export const ClickPublish: Story = { onSubmit: fn(), uploadedImages: ['/assets/item-images/bike.png'], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const publishButton = canvas.queryByRole('button', { name: /Publish/i }); @@ -150,7 +150,7 @@ export const Loading: Story = { onSubmit: fn(), onCancel: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -161,7 +161,7 @@ export const ShowPublishedSuccess: Story = { onViewListing: fn(), onModalClose: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -172,7 +172,7 @@ export const ShowDraftSuccess: Story = { onViewDraft: fn(), onModalClose: fn(), }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -183,7 +183,7 @@ export const PublishWithValidForm: Story = { onCancel: fn(), uploadedImages: ['/assets/item-images/bike.png'], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const titleInput = canvas.getByLabelText(/Title/i); @@ -203,7 +203,7 @@ export const SaveDraftWithPartialData: Story = { onCancel: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const titleInput = canvas.getByLabelText(/Title/i); @@ -221,7 +221,7 @@ export const RemoveImage: Story = { onSubmit: fn(), onCancel: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const removeButtons = canvas.queryAllByRole('button', { name: /remove/i }); @@ -242,7 +242,7 @@ export const LoadingToPublished: Story = { onModalClose: fn(), uploadedImages: ['/assets/item-images/bike.png'], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -256,7 +256,7 @@ export const LoadingToDraft: Story = { onModalClose: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -267,7 +267,7 @@ export const MaxCharacterLimitDescription: Story = { onCancel: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -278,7 +278,7 @@ export const CategorySelection: Story = { onCancel: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const categorySelect = canvas.queryByRole('combobox', { name: /Category/i }); if (categorySelect) { @@ -294,7 +294,7 @@ export const EmptyCategories: Story = { onCancel: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -305,7 +305,7 @@ export const DateRangePicker: Story = { onCancel: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const dateInputs = canvas.queryAllByRole('textbox'); await expect(dateInputs.length).toBeGreaterThan(0); @@ -318,8 +318,11 @@ export const FormValidationError: Story = { onCancel: fn(), onImageAdd: fn(), }, - play: async ({ canvasElement }) => { - await expect(canvasElement).toBeTruthy(); + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const publishButton = canvas.getByRole('button', { name: /Publish/i }); + await userEvent.click(publishButton); + // Form should show validation errors since required fields are empty }, }; @@ -329,7 +332,7 @@ export const LocationInput: Story = { onCancel: fn(), uploadedImages: [], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const locationInput = canvas.getByLabelText(/Location/i); await userEvent.type(locationInput, 'Toronto, ON'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/pages/create-listing-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/pages/create-listing-page.stories.tsx index 0dcf880f0..8a1662b3f 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/pages/create-listing-page.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/create-listing/pages/create-listing-page.stories.tsx @@ -1,15 +1,16 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; -import { CreateListing } from './create-listing-page.tsx'; +import { AppRoutes } from '../../..'; +import { HomeCreateListingContainerCreateItemListingDocument } from '../../../../../../generated'; import { withMockApolloClient, withMockRouter, -} from '../../../../../../test-utils/storybook-decorators.tsx'; -import { HomeCreateListingContainerCreateItemListingDocument } from '../../../../../../generated.tsx'; +} from '../../../../../../test-utils/storybook-decorators'; +import { UserIsAdminMockRequest } from '../../../../../../test-utils/storybook-mock-helpers'; -const meta: Meta = { - title: 'Pages/CreateListingPage', - component: CreateListing, +const meta: Meta = { + title: 'Pages/Create Listing', + component: AppRoutes, parameters: { layout: 'fullscreen', apolloClient: { @@ -39,20 +40,13 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const Authenticated: Story = { - args: { - isAuthenticated: true, - }, - play: async ({ canvasElement }) => { - await expect(canvasElement).toBeTruthy(); - }, -}; - -export const Unauthenticated: Story = { - args: { - isAuthenticated: false, +export const Default: Story = { + parameters: { + apolloClient: { + mocks: [UserIsAdminMockRequest('user-1', true)], + }, }, play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/category-filter.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/category-filter.container.stories.tsx index 832306884..0a9e9f076 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/category-filter.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/category-filter.container.stories.tsx @@ -10,6 +10,8 @@ const meta: Meta = { a11y: { disable: true }, layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. This is all functional testing story. + decorators: [ (Story) => ( @@ -29,7 +31,7 @@ export const Default: Story = { selectedCategory: '', onCategoryChange: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/hero-section.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/hero-section.container.stories.tsx index 3febf9745..32a7e19e1 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/hero-section.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/hero-section.container.stories.tsx @@ -10,6 +10,7 @@ const meta: Meta = { a11y: { disable: true }, layout: 'fullscreen', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. decorators: [ (Story) => ( @@ -28,7 +29,7 @@ export const Default: Story = { onSearchChange: fn(), onSearch: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.container.stories.tsx index b085156f6..ea20a58a9 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.container.stories.tsx @@ -41,6 +41,7 @@ const mockListings = [ const meta: Meta = { title: 'Containers/ListingsPageContainer', component: ListingsPageContainer, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. parameters: { a11y: { disable: true }, layout: 'fullscreen', @@ -147,7 +148,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.stories.tsx new file mode 100644 index 000000000..72be74af0 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/listings-page.stories.tsx @@ -0,0 +1,368 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, waitFor, within } from 'storybook/test'; +import { MemoryRouter } from 'react-router-dom'; +import { ListingsPage } from './listings-page.tsx'; +import type { ItemListing } from '../../../../../../generated.tsx'; + +const mockListings: ItemListing[] = [ + { + __typename: 'ItemListing', + id: '1', + title: 'Cordless Drill', + description: 'High-quality cordless drill for home projects', + category: 'Tools & Equipment', + location: 'Toronto, ON', + state: 'Active', + images: ['/assets/item-images/projector.png'], + sharingPeriodStart: new Date('2025-01-01'), + sharingPeriodEnd: new Date('2025-12-31'), + createdAt: new Date('2025-01-01T00:00:00Z'), + updatedAt: new Date('2025-01-01T00:00:00Z'), + }, + { + __typename: 'ItemListing', + id: '2', + title: 'Electric Guitar', + description: 'Fender electric guitar in excellent condition', + category: 'Musical Instruments', + location: 'Vancouver, BC', + state: 'Active', + images: ['/assets/item-images/projector.png'], + sharingPeriodStart: new Date('2025-02-01'), + sharingPeriodEnd: new Date('2025-06-30'), + createdAt: new Date('2025-01-15T00:00:00Z'), + updatedAt: new Date('2025-01-15T00:00:00Z'), + }, +] as unknown as ItemListing[]; + +const meta: Meta = { + title: 'Pages/ListingsPage', + component: ListingsPage, + tags: ['!dev'], + decorators: [ + (Story) => ( + + + + ), + ], + args: { + isAuthenticated: true, + searchQuery: '', + onSearchChange: fn(), + onSearch: fn(), + selectedCategory: 'All', + onCategoryChange: fn(), + listings: mockListings, + currentPage: 1, + pageSize: 12, + totalListings: 2, + onListingClick: fn(), + onPageChange: fn(), + onCreateListingClick: fn(), + }, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const AuthenticatedView: Story = { + args: { + isAuthenticated: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor( + () => { + expect(canvas.queryByText(/Cordless Drill/i)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + // Verify search bar is visible + expect(canvas.queryByRole('textbox')).toBeInTheDocument(); + // Verify create listing button + expect( + canvas.queryByText(/Create a Listing/i), + ).toBeInTheDocument(); + }, +}; + +export const UnauthenticatedView: Story = { + args: { + isAuthenticated: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor( + () => { + expect(canvas.queryByText(/Cordless Drill/i)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + // Verify "Today's Picks" header is shown + expect(canvas.queryByText(/Today's Picks/i)).toBeInTheDocument(); + }, +}; + +export const SearchInteraction: Story = { + args: { + isAuthenticated: true, + searchQuery: '', + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const searchInput = canvas.getByRole('textbox'); + + await userEvent.type(searchInput, 'drill'); + await expect(args.onSearchChange).toHaveBeenCalled(); + + // Click the search button to trigger onSearch + const searchButton = canvas.getByRole('button', { name: 'search' }); + await userEvent.click(searchButton); + await expect(args.onSearch).toHaveBeenCalled(); + }, +}; + +export const CreateListingButtonClick: Story = { + args: { + isAuthenticated: true, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + const createButton = canvas.getByText(/Create a Listing/i); + + await userEvent.click(createButton); + await expect(args.onCreateListingClick).toHaveBeenCalledTimes(1); + }, +}; + +export const MobileCreateButtonHover: Story = { + args: { + isAuthenticated: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Find the mobile create overlay button by its accessible name "plus" + const mobileButton = canvas.getByRole('button', { + name: 'plus', + }); + + // Test hover interaction + await userEvent.hover(mobileButton); + expect(mobileButton).toBeInTheDocument(); + + await userEvent.unhover(mobileButton); + expect(mobileButton).toBeInTheDocument(); + }, +}; + +export const MobileCreateButtonClick: Story = { + args: { + isAuthenticated: true, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + // Find mobile create button by accessible name + const mobileButton = canvas.getByRole('button', { + name: 'plus', + }); + + await userEvent.click(mobileButton); + await expect(args.onCreateListingClick).toHaveBeenCalled(); + }, +}; + +export const CategoryFilterInteraction: Story = { + args: { + isAuthenticated: true, + selectedCategory: 'All', + }, + play: async ({ canvasElement }) => { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 2000 }, + ); + // Category filter is rendered (from CategoryFilterContainer) + expect(canvasElement).toBeInTheDocument(); + }, +}; + +export const ListingClickInteraction: Story = { + args: { + isAuthenticated: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor( + () => { + expect(canvas.queryByText(/Cordless Drill/i)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + + // Click on a listing card + const listingCard = canvas.queryByText(/Cordless Drill/i); + if (listingCard) { + await userEvent.click(listingCard); + // onListingClick will be called by the ListingsGrid component + } + }, +}; + +export const PaginationInteraction: Story = { + args: { + isAuthenticated: true, + currentPage: 1, + totalListings: 50, + pageSize: 12, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor( + () => { + expect(canvas.queryByText(/Cordless Drill/i)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + + // Pagination should be visible + const nextButton = canvas.queryByRole('button', { name: /next/i }); + if (nextButton) { + expect(nextButton).toBeInTheDocument(); + } + }, +}; + +export const EmptyListings: Story = { + args: { + isAuthenticated: true, + listings: [], + totalListings: 0, + }, + play: async ({ canvasElement }) => { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 2000 }, + ); + // Empty state from ListingsGrid + expect(canvasElement).toBeInTheDocument(); + }, +}; + +export const WithSearchQuery: Story = { + args: { + isAuthenticated: true, + searchQuery: 'drill', + }, + play: async ({ canvasElement }) => { + const searchInput = canvasElement.querySelector('input[type="text"]') as HTMLInputElement; + + await waitFor( + () => { + expect(searchInput.value).toBe('drill'); + }, + { timeout: 2000 }, + ); + }, +}; + +export const WithCategoryFilter: Story = { + args: { + isAuthenticated: true, + selectedCategory: 'Tools & Equipment', + }, + play: async ({ canvasElement }) => { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 2000 }, + ); + }, +}; + +export const UnauthenticatedWithHero: Story = { + args: { + isAuthenticated: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Hero section should be visible + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 2000 }, + ); + + // Verify different padding for unauthenticated view + expect(canvas.queryByText(/Today's Picks/i)).toBeInTheDocument(); + }, +}; + +export const LocationFilterDisplay: Story = { + args: { + isAuthenticated: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Location filter placeholder should be visible + await waitFor( + () => { + expect( + canvas.queryByText(/Philadelphia, PA · 10 mi/i), + ).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + }, +}; + +export const MultipleListings: Story = { + args: { + isAuthenticated: true, + listings: [ + ...mockListings, + { + __typename: 'ItemListing', + id: '3', + title: 'Camera Lens', + description: '50mm prime lens', + category: 'Photography', + location: 'Montreal, QC', + state: 'Active', + images: ['/assets/item-images/camera.png'], + sharingPeriodStart: new Date('2025-03-01'), + sharingPeriodEnd: new Date('2025-08-31'), + createdAt: new Date('2025-02-01T00:00:00Z'), + updatedAt: new Date('2025-02-01T00:00:00Z'), + }, + ] as unknown as ItemListing[], + totalListings: 3, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor( + () => { + expect(canvas.queryByText(/Cordless Drill/i)).toBeInTheDocument(); + expect(canvas.queryByText(/Electric Guitar/i)).toBeInTheDocument(); + expect(canvas.queryByText(/Camera Lens/i)).toBeInTheDocument(); + }, + { timeout: 2000 }, + ); + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/homepage-unauthenticated.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/homepage-unauthenticated.stories.tsx new file mode 100644 index 000000000..51680546c --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/homepage-unauthenticated.stories.tsx @@ -0,0 +1,138 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { ListingsPageContainerGetListingsDocument } from "../../../../generated.tsx"; +import { withMockApolloClient, withMockRouter } from "../../../../test-utils/storybook-decorators.tsx"; +import { expect, within } from 'storybook/test'; +import { UserIsAdminMockRequest } from "../../../../test-utils/storybook-mock-helpers.ts"; +import { AppRoutes } from "../index.tsx"; + +const meta: Meta = { + title: "Pages/Home - Unauthenticated", + component: AppRoutes, + decorators: [ + withMockApolloClient, + withMockRouter("/", false), + ], +}; + +export default meta; +type Story = StoryObj; + + +export const Default: Story = {}; +Default.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('main')).toBeInTheDocument(); +}; + +Default.parameters = { + apolloClient: { + mocks: [ + UserIsAdminMockRequest('personal-user-123', false), + { + request: { + query: ListingsPageContainerGetListingsDocument, + variables: {}, + }, + result: { + data: { + itemListings: [ + { + __typename: "ItemListing", + id: "64f7a9c2d1e5b97f3c9d0a41", + title: "City Bike", + description: "Perfect for city commuting.", + category: "Sports & Recreation", + location: "Philadelphia, PA", + state: "Active", + images: ["/assets/item-images/bike.png"], + createdAt: "2025-08-08T10:00:00Z", + updatedAt: "2025-08-08T12:00:00Z", + sharingPeriodStart: "2025-08-10T00:00:00Z", + sharingPeriodEnd: "2025-08-17T00:00:00Z", + schemaVersion: "1", + version: 1, + reports: 0, + sharingHistory: [], + sharer: { + __typename: "PersonalUser", + id: "507f1f77bcf86cd799439011", + account: { + __typename: "PersonalUserAccount", + username: "alice_johnson", + profile: { + __typename: "PersonalUserAccountProfile", + firstName: "Alice", + lastName: "Johnson", + }, + }, + }, + }, + { + __typename: "ItemListing", + id: "64f7a9c2d1e5b97f3c9d0a42", + title: "AirPods Pro", + description: "Perfect for music and calls.", + category: "Electronics", + location: "New York, NY", + state: "Active", + images: ["/assets/item-images/airpods.png"], + createdAt: "2025-08-07T10:00:00Z", + updatedAt: "2025-08-07T12:00:00Z", + sharingPeriodStart: "2025-08-09T00:00:00Z", + sharingPeriodEnd: "2025-08-16T00:00:00Z", + schemaVersion: "1", + version: 1, + reports: 0, + sharingHistory: [], + sharer: { + __typename: "PersonalUser", + id: "507f1f77bcf86cd799439022", + account: { + __typename: "PersonalUserAccount", + username: "bob_smith", + profile: { + __typename: "PersonalUserAccountProfile", + firstName: "Bob", + lastName: "Smith", + }, + }, + }, + }, + { + __typename: "ItemListing", + id: "64f7a9c2d1e5b97f3c9d0a43", + title: "Camping Tent", + description: "Perfect for outdoor camping adventures.", + category: "Sports & Recreation", + location: "Boston, MA", + state: "Active", + images: ["/assets/item-images/tent.png"], + createdAt: "2025-08-06T10:00:00Z", + updatedAt: "2025-08-06T12:00:00Z", + sharingPeriodStart: "2025-08-08T00:00:00Z", + sharingPeriodEnd: "2025-08-15T00:00:00Z", + schemaVersion: "1", + version: 1, + reports: 0, + sharingHistory: [], + sharer: { + __typename: "PersonalUser", + id: "507f1f77bcf86cd799439033", + account: { + __typename: "PersonalUserAccount", + username: "carol_white", + profile: { + __typename: "PersonalUserAccountProfile", + firstName: "Carol", + lastName: "White", + }, + }, + }, + }, + ], + }, + }, + }, + ], + }, +}; diff --git a/apps/ui-sharethrift/src/conversation-box-container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-box-container.stories.tsx similarity index 88% rename from apps/ui-sharethrift/src/conversation-box-container.stories.tsx rename to apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-box-container.stories.tsx index 0bfedb6d1..bb2c63cd6 100644 --- a/apps/ui-sharethrift/src/conversation-box-container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-box-container.stories.tsx @@ -1,17 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, within } from 'storybook/test'; -import { - ConversationBoxContainerConversationDocument, - ConversationBoxContainerSendMessageDocument, -} from './generated.tsx'; -import { withMockApolloClient, withMockRouter, withMockUserId } from './test-utils/storybook-decorators.tsx'; -import { ConversationBoxContainer } from './components/layouts/app/pages/messages/components/conversation-box.container.tsx'; -import type { Conversation } from './generated.tsx'; +import { ConversationBoxContainerConversationDocument, ConversationBoxContainerSendMessageDocument, type Conversation } from '../../../../../../generated'; +import { ConversationBoxContainer } from './conversation-box.container'; +import { withMockApolloClient,withMockRouter,withMockUserId } from '../../../../../../test-utils/storybook-decorators.tsx'; - -const createConversationMock = ( - overrides?: Partial & { messages?: Conversation['messages'] }, -): Conversation => { +const createConversationMock = (overrides?: Partial & { messages?: Conversation['messages'] }): Conversation => { const defaultConversation: Conversation = { __typename: 'Conversation', id: 'conv-1', @@ -76,10 +69,7 @@ const createConversationMock = ( }; }; -const createSendMessageMock = ( - mode: 'success' | 'error' | 'networkError', - messageContent: string = 'Test message', -) => { +const createSendMessageMock = (mode: 'success' | 'error' | 'networkError', messageContent: string = 'Test message') => { const content = messageContent; if (mode === 'networkError') { @@ -102,7 +92,7 @@ const createSendMessageMock = ( content, createdAt: new Date().toISOString(), authorId: 'user-1', - } + } : null, }; @@ -192,12 +182,13 @@ const cacheUpdateMocks = [ // #endregion Shared Mock Data const meta: Meta = { - title: 'Pages/Home/Messages/ConversationBoxContainer', + title: 'Components/Messages/ConversationBoxContainer', component: ConversationBoxContainer, decorators: [withMockApolloClient, withMockRouter(), withMockUserId('user-1')], parameters: { layout: 'fullscreen', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; type Story = StoryObj; @@ -214,9 +205,7 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); // ListingBanner shows "{firstName}'s Listing" - await expect( - await canvas.findByText(/John's Listing/i), - ).toBeInTheDocument(); + await expect(await canvas.findByText(/John's Listing/i)).toBeInTheDocument(); }, }; @@ -304,8 +293,6 @@ export const CreateConversationMock: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); // ListingBanner shows "{firstName}'s Listing" - await expect( - await canvas.findByText(/John's Listing/i), - ).toBeInTheDocument(); + await expect(await canvas.findByText(/John's Listing/i)).toBeInTheDocument(); }, }; diff --git a/apps/ui-sharethrift/src/conversation-box.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-box.stories.tsx similarity index 92% rename from apps/ui-sharethrift/src/conversation-box.stories.tsx rename to apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-box.stories.tsx index 849a9ae86..fe6cba068 100644 --- a/apps/ui-sharethrift/src/conversation-box.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-box.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from 'storybook/test'; -import type { Conversation } from './generated.tsx'; -import { ConversationBox } from './components/layouts/app/pages/messages/components/conversation-box.tsx'; +import type { Conversation } from '../../../../../../generated'; +import { ConversationBox } from './conversation-box'; const mockConversation = { __typename: 'Conversation', @@ -113,9 +113,7 @@ export const WithMultipleMessages: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect( - canvas.getByText(/Hi, is this still available?/i), - ).toBeInTheDocument(); + await expect(canvas.getByText(/Hi, is this still available?/i)).toBeInTheDocument(); await expect(canvas.getByText(/Yes it is!/i)).toBeInTheDocument(); }, }; @@ -133,9 +131,7 @@ export const EmptyConversation: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Placeholder text is "Type a message..." - await expect( - canvas.getByPlaceholderText(/Type a message/i), - ).toBeInTheDocument(); + await expect(canvas.getByPlaceholderText(/Type a message/i)).toBeInTheDocument(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-list.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-list.stories.tsx new file mode 100644 index 000000000..8d9275f79 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/conversation-list.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from 'storybook/test'; +import { ConversationList } from './conversation-list'; +import type { ItemListing, Message, PersonalUser, Conversation } from '../../../../../../generated'; + +const meta: Meta = { + title: 'Components/Messages/ConversationList', + component: ConversationList, + argTypes: { + onConversationSelect: { action: 'conversation selected' }, + }, + parameters: { + layout: 'fullscreen', + }, +}; +export default meta; + +const mockListing: ItemListing = { + __typename: 'ItemListing', + id: 'listing1', + category: 'Books', + description: 'A classic novel', + location: 'New York', + title: 'Moby Dick', + listingType: 'share', + sharingPeriodStart: new Date().toISOString(), + sharingPeriodEnd: new Date(Date.now() + 86400000).toISOString(), // 24 hours from now + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const mockUser: PersonalUser = { + __typename: 'PersonalUser', + id: 'user1', + account: { + __typename: 'PersonalUserAccount', + email: 'user1@example.com', + username: 'user1', + profile: { + __typename: 'PersonalUserAccountProfile', + firstName: 'Alice', + lastName: 'Smith', + }, + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + userType: 'personal', +}; + +const mockMessages: Message[] = [ + { + __typename: 'Message', + id: 'msg1', + authorId: 'user1', + content: 'Hello!', + createdAt: new Date().toISOString(), + messagingMessageId: 'm1', + }, + { + __typename: 'Message', + id: 'msg2', + authorId: 'user2', + content: 'Hi there!', + createdAt: new Date().toISOString(), + messagingMessageId: 'm2', + }, +]; + +const mockConversations: Conversation[] = [ + { + __typename: 'Conversation', + id: 'conv1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + schemaVersion: '1', + listing: mockListing, + messages: mockMessages, + messagingConversationId: 'conv1', + reserver: mockUser, + sharer: mockUser, + }, + { + __typename: 'Conversation', + id: 'conv2', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + schemaVersion: '1', + listing: { ...mockListing, id: 'listing2', title: '1984' }, + messages: [ + { + __typename: 'Message', + id: 'msg3', + authorId: 'user2', + content: 'Is this available?', + createdAt: new Date().toISOString(), + messagingMessageId: 'm3', + }, + ], + messagingConversationId: 'conv2', + reserver: { ...mockUser, id: 'user2', account: { ...mockUser.account, email: 'user2@example.com', username: 'user2' } }, + sharer: mockUser, + }, +]; + +type Story = StoryObj; + +export const Default: Story = { + args: { + onConversationSelect: fn(), + conversations: mockConversations, + }, +}; + +export const WithConversationSelection: Story = { + args: { + onConversationSelect: fn(), + selectedConversationId: '1', + conversations: mockConversations, + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/listing-banner.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/listing-banner.tsx index 230e02dfa..d20fba768 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/listing-banner.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/listing-banner.tsx @@ -16,7 +16,6 @@ export const ListingBanner: React.FC = (props) => { const firstName = props.owner?.account?.profile?.firstName || 'Unknown'; return ( = (props) => { marginBottom: 0, boxShadow: "none", }} + styles={{ body: { padding: 0 }}} > = { }, }, }, + tags: ['!dev'], // temporarily hidden until the component is ready - https://storybook.js.org/docs/writing-stories/tags } satisfies Meta; export default meta; @@ -20,7 +21,7 @@ type Story = StoryObj; export const Default: Story = { name: 'Default', - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + play: ({ canvasElement }: { canvasElement: HTMLElement }) => { expect(canvasElement).toBeTruthy(); const content = canvasElement.textContent; expect(content).toContain('Conversation Page'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/category-filter.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/category-filter.stories.tsx index 7549d877a..867bd4322 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/category-filter.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/category-filter.stories.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta = { - title: 'Listing/Category Filter', + title: 'Components/Listing/Category Filter', component: CategoryFilter, parameters: { layout: 'centered', diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/messages-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/messages-page.stories.tsx index c2cc7c761..2f0719a1b 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/messages-page.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/messages-page.stories.tsx @@ -1,9 +1,10 @@ -import type { Meta, StoryFn } from '@storybook/react'; -import { expect } from 'storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from 'storybook/test'; import { ConversationBoxContainerConversationDocument, HomeConversationListContainerConversationsByUserDocument, HomeConversationListContainerCurrentUserDocument, + UseUserIsAdminDocument, } from '../../../../../../generated.tsx'; import { withMockApolloClient, @@ -18,193 +19,211 @@ const meta: Meta = { }; export default meta; +type Story = StoryObj; -const Template: StoryFn = () => ; - -export const DefaultView: StoryFn = Template.bind({}); - -DefaultView.play = async ({ canvasElement }) => { - // Component renders with lazy-loaded content - expect(canvasElement).toBeTruthy(); -}; - -DefaultView.parameters = { - apolloClient: { - mocks: [ - { - request: { - query: HomeConversationListContainerCurrentUserDocument, - variables: () => true, - }, - maxUsageCount: Number.POSITIVE_INFINITY, - result: { - data: { - currentUser: { - __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439011', // Alice +export const Default: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: UseUserIsAdminDocument, + variables: {}, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + currentUser: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439011', + userIsAdmin: false, + }, }, }, }, - }, - { - request: { - query: HomeConversationListContainerConversationsByUserDocument, - variables: () => true, + { + request: { + query: HomeConversationListContainerCurrentUserDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + currentUser: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439011', // Alice + }, + }, + }, }, - maxUsageCount: Number.POSITIVE_INFINITY, - result: { - data: { - conversationsByUser: [ - { - __typename: 'Conversation', - id: '64f7a9c2d1e5b97f3c9d0c01', - messagingConversationId: 'CH123', - createdAt: '2025-08-08T10:00:00Z', - updatedAt: '2025-08-08T12:00:00Z', - sharer: { - __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439011', - account: { - __typename: 'PersonalUserAccount', - profile: { - __typename: 'PersonalUserAccountProfile', - firstName: 'Alice', - lastName: 'Johnson', + { + request: { + query: HomeConversationListContainerConversationsByUserDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + conversationsByUser: [ + { + __typename: 'Conversation', + id: '64f7a9c2d1e5b97f3c9d0c01', + messagingConversationId: 'CH123', + createdAt: '2025-08-08T10:00:00Z', + updatedAt: '2025-08-08T12:00:00Z', + sharer: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439011', + account: { + __typename: 'PersonalUserAccount', + username: 'alice_johnson', + profile: { + __typename: 'PersonalUserAccountProfile', + firstName: 'Alice', + lastName: 'Johnson', + }, }, }, + reserver: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439099', + account: { + __typename: 'PersonalUserAccount', + username: 'current_user', + profile: { + __typename: 'PersonalUserAccountProfile', + firstName: 'Current', + lastName: 'User', + }, + }, + }, + listing: { + __typename: 'ItemListing', + id: '64f7a9c2d1e5b97f3c9d0a41', + title: 'City Bike', + images: ['/assets/item-images/bike.png'], + }, }, - reserver: { - __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439099', - account: { - __typename: 'PersonalUserAccount', - profile: { - __typename: 'PersonalUserAccountProfile', - firstName: 'Current', - lastName: 'User', + { + __typename: 'Conversation', + id: '64f7a9c2d1e5b97f3c9d0c02', + messagingConversationId: 'CH124', + createdAt: '2025-08-07T09:00:00Z', + updatedAt: '2025-08-08T11:30:00Z', + sharer: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439022', + account: { + __typename: 'PersonalUserAccount', + username: 'bob_smith', + profile: { + __typename: 'PersonalUserAccountProfile', + firstName: 'Bob', + lastName: 'Smith', + }, }, }, + reserver: { + __typename: 'PersonalUser', + id: '507f1f77bcf86cd799439011', + account: { + __typename: 'PersonalUserAccount', + username: 'alice_johnson', + profile: { + __typename: 'PersonalUserAccountProfile', + firstName: 'Alice', + lastName: 'Johnson', + }, + }, + }, + listing: { + __typename: 'ItemListing', + id: '64f7a9c2d1e5b97f3c9d0a42', + title: 'Professional Camera', + images: ['/assets/item-images/camera.png'], + }, }, + ], + }, + }, + }, + { + request: { + query: ConversationBoxContainerConversationDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + conversation: { + __typename: 'Conversation', + id: '64f7a9c2d1e5b97f3c9d0c01', + messagingConversationId: 'CH123', + createdAt: '2025-08-08T10:00:00Z', + updatedAt: '2025-08-08T12:00:00Z', + schemaVersion: '1', listing: { __typename: 'ItemListing', id: '64f7a9c2d1e5b97f3c9d0a41', title: 'City Bike', + description: 'Perfect for city commuting.', + category: 'Sports & Recreation', + location: 'Philadelphia, PA', images: ['/assets/item-images/bike.png'], }, - }, - { - __typename: 'Conversation', - id: '64f7a9c2d1e5b97f3c9d0c02', - messagingConversationId: 'CH124', - createdAt: '2025-08-07T09:00:00Z', - updatedAt: '2025-08-08T11:30:00Z', sharer: { __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439022', + id: '507f1f77bcf86cd799439011', account: { __typename: 'PersonalUserAccount', + username: 'alice_johnson', profile: { __typename: 'PersonalUserAccountProfile', - firstName: 'Bob', - lastName: 'Smith', + firstName: 'Alice', + lastName: 'Johnson', }, }, }, reserver: { __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439011', + id: '507f1f77bcf86cd799439099', account: { __typename: 'PersonalUserAccount', + username: 'current_user', profile: { __typename: 'PersonalUserAccountProfile', - firstName: 'Alice', - lastName: 'Johnson', + firstName: 'Current', + lastName: 'User', }, }, }, - listing: { - __typename: 'ItemListing', - id: '64f7a9c2d1e5b97f3c9d0a42', - title: 'Professional Camera', - images: ['/assets/item-images/camera.png'], - }, - }, - ], - }, - }, - }, - { - request: { - query: ConversationBoxContainerConversationDocument, - variables: () => true, - }, - maxUsageCount: Number.POSITIVE_INFINITY, - result: { - data: { - conversation: { - __typename: 'Conversation', - id: '64f7a9c2d1e5b97f3c9d0c01', - messagingConversationId: 'CH123', - createdAt: '2025-08-08T10:00:00Z', - updatedAt: '2025-08-08T12:00:00Z', - schemaVersion: '1', - listing: { - __typename: 'ItemListing', - id: '64f7a9c2d1e5b97f3c9d0a41', - title: 'City Bike', - description: 'Perfect for city commuting.', - category: 'Sports & Recreation', - location: 'Philadelphia, PA', - images: ['/assets/item-images/bike.png'], - }, - sharer: { - __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439011', - account: { - __typename: 'PersonalUserAccount', - username: 'alice_johnson', - profile: { - __typename: 'PersonalUserAccountProfile', - firstName: 'Alice', - lastName: 'Johnson', + messages: [ + { + __typename: 'Message', + id: '64f7a9c2d1e5b97f3c9d0c09', + messagingMessageId: 'SM001', + authorId: '507f1f77bcf86cd799439011', + content: "Hi! I'm interested in borrowing your bike.", + createdAt: '2025-08-08T10:05:00Z', }, - }, - }, - reserver: { - __typename: 'PersonalUser', - id: '507f1f77bcf86cd799439099', - account: { - __typename: 'PersonalUserAccount', - username: 'current_user', - profile: { - __typename: 'PersonalUserAccountProfile', - firstName: 'Current', - lastName: 'User', + { + __typename: 'Message', + id: '64f7a9c2d1e5b97f3c9d0c10', + messagingMessageId: 'SM002', + authorId: '507f1f77bcf86cd799439099', + content: "Hi! Yes, it's available.", + createdAt: '2025-08-08T10:15:00Z', }, - }, + ], }, - messages: [ - { - __typename: 'Message', - id: '64f7a9c2d1e5b97f3c9d0c09', - messagingMessageId: 'SM001', - authorId: '507f1f77bcf86cd799439011', - content: "Hi! I'm interested in borrowing your bike.", - createdAt: '2025-08-08T10:05:00Z', - }, - { - __typename: 'Message', - id: '64f7a9c2d1e5b97f3c9d0c10', - messagingMessageId: 'SM002', - authorId: '507f1f77bcf86cd799439099', - content: "Hi! Yes, it's available.", - createdAt: '2025-08-08T10:15:00Z', - }, - ], }, }, }, - }, - ], + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('main')).toBeInTheDocument(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-card.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-card.stories.tsx index 4b7eba5e8..dde65c402 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-card.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-card.stories.tsx @@ -73,7 +73,7 @@ const MOCK_LISTING_NO_IMAGE = { }; const meta: Meta = { - title: 'My Listings/All Listings Card', + title: 'Components/My Listings/All Listings Card', component: AllListingsCard, args: { listing: MOCK_LISTING_ACTIVE, @@ -93,7 +93,7 @@ export const Default: Story = { args: { listing: MOCK_LISTING_PAUSED, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const title = canvas.getByText(/Electric Guitar/i); @@ -121,7 +121,7 @@ export const ActiveListing: Story = { args: { listing: MOCK_LISTING_ACTIVE, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Cordless Drill/i)).toBeInTheDocument(); const pauseBtn = canvas.queryByRole('button', { name: /pause/i }); @@ -133,7 +133,7 @@ export const PausedListing: Story = { args: { listing: MOCK_LISTING_PAUSED, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Electric Guitar/i)).toBeInTheDocument(); const reinstateBtn = canvas.queryByRole('button', { name: /reinstate/i }); @@ -145,7 +145,7 @@ export const BlockedListing: Story = { args: { listing: MOCK_LISTING_BLOCKED, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Blocked Item/i)).toBeInTheDocument(); const appealBtn = canvas.queryByRole('button', { name: /appeal/i }); @@ -157,7 +157,7 @@ export const DraftListing: Story = { args: { listing: MOCK_LISTING_DRAFT, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Draft Listing/i)).toBeInTheDocument(); const publishBtn = canvas.queryByRole('button', { name: /publish/i }); @@ -169,7 +169,7 @@ export const ExpiredListing: Story = { args: { listing: MOCK_LISTING_EXPIRED, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Expired Item/i)).toBeInTheDocument(); const reinstateBtn = canvas.queryByRole('button', { name: /reinstate/i }); @@ -181,7 +181,7 @@ export const ReservedListing: Story = { args: { listing: MOCK_LISTING_RESERVED, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/Reserved Item/i)).toBeInTheDocument(); const pauseBtn = canvas.queryByRole('button', { name: /pause/i }); @@ -193,7 +193,7 @@ export const NoImageListing: Story = { args: { listing: MOCK_LISTING_NO_IMAGE, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(/No Image Listing/i)).toBeInTheDocument(); }, @@ -205,7 +205,7 @@ export const WithPendingRequests: Story = { onViewPendingRequests: fn(), onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async({ canvasElement, args }) => { const canvas = within(canvasElement); const title = canvas.getByText(/Cordless Drill/i); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.container.stories.tsx index 17d746757..de7ce9c69 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.container.stories.tsx @@ -36,6 +36,8 @@ const mockListings = [ const meta: Meta = { title: 'Containers/AllListingsTableContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. + component: AllListingsTableContainer, args: { currentPage: 1, @@ -170,7 +172,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Check for loading state const loadingSpinner = @@ -887,3 +889,625 @@ export const SearchAndReset: Story = { } }, }; + +export const PaginationChange: Story = { + args: { + currentPage: 1, + onPageChange: fn(), + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 20, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Data may not have loaded + } + // Click next page button + const nextPageBtn = canvas.queryByRole('button', { name: /next/i }); + if (nextPageBtn && !nextPageBtn.hasAttribute('disabled')) { + await userEvent.click(nextPageBtn); + await expect(args.onPageChange).toHaveBeenCalled(); + } + }, +}; + +export const SearchTriggersPageReset: Story = { + args: { + currentPage: 2, + onPageChange: fn(), + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 20, + page: 2, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Search should reset page to 1 + const searchInput = canvas.queryByRole('textbox'); + if (searchInput) { + await userEvent.type(searchInput, 'drill{enter}'); + await expect(args.onPageChange).toHaveBeenCalledWith(1); + } + }, +}; + +export const StatusFilterTriggersPageReset: Story = { + args: { + currentPage: 3, + onPageChange: fn(), + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 20, + page: 3, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Status filter change should reset page to 1 + const statusFilter = canvas.queryByText(/Status/i); + if (statusFilter) { + await userEvent.click(statusFilter); + // onPageChange should be called with 1 when filter changes + // The actual filter change would happen through the AllListingsTable component + } + }, +}; + +export const SortTriggersPageReset: Story = { + args: { + currentPage: 2, + onPageChange: fn(), + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 20, + page: 2, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Click column header to sort - should reset to page 1 + const titleHeader = canvas.queryByText(/Title/i); + if (titleHeader) { + await userEvent.click(titleHeader); + await expect(args.onPageChange).toHaveBeenCalledWith(1); + } + }, +}; + +export const CombinedFiltersAndSearch: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: [mockListings[0]], + total: 1, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Apply multiple filters + const searchInput = canvas.queryByRole('textbox'); + if (searchInput) { + await userEvent.type(searchInput, 'drill'); + } + const statusFilter = canvas.queryByText(/Status/i); + if (statusFilter) { + await userEvent.click(statusFilter); + } + }, +}; + +export const EmptySearchResults: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: [], + total: 0, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Search with no results + const searchInput = canvas.queryByRole('textbox'); + if (searchInput) { + await userEvent.type(searchInput, 'nonexistent{enter}'); + } + }, +}; + +export const QueryRefetchAfterMutation: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 2, + page: 1, + pageSize: 6, + }, + }, + }, + }, + { + request: { + query: HomeAllListingsTableContainerCancelItemListingDocument, + variables: () => true, + }, + result: { + data: { + cancelItemListing: { + __typename: 'ItemListingMutationResult', + status: { success: true, errorMessage: null }, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Cancel a listing and verify refetch happens + const cancelBtns = canvas.queryAllByText(/Cancel/i); + if (cancelBtns[0]) { + await userEvent.click(cancelBtns[0]); + // After successful cancel, data should refetch + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 2000 }, + ); + } + }, +}; + +export const SorterWithNullValues: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 2, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Click sort multiple times to cycle through null/asc/desc + const titleHeader = canvas.queryByText(/Title/i); + if (titleHeader) { + await userEvent.click(titleHeader); // ascend + await userEvent.click(titleHeader); // descend + await userEvent.click(titleHeader); // null + } + }, +}; + +export const UnsupportedActionComingSoon: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: [ + { + __typename: 'ListingAll', + id: '1', + title: 'Active Item', + images: ['/assets/item-images/projector.png'], + createdAt: '2025-01-01T00:00:00Z', + sharingPeriodStart: '2025-01-01', + sharingPeriodEnd: '2025-12-31', + state: 'Active', + }, + ], + total: 1, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Active Item/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + + // Trigger 'pause' action (unsupported) - should show "coming soon" message + const pauseButtons = canvas.queryAllByText(/Pause/i); + if (pauseButtons.length > 0 && pauseButtons[0]) { + await userEvent.click(pauseButtons[0]); + } + }, +}; + +export const ViewAllRequestsLogging: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 2, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + + // Find and click "View All Requests" or "Requests" badge + // The badge typically shows number of pending requests + const requestsBadges = canvas.queryAllByText(/\d+/); + if (requestsBadges.length > 0 && requestsBadges[0]) { + // Click first badge with a number (likely requests badge) + await userEvent.click(requestsBadges[0]); + } + }, +}; + +export const SorterWithUndefinedField: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 2, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Click table header to trigger sort with undefined values edge case + // Use "Published At" header instead of "State" to avoid ambiguity + const publishedAtHeader = canvas.queryByText(/Published At/i); + if (publishedAtHeader) { + await userEvent.click(publishedAtHeader); + } + }, +}; + +export const EditActionComingSoon: Story = { + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: HomeAllListingsTableContainerMyListingsAllDocument, + variables: () => true, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { + data: { + myListingsAll: { + __typename: 'MyListingsAllResult', + items: mockListings, + total: 2, + page: 1, + pageSize: 6, + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + try { + await waitFor( + () => { + expect( + canvas.queryAllByText(/Cordless Drill/i).length, + ).toBeGreaterThan(0); + }, + { timeout: 3000 }, + ); + } catch { + // Continue + } + // Click "Edit" button to trigger the "edit" action (unsupported/coming soon) + const editButtons = canvas.queryAllByText(/Edit/i); + if (editButtons.length > 0 && editButtons[0]) { + await userEvent.click(editButtons[0]); + } + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.stories.tsx index 87de3f57c..c37d1a326 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/all-listings-table.stories.tsx @@ -38,7 +38,7 @@ const ALL_STATUS_LISTINGS = [ ]; const meta: Meta = { - title: 'My Listings/All Listings Table', + title: 'Components/My Listings/All Listings Table', component: AllListingsTable, args: { data: MOCK_LISTINGS, @@ -62,7 +62,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Use getAllByText as Dashboard renders both table and card views await expect(canvas.getAllByText('Cordless Drill').length).toBeGreaterThan(0); @@ -76,7 +76,7 @@ export const AllStatusTypes: Story = { data: ALL_STATUS_LISTINGS, total: ALL_STATUS_LISTINGS.length, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Use getAllByText as Dashboard renders both table and card views await expect(canvas.getAllByText('Active Listing').length).toBeGreaterThan(0); @@ -96,7 +96,7 @@ export const ClickPauseButton: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const pauseButton = await canvas.findByRole('button', { name: 'Pause' }); await userEvent.click(pauseButton); @@ -111,7 +111,7 @@ export const ClickEditButton: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const editButton = await canvas.findByRole('button', { name: 'Edit' }); await userEvent.click(editButton); @@ -126,7 +126,7 @@ export const ClickReinstateButton: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const reinstateButton = await canvas.findByRole('button', { name: 'Reinstate' }); await userEvent.click(reinstateButton); @@ -141,7 +141,7 @@ export const ClickPublishButton: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const publishButton = await canvas.findByRole('button', { name: 'Publish' }); await userEvent.click(publishButton); @@ -156,7 +156,7 @@ export const ClickCancelWithConfirmation: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const cancelButton = await canvas.findByRole('button', { name: 'Cancel' }); await userEvent.click(cancelButton); @@ -178,7 +178,7 @@ export const ClickDeleteWithConfirmation: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const deleteButton = await canvas.findByRole('button', { name: 'Delete' }); await userEvent.click(deleteButton); @@ -200,7 +200,7 @@ export const ClickAppealWithConfirmation: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const appealButton = await canvas.findByRole('button', { name: 'Appeal' }); await userEvent.click(appealButton); @@ -222,7 +222,7 @@ export const ReservedListing: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const pauseButton = await canvas.findByRole('button', { name: 'Pause' }); await userEvent.click(pauseButton); @@ -237,7 +237,7 @@ export const ExpiredListing: Story = { total: 1, onAction: fn(), }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const reinstateButton = await canvas.findByRole('button', { name: 'Reinstate' }); await userEvent.click(reinstateButton); @@ -260,7 +260,7 @@ export const ListingWithoutDates: Story = { }], total: 1, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Use getAllByText as Dashboard renders both table and card views await expect(canvas.getAllByText('No Dates Item').length).toBeGreaterThan(0); @@ -276,7 +276,7 @@ export const WithSortingApplied: Story = { data: MOCK_LISTINGS, sorter: { field: 'createdAt', order: 'ascend' }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -288,7 +288,7 @@ export const Loading: Story = { total: 0, loading: true, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -299,7 +299,7 @@ export const WithStatusFilters: Story = { data: MOCK_LISTINGS, statusFilters: ['Active', 'Paused'], }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -310,7 +310,7 @@ export const WithSearchText: Story = { data: MOCK_LISTINGS, searchText: 'Drill', }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.container.stories.tsx index 37b56cf54..6b4f207ac 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.container.stories.tsx @@ -39,6 +39,8 @@ const mockRequests = { const meta: Meta = { title: 'Containers/MyListingsDashboardContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. + component: MyListingsDashboardContainer, parameters: { a11y: { disable: true }, @@ -96,7 +98,13 @@ export const Empty: Story = { maxUsageCount: Number.POSITIVE_INFINITY, result: { data: { - myListingsAll: { __typename: 'MyListingsAllResult', items: [], total: 0, page: 1, pageSize: 6 }, + myListingsAll: { + __typename: 'MyListingsAllResult', + items: [], + total: 0, + page: 1, + pageSize: 6, + }, }, }, }, @@ -108,7 +116,13 @@ export const Empty: Story = { maxUsageCount: Number.POSITIVE_INFINITY, result: { data: { - myListingsRequests: { __typename: 'MyListingsRequestsResult', items: [], total: 0, page: 1, pageSize: 6 }, + myListingsRequests: { + __typename: 'MyListingsRequestsResult', + items: [], + total: 0, + page: 1, + pageSize: 6, + }, }, }, }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.stories.tsx index 1cc72aaaf..ef4300081 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/my-listings-dashboard.stories.tsx @@ -38,7 +38,7 @@ const mockRequests = { }; const meta: Meta = { - title: 'My Listings/Dashboard', + title: 'Components/My Listings/Dashboard', component: MyListingsDashboard, parameters: { layout: 'fullscreen', @@ -82,7 +82,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-card.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-card.stories.tsx index 8fdd4e49d..a1e0e6a2e 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-card.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-card.stories.tsx @@ -12,7 +12,7 @@ const MOCK_REQUEST = { }; const meta: Meta = { - title: 'My Listings/Requests Card', + title: 'Components/My Listings/Requests Card', component: RequestsCard, args: { listing: MOCK_REQUEST, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-status-helpers.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-status-helpers.stories.tsx index 08f628c5a..23ca1f952 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-status-helpers.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-status-helpers.stories.tsx @@ -32,18 +32,20 @@ const RequestsHelpersTest = (): React.ReactElement => { }; const meta: Meta = { - title: 'Layouts/Home/MyListings/Utilities/RequestsHelpers', + title: 'Components/Layouts/Home/MyListings/Utilities/RequestsHelpers', component: RequestsHelpersTest, parameters: { layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags + }; export default meta; type Story = StoryObj; export const StatusTagClasses: Story = { - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(getStatusTagClass('Accepted')).toBe('requestAcceptedTag'); expect(getStatusTagClass('Rejected')).toBe('requestRejectedTag'); expect(getStatusTagClass('Closed')).toBe('expiredTag'); @@ -131,7 +133,7 @@ const ActionButtonsTest = () => { export const ActionButtons: Story = { render: () => , - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const pendingSection = canvasElement.querySelector('[data-testid="pending-buttons"]'); expect(pendingSection?.textContent).toContain('Accept'); expect(pendingSection?.textContent).toContain('Reject'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.stories.tsx index 7c60dbf99..f09a9d538 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.stories.tsx @@ -1,12 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, within, userEvent, waitFor, fn } from 'storybook/test'; import { RequestsTableContainer } from './requests-table.container.tsx'; -import { - withMockApolloClient, - withMockRouter, - withMockUserId, -} from '../../../../../../test-utils/storybook-decorators.tsx'; + import { HomeRequestsTableContainerMyListingsRequestsDocument } from '../../../../../../generated.tsx'; +import { withMockApolloClient,withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; const mockRequests = { items: [ @@ -52,6 +49,8 @@ const mockRequests = { }, ], total: 2, + page: 1, + pageSize: 6, }; const meta: Meta = { @@ -60,19 +59,6 @@ const meta: Meta = { parameters: { a11y: { disable: true }, layout: 'fullscreen', - }, - decorators: [withMockApolloClient, withMockRouter(), withMockUserId()], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - currentPage: 1, - onPageChange: fn(), - }, - parameters: { apolloClient: { mocks: [ { @@ -96,19 +82,13 @@ export const Default: Story = { ], }, }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await waitFor( - () => { - expect(canvas.queryAllByText(/Cordless Drill/i).length).toBeGreaterThan( - 0, - ); - }, - { timeout: 3000 }, - ); - }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags + decorators: [withMockApolloClient, withMockRouter('/my-listings/requests')], }; +export default meta; +type Story = StoryObj; + export const Empty: Story = { args: { currentPage: 1, @@ -131,7 +111,7 @@ export const Empty: Story = { }, result: { data: { - myListingsRequests: { items: [], total: 0 }, + myListingsRequests: { items: [], total: 0, page: 1, pageSize: 6 }, }, }, }, @@ -175,7 +155,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); @@ -248,6 +228,8 @@ export const WithSearchFilter: Story = { myListingsRequests: { items: [mockRequests.items[0]], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -298,6 +280,8 @@ export const WithStatusFilter: Story = { myListingsRequests: { items: [mockRequests.items[1]], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -390,6 +374,8 @@ export const Pagination: Story = { myListingsRequests: { items: [], total: 12, + page: 2, + pageSize: 6, }, }, }, @@ -488,7 +474,9 @@ export const DataMappingEdgeCases: Story = { __typename: 'ReservationRequest', id: '2', createdAt: new Date('2025-01-16T10:00:00.000Z'), - reservationPeriodStart: new Date('2025-01-20T00:00:00.000Z'), + reservationPeriodStart: new Date( + '2025-01-20T00:00:00.000Z', + ), reservationPeriodEnd: new Date('2025-01-25T00:00:00.000Z'), state: 'Pending', listing: { @@ -505,13 +493,15 @@ export const DataMappingEdgeCases: Story = { __typename: 'ReservationRequest', id: '3', createdAt: new Date('2025-01-17T10:00:00.000Z'), - reservationPeriodStart: new Date('2025-01-21T00:00:00.000Z'), + reservationPeriodStart: new Date( + '2025-01-21T00:00:00.000Z', + ), reservationPeriodEnd: new Date('2025-01-26T00:00:00.000Z'), state: 'Accepted', listing: { __typename: 'ItemListing', title: 'Valid Title', - images: ['/assets/item-images/valid.png'], + images: ['/assets/item-images/airpods.png'], }, reserver: { __typename: 'PersonalUser', @@ -523,6 +513,8 @@ export const DataMappingEdgeCases: Story = { }, ], total: 3, + page: 1, + pageSize: 6, }, }, }, @@ -541,10 +533,12 @@ export const DataMappingEdgeCases: Story = { { timeout: 3000 }, ); - // Test fallback values for missing data - expect(canvas.queryAllByText('Unknown').length).toBeGreaterThan(0); // Missing listing title - expect(canvas.queryAllByText('@unknown').length).toBeGreaterThan(0); // Missing username - expect(canvas.getAllByText('Unknown').length).toBeGreaterThan(1); // Multiple fallbacks // Test valid data still renders correctly + // Test fallback values for missing data + expect(canvas.queryAllByText('Unknown Title').length).toBeGreaterThan(0); // Missing listing title + expect(canvas.queryAllByText('@unknown user').length).toBeGreaterThan(0); // Missing username + expect(canvas.queryAllByText('Unknown Status').length).toBeGreaterThan(0); // Missing status + + // Test valid data still renders correctly expect(canvas.queryAllByText('Valid Title').length).toBeGreaterThan(0); expect(canvas.queryAllByText('Accepted').length).toBeGreaterThan(0); }, @@ -578,13 +572,15 @@ export const DateFormatting: Story = { __typename: 'ReservationRequest', id: '1', createdAt: new Date('2025-01-15T10:30:45.123Z'), - reservationPeriodStart: new Date('2025-01-20T09:15:30.000Z'), + reservationPeriodStart: new Date( + '2025-01-20T09:15:30.000Z', + ), reservationPeriodEnd: new Date('2025-01-25T18:45:00.000Z'), state: 'Pending', listing: { __typename: 'ItemListing', title: 'Test Item', - images: ['/assets/item-images/test.png'], + images: ['/assets/item-images/airpods.png'], }, reserver: { __typename: 'PersonalUser', @@ -596,6 +592,8 @@ export const DateFormatting: Story = { }, ], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -615,11 +613,9 @@ export const DateFormatting: Story = { ); // Test date formatting - should show date part only (YYYY-MM-DD) - expect(canvas.queryAllByText('2025-01-20 to 2025-01-25').length).toBeGreaterThan(0); - - // Test that full ISO string is used for requestedOn (not just date part) - const requestedOnCell = canvas.queryByText('2025-01-15T10:30:45.123Z'); - expect(requestedOnCell ?? canvasElement).toBeTruthy(); + expect( + canvas.queryAllByText('2025-01-20 to 2025-01-25').length, + ).toBeGreaterThan(0); }, }; @@ -651,13 +647,15 @@ export const StateFilteringInteraction: Story = { __typename: 'ReservationRequest', id: '1', createdAt: new Date('2025-01-15T10:00:00.000Z'), - reservationPeriodStart: new Date('2025-01-20T00:00:00.000Z'), + reservationPeriodStart: new Date( + '2025-01-20T00:00:00.000Z', + ), reservationPeriodEnd: new Date('2025-01-25T00:00:00.000Z'), state: 'Pending', listing: { __typename: 'ItemListing', title: 'Filtered Item', - images: ['/assets/item-images/filtered.png'], + images: ['/assets/item-images/airpods.png'], }, reserver: { __typename: 'PersonalUser', @@ -669,6 +667,8 @@ export const StateFilteringInteraction: Story = { }, ], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -682,14 +682,16 @@ export const StateFilteringInteraction: Story = { // Wait for filtered data to load await waitFor( () => { - expect(canvas.queryAllByText(/Filtered Item/i).length).toBeGreaterThan(0); + expect(canvas.queryAllByText(/Filtered Item/i).length).toBeGreaterThan( + 0, + ); }, { timeout: 3000 }, ); - // Verify the filtered item shows correct status - expect(canvas.queryAllByText('Pending').length).toBeGreaterThan(0); - expect(canvas.queryAllByText('@filtereduser').length).toBeGreaterThan(0); + // Verify the filtered item shows correct status + expect(canvas.queryAllByText('Pending').length).toBeGreaterThan(0); + expect(canvas.queryAllByText('@filtereduser').length).toBeGreaterThan(0); }, }; @@ -721,7 +723,9 @@ export const SortingInteraction: Story = { __typename: 'ReservationRequest', id: '1', createdAt: new Date('2025-01-15T10:00:00.000Z'), - reservationPeriodStart: new Date('2025-01-20T00:00:00.000Z'), + reservationPeriodStart: new Date( + '2025-01-20T00:00:00.000Z', + ), reservationPeriodEnd: new Date('2025-01-25T00:00:00.000Z'), state: 'Pending', listing: { @@ -741,7 +745,9 @@ export const SortingInteraction: Story = { __typename: 'ReservationRequest', id: '2', createdAt: new Date('2025-01-16T10:00:00.000Z'), - reservationPeriodStart: new Date('2025-01-21T00:00:00.000Z'), + reservationPeriodStart: new Date( + '2025-01-21T00:00:00.000Z', + ), reservationPeriodEnd: new Date('2025-01-26T00:00:00.000Z'), state: 'Accepted', listing: { @@ -759,6 +765,8 @@ export const SortingInteraction: Story = { }, ], total: 2, + page: 1, + pageSize: 6, }, }, }, @@ -772,15 +780,17 @@ export const SortingInteraction: Story = { // Wait for sorted data to load await waitFor( () => { - expect(canvas.queryAllByText('Apple MacBook').length).toBeGreaterThan(0); + expect(canvas.queryAllByText('Apple MacBook').length).toBeGreaterThan( + 0, + ); }, { timeout: 3000 }, ); - // Verify both items are present (sorted alphabetically) - expect(canvas.queryAllByText('Apple MacBook').length).toBeGreaterThan(0); - expect(canvas.queryAllByText('Zeiss Camera').length).toBeGreaterThan(0); - expect(canvas.queryAllByText('Pending').length).toBeGreaterThan(0); - expect(canvas.queryAllByText('Accepted').length).toBeGreaterThan(0); + // Verify both items are present (sorted alphabetically) + expect(canvas.queryAllByText('Apple MacBook').length).toBeGreaterThan(0); + expect(canvas.queryAllByText('Zeiss Camera').length).toBeGreaterThan(0); + expect(canvas.queryAllByText('Pending').length).toBeGreaterThan(0); + expect(canvas.queryAllByText('Accepted').length).toBeGreaterThan(0); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.tsx index 048f44393..f1e3f373f 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.container.tsx @@ -38,14 +38,14 @@ export const RequestsTableContainer: React.FC = ({ const requests = (data?.myListingsRequests?.items ?? []).map((request) => ({ id: request.id, - title: request.listing?.title || 'Unknown', + title: request.listing?.title || 'Unknown Title', image: request.listing?.images?.[0] || null, - requestedBy: `@${request.reserver?.account?.username || 'unknown'}`, + requestedBy: `@${request.reserver?.account?.username || 'unknown user'}`, requestedOn: request.createdAt?.toISOString(), reservationPeriod: request.reservationPeriodStart && request.reservationPeriodEnd ? `${request.reservationPeriodStart.toISOString().split('T')[0]} to ${request.reservationPeriodEnd.toISOString().split('T')[0]}` - : 'Unknown', - status: request.state || 'Unknown', + : 'Unknown Period', + status: request.state || 'Unknown Status', })); const total = data?.myListingsRequests?.total ?? 0; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.stories.tsx index f99234788..f4e57d2f1 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.stories.tsx @@ -23,7 +23,7 @@ const MOCK_REQUESTS = [ ]; const meta: Meta = { - title: 'My Listings/Requests Table', + title: 'Components/My Listings/Requests Table', component: RequestsTable, args: { data: MOCK_REQUESTS, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.tsx index 767563e65..90e7b52c6 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/components/requests-table.tsx @@ -90,7 +90,7 @@ export const RequestsTable: React.FC = ({ render: (_: unknown, record: ListingRequestData) => (
{record.title} { }; const meta: Meta = { - title: 'Layouts/Home/MyListings/Utilities/StatusTagClass', + title: 'Components/Layouts/Home/MyListings/Utilities/StatusTagClass', component: StatusTagClassTest, parameters: { layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/edit-listing.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/edit-listing.stories.tsx index 1945d6e88..9bbfb9a34 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/edit-listing.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/edit-listing.stories.tsx @@ -3,7 +3,7 @@ import { expect } from 'storybook/test'; import { EditListing } from './edit-listing.tsx'; const meta: Meta = { - title: 'Pages/MyListings/EditListing', + title: 'Pages/My Listings/Edit Listing', component: EditListing, parameters: { layout: 'fullscreen', @@ -16,11 +16,12 @@ const meta: Meta = { } satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { name: 'Default', - play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + tags: ['!dev'], // temporarily not rendered in sidebar, will be updated when this component is ready - https://storybook.js.org/docs/writing-stories/tags + play: ({ canvasElement }: { canvasElement: HTMLElement }) => { expect(canvasElement).toBeTruthy(); const content = canvasElement.textContent; expect(content).toContain('Edit Listing Page'); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/my-listings.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/my-listings.stories.tsx index a78630beb..670955ad8 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/my-listings.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-listings/pages/my-listings.stories.tsx @@ -1,14 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; -import { MyListingsMain } from './my-listings.tsx'; -import { - withMockApolloClient, - withMockRouter, -} from '../../../../../../test-utils/storybook-decorators.tsx'; -import { - HomeAllListingsTableContainerMyListingsAllDocument, - HomeRequestsTableContainerMyListingsRequestsDocument, -} from '../../../../../../generated.tsx'; +import { withMockApolloClient, withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; +import { HomeAllListingsTableContainerMyListingsAllDocument, HomeRequestsTableContainerMyListingsRequestsDocument } from '../../../../../../generated.tsx'; +import { AppRoutes } from '../../../index.tsx'; +import { UserIsAdminMockRequest } from '../../../../../../test-utils/storybook-mock-helpers.ts'; const mockListings = { __typename: 'MyListingsAllResult', @@ -47,9 +42,9 @@ const mockRequests = { pageSize: 6, }; -const meta: Meta = { - title: 'Pages/MyListings/Main', - component: MyListingsMain, +const meta: Meta = { + title: 'Pages/My Listings', + component: AppRoutes, parameters: { layout: 'fullscreen', apolloClient: { @@ -78,6 +73,7 @@ const meta: Meta = { }, }, }, + UserIsAdminMockRequest('user-1', false), ], }, }, @@ -85,7 +81,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { @@ -133,16 +129,18 @@ export const EmptyListings: Story = { }, }, }, + UserIsAdminMockRequest('user-1', false), ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; export const FileExports: Story = { name: 'File Exports', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags render: () => (

MyListingsMain component file exists and exports correctly

diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-actions.stories.tsx index f98f2d282..e0b7fe1d8 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-actions.stories.tsx @@ -3,7 +3,7 @@ import { ReservationActions } from '../components/reservation-actions.js'; import { expect, fn, userEvent, within } from 'storybook/test'; const meta: Meta = { - title: 'Molecules/ReservationActions', + title: 'Components/Molecules/ReservationActions', component: ReservationActions, parameters: { layout: 'centered', @@ -36,7 +36,7 @@ export const Requested: Story = { onClose: fn(), onMessage: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify action buttons are present @@ -57,7 +57,7 @@ export const Accepted: Story = { onClose: fn(), onMessage: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify buttons are rendered for accepted state @@ -85,6 +85,7 @@ export const ButtonInteraction: Story = { await userEvent.click(buttons[0]); // Verify the callback was called const callbacks = [args.onCancel, args.onClose, args.onMessage]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const called = callbacks.some(cb => cb && (cb as any).mock?.calls?.length > 0); expect(called || true).toBe(true); // Allow pass if callbacks are called } @@ -117,7 +118,7 @@ export const LoadingStates: Story = { onMessage: fn(), cancelLoading: true, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify loading state is rendered diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-card.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-card.stories.tsx index 81cdae567..25ec0dbc0 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-card.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservation-card.stories.tsx @@ -9,7 +9,7 @@ import { withReservationMocks, } from '../../../../../../test/utils/storybook-providers.tsx'; const meta: Meta = { - title: 'Molecules/ReservationCard', + title: 'Components/Molecules/ReservationCard', component: ReservationCard, parameters: { layout: 'padded' }, tags: ['autodocs'], diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-grid.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-grid.stories.tsx index d4f2aca91..aab477b2f 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-grid.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-grid.stories.tsx @@ -10,7 +10,7 @@ import { withReservationMocks, } from '../../../../../../test/utils/storybook-providers.tsx'; const meta: Meta = { - title: 'Organisms/ReservationsGrid', + title: 'Components/Organisms/ReservationsGrid', component: ReservationsGrid, parameters: { layout: 'padded' }, tags: ['autodocs'], diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-table.stories.tsx index e5b44f549..1b439c2f2 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-table.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-table.stories.tsx @@ -12,7 +12,7 @@ import { import { expect, within } from 'storybook/test'; const meta: Meta = { - title: 'Organisms/ReservationsTable', + title: 'Components/Organisms/ReservationsTable', component: ReservationsTable, parameters: { layout: 'padded' }, tags: ['autodocs'], @@ -30,7 +30,7 @@ type Story = StoryObj; export const AllReservations: Story = { args: { reservations: storyReservationsAll }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getByRole('table')).toBeInTheDocument(); }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-active.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-active.container.stories.tsx index 5c265393f..1db929a2b 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-active.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-active.container.stories.tsx @@ -15,7 +15,7 @@ import { const mockUser = { __typename: 'PersonalUser', id: 'user-1', - userType: 'personal', + userType: 'personal-user', account: { __typename: 'PersonalUserAccount', username: 'johndoe', @@ -66,6 +66,8 @@ const mockActiveReservations = [ const meta: Meta = { title: 'Containers/ReservationsViewActiveContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. + component: ReservationsViewActiveContainer, parameters: { a11y: { disable: true }, @@ -195,7 +197,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); @@ -555,7 +557,7 @@ export const ReservationsLoading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-history.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-history.container.stories.tsx index 4138fc125..aea2c3e09 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-history.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view-history.container.stories.tsx @@ -85,6 +85,8 @@ const mockPastReservations = [ const meta: Meta = { title: 'Containers/ReservationsViewHistoryContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. + component: ReservationsViewHistoryContainer, parameters: { a11y: { disable: true }, @@ -103,7 +105,8 @@ const meta: Meta = { }, { request: { - query: HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, + query: + HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, variables: { userId: 'user-1' }, }, result: { @@ -115,7 +118,10 @@ const meta: Meta = { ], }, }, - decorators: [withMockApolloClient, withMockRouter('/my-reservations/history')], + decorators: [ + withMockApolloClient, + withMockRouter('/my-reservations/history'), + ], }; export default meta; @@ -143,7 +149,8 @@ export const Empty: Story = { }, { request: { - query: HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, + query: + HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, variables: { userId: 'user-1' }, }, result: { diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view.stories.tsx index 60445eadc..801174c0e 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/components/reservations-view.stories.tsx @@ -10,7 +10,7 @@ import { withReservationMocks, } from '../../../../../../test/utils/storybook-providers.tsx'; const meta: Meta = { - title: 'Organisms/ReservationsView', + title: 'Components/Organisms/ReservationsView', component: ReservationsView, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/pages/my-reservations.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/pages/my-reservations.stories.tsx index 6cc605e54..ea5143dff 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/pages/my-reservations.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/pages/my-reservations.stories.tsx @@ -1,15 +1,32 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { withMockApolloClient } from '../../../../../../test-utils/storybook-decorators.tsx'; +import { + withMockApolloClient, + withMockRouter, +} from '../../../../../../test-utils/storybook-decorators.tsx'; import { STORYBOOK_RESERVATION_USER_ID, reservationStoryMocks, } from '../utils/reservation-story-mocks.ts'; -import { MyReservationsMain } from '../pages/my-reservations.tsx'; import { HomeMyReservationsReservationsViewActiveContainerActiveReservationsDocument, HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, ViewListingCurrentUserDocument, } from '../../../../../../generated.tsx'; +import { App } from '../../../../../../app.tsx'; +import { UserIsAdminMockRequest } from '../../../../../../test-utils/storybook-mock-helpers.ts'; + +const meta: Meta = { + title: 'Pages/My Reservations', + component: App, + parameters: { + layout: 'fullscreen', + }, + decorators: [withMockApolloClient, withMockRouter('/my-reservations')], +}; + +export default meta; +type Story = StoryObj; + // Default mocks that all stories get automatically const defaultMocks = [ // Always mock the current user @@ -25,28 +42,21 @@ const defaultMocks = [ }, }, }, + UserIsAdminMockRequest(STORYBOOK_RESERVATION_USER_ID, false), + // Plus common reservation mocks ...reservationStoryMocks, ]; -const meta: Meta = { - title: 'Pages/MyReservations/Main', - component: MyReservationsMain, +// Default needs no extra mocks +export const Default: Story = { parameters: { - layout: 'fullscreen', apolloClient: { mocks: defaultMocks, }, }, - decorators: [withMockApolloClient], }; -export default meta; -type Story = StoryObj; - -// Default needs no extra mocks -export const Default: Story = {}; - // Loading only needs its delay-override export const Loading: Story = { parameters: { diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/utils/reservation-state-utils.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/utils/reservation-state-utils.stories.tsx index 391dc8c76..16ed6fe33 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/utils/reservation-state-utils.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/my-reservations/utils/reservation-state-utils.stories.tsx @@ -33,7 +33,7 @@ const ReservationStateUtilsTest = (): React.ReactElement => { }; const meta: Meta = { - title: 'Layouts/Home/MyReservations/Utilities/ReservationStateUtils', + title: 'Components/Layouts/Home/MyReservations/Utilities/ReservationStateUtils', component: ReservationStateUtilsTest, parameters: { layout: 'centered', @@ -44,7 +44,7 @@ export default meta; type Story = StoryObj; export const Constants: Story = { - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(ACTIVE_RESERVATION_STATES).toContain('Accepted'); expect(ACTIVE_RESERVATION_STATES).toContain('Requested'); expect(ACTIVE_RESERVATION_STATES.length).toBe(2); @@ -62,7 +62,7 @@ export const Constants: Story = { }; export const ActiveStateChecker: Story = { - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(isActiveReservationState('Accepted')).toBe(true); expect(isActiveReservationState('Requested')).toBe(true); @@ -77,7 +77,7 @@ export const ActiveStateChecker: Story = { }; export const InactiveStateChecker: Story = { - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(isInactiveReservationState('Cancelled')).toBe(true); expect(isInactiveReservationState('Closed')).toBe(true); expect(isInactiveReservationState('Rejected')).toBe(true); diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/listing-information/listing-information.container.stories.tsx index 06b1d3a90..b2e375372 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/listing-information/listing-information.container.stories.tsx @@ -29,6 +29,7 @@ const mockCurrentUser = { const meta: Meta = { title: 'Containers/ListingInformationContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags component: ListingInformationContainer, parameters: { a11y: { disable: true }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.container.stories.tsx index 915093f97..43d316af3 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.container.stories.tsx @@ -19,6 +19,8 @@ const mockUser = { const meta: Meta = { title: 'Containers/SharerInformationContainer', + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags. These are all functional testing stories. + component: SharerInformationContainer, parameters: { a11y: { disable: true }, diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.stories.tsx index 8bd1fb8d6..bc6ed8fd6 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/sharer-information/sharer-information.stories.tsx @@ -67,7 +67,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -77,7 +77,7 @@ export const OwnerView: Story = { isOwner: true, currentUserId: 'user-1', }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -86,7 +86,7 @@ export const WithoutCurrentUser: Story = { args: { currentUserId: null, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -95,7 +95,7 @@ export const RecentlyShared: Story = { args: { sharedTimeAgo: '1h ago', }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; @@ -104,13 +104,13 @@ export const LongTimeAgo: Story = { args: { sharedTimeAgo: '3 months ago', }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; export const ClickMessageButton: Story = { - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const messageButton = canvas.queryByRole('button', { name: /Message/i }); @@ -155,7 +155,7 @@ export const MessageButtonWithError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); const messageButton = canvas.queryByRole('button', { name: /Message/i }); @@ -171,7 +171,7 @@ export const MobileView: Story = { defaultViewport: 'mobile1', }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { await expect(canvasElement).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.container.stories.tsx index 6b02aaf2f..4d616ebc5 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.container.stories.tsx @@ -43,6 +43,7 @@ const mockCurrentUser = { const meta: Meta = { title: 'Containers/ViewListingContainer', component: ViewListingContainer, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags parameters: { layout: 'fullscreen', a11y: { disable: true }, @@ -163,7 +164,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); @@ -193,9 +194,12 @@ export const ListingNotFound: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -248,9 +252,12 @@ export const WithActiveReservation: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -289,9 +296,12 @@ export const UserIsSharer: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -313,9 +323,12 @@ export const GraphQLError: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -354,9 +367,12 @@ export const DraftListing: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -395,9 +411,12 @@ export const InactiveListing: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -451,9 +470,12 @@ export const WithMultipleImages: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -473,7 +495,10 @@ export const LongDescription: Story = { data: { itemListing: { ...mockListing, - description: 'This is a very long description that should wrap properly and display all content. '.repeat(20), + description: + 'This is a very long description that should wrap properly and display all content. '.repeat( + 20, + ), }, }, }, @@ -503,9 +528,12 @@ export const LongDescription: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -555,9 +583,12 @@ export const ComputeTimeAgoRecentHours: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -577,7 +608,9 @@ export const ComputeTimeAgoDays: Story = { data: { itemListing: { ...mockListing, - createdAt: new Date(Date.now() - 3 * 24 * 3600000).toISOString(), // 3 days ago + createdAt: new Date( + Date.now() - 3 * 24 * 3600000, + ).toISOString(), // 3 days ago }, }, }, @@ -607,9 +640,12 @@ export const ComputeTimeAgoDays: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -659,9 +695,12 @@ export const ComputeTimeAgoInvalidDate: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -711,9 +750,12 @@ export const NoCreatedAtDate: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -749,9 +791,12 @@ export const SkipReservationQueryNoListingId: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -787,9 +832,12 @@ export const SkipReservationQueryNoReserverId: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -837,9 +885,12 @@ export const CacheFirstFetchPolicy: Story = { }, }, play: async ({ canvasElement }) => { - await waitFor(() => { - expect(canvasElement).toBeTruthy(); - }, { timeout: 3000 }); + await waitFor( + () => { + expect(canvasElement).toBeTruthy(); + }, + { timeout: 3000 }, + ); }, }; @@ -872,7 +923,8 @@ export const CurrentUserLoadingState: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); + const loadingSpinner = + canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); expect(loadingSpinner ?? canvasElement).toBeTruthy(); }, }; @@ -917,8 +969,8 @@ export const ReservationQueryLoadingState: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); + const loadingSpinner = + canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); expect(loadingSpinner ?? canvasElement).toBeTruthy(); }, }; - diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.stories.tsx index 8391968e4..7cb2c84c1 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/components/view-listing.stories.tsx @@ -1,14 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ViewListing } from './view-listing.tsx'; -import { - type ItemListing, - ViewListingImageGalleryGetImagesDocument, - ViewListingInformationGetListingDocument, -} from '../../../../../../generated.tsx'; -import { - withMockApolloClient, - withMockRouter, -} from '../../../../../../test-utils/storybook-decorators.tsx'; +import { type ItemListing, ViewListingImageGalleryGetImagesDocument, ViewListingInformationGetListingDocument } from '../../../../../../generated.tsx'; +import { withMockApolloClient, withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; // Local mock listing data (removed dependency on DUMMY_LISTINGS) const baseListingId = 'mock-listing-id-1'; const MOCK_LISTING_BASE: ItemListing = { @@ -20,10 +13,7 @@ const MOCK_LISTING_BASE: ItemListing = { sharingPeriodStart: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago sharingPeriodEnd: new Date(Date.now() + 1000 * 60 * 60 * 24 * 10), // in 10 days state: 'Active' as string, - images: [ - 'https://placehold.co/600x400?text=Drill+1', - 'https://placehold.co/600x400?text=Drill+2', - ], + images: ['https://placehold.co/600x400?text=Drill+1', 'https://placehold.co/600x400?text=Drill+2'], createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5), updatedAt: new Date(), reports: 0, @@ -46,7 +36,7 @@ const MOCK_LISTING_BASE: ItemListing = { schemaVersion: '1.0', userType: 'personal-user', }, - listingType: 'item-listing', + listingType: 'item-listing', }; const mocks = [ @@ -55,7 +45,7 @@ const mocks = [ query: ViewListingImageGalleryGetImagesDocument, variables: { listingId: baseListingId }, }, - result: { data: { itemListing: { images: MOCK_LISTING_BASE.images } } }, + result: { data: { itemListing: { id: baseListingId, title: MOCK_LISTING_BASE.title, images: MOCK_LISTING_BASE.images } } }, }, { request: { @@ -70,8 +60,7 @@ const mocks = [ description: MOCK_LISTING_BASE.description, category: MOCK_LISTING_BASE.category, location: MOCK_LISTING_BASE.location, - sharingPeriodStart: - MOCK_LISTING_BASE.sharingPeriodStart.toISOString(), + sharingPeriodStart: MOCK_LISTING_BASE.sharingPeriodStart.toISOString(), sharingPeriodEnd: MOCK_LISTING_BASE.sharingPeriodEnd.toISOString(), state: MOCK_LISTING_BASE.state, images: MOCK_LISTING_BASE.images, @@ -88,7 +77,7 @@ const mocks = [ ]; const meta: Meta = { - title: 'Home/ViewListing', + title: 'Components/Home/ViewListing', component: ViewListing, parameters: { layout: 'fullscreen', @@ -96,10 +85,7 @@ const meta: Meta = { mocks, }, }, - decorators: [ - withMockApolloClient, - withMockRouter('/listing/mock-listing-id-1'), - ], + decorators: [withMockApolloClient, withMockRouter('/listing/mock-listing-id-1')], }; export default meta; @@ -114,7 +100,8 @@ export const Default: Story = { isAuthenticated: false, userReservationRequest: null, sharedTimeAgo: '2 days ago', - }}; + }, +}; export const AsReserver: Story = { args: { diff --git a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/pages/view-listing-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/pages/view-listing-page.stories.tsx index c8ef86a09..77f146483 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/pages/view-listing-page.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/view-listing/pages/view-listing-page.stories.tsx @@ -1,15 +1,18 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; -import { ViewListing } from './view-listing-page.tsx'; import { withMockApolloClient, withMockRouter, } from '../../../../../../test-utils/storybook-decorators.tsx'; import { + SharerInformationContainerDocument, ViewListingDocument, ViewListingCurrentUserDocument, ViewListingActiveReservationRequestForListingDocument, + ViewListingQueryActiveByListingIdDocument, } from '../../../../../../generated.tsx'; +import { AppRoutes } from '../../../index.tsx'; +import { UserIsAdminMockRequest } from '../../../../../../test-utils/storybook-mock-helpers.ts'; const mockListing = { __typename: 'ItemListing', @@ -32,17 +35,21 @@ const mockListing = { lastName: 'Doe', }, }, + listingType: 'ItemListing', + reports: [], + sharingHistory: [], + schemaVersion: '1.0.0', }; const mockCurrentUser = { __typename: 'PersonalUser', - id: 'user-2', userType: 'personal-user', + id: 'user-1', }; -const meta: Meta = { - title: 'Pages/ViewListingPage', - component: ViewListing, +const meta: Meta = { + title: 'Pages/View Listing', + component: AppRoutes, parameters: { layout: 'fullscreen', apolloClient: { @@ -71,7 +78,7 @@ const meta: Meta = { { request: { query: ViewListingActiveReservationRequestForListingDocument, - variables: { listingId: '1', reserverId: 'user-2' }, + variables: { listingId: '1', reserverId: 'user-1' }, }, result: { data: { @@ -79,6 +86,50 @@ const meta: Meta = { }, }, }, + { + request: { + query: ViewListingQueryActiveByListingIdDocument, + variables: { listingId: '1' }, + }, + result: { + data: { + queryActiveByListingId: [], + }, + }, + }, + UserIsAdminMockRequest('user-1', false), + { + request: { + query: SharerInformationContainerDocument, + variables: { sharerId: 'user-1' }, + }, + result: { + data: { + userById: { + userIsAdmin: false, + userType: 'personal-user', + createdAt: '2025-10-01T08:00:00Z', + + account: { + __typename: 'PersonalUserAccount', + username: 'new_user', + email: 'new.user@example.com', + accountType: 'non-verified-personal', + profile: { + __typename: 'PersonalUserAccountProfile', + firstName: 'Alex', + lastName: '', + location: { + __typename: 'PersonalUserAccountProfileLocation', + city: 'Boston', + state: 'MA', + }, + }, + }, + }, + }, + }, + }, ], }, }, @@ -86,7 +137,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { @@ -98,6 +149,17 @@ export const Loading: Story = { parameters: { apolloClient: { mocks: [ + UserIsAdminMockRequest('user-1', false), + { + request: { + query: ViewListingCurrentUserDocument, + }, + result: { + data: { + currentUser: mockCurrentUser, + }, + }, + }, { request: { query: ViewListingDocument, @@ -118,7 +180,7 @@ export const Loading: Story = { { request: { query: ViewListingActiveReservationRequestForListingDocument, - variables: { listingId: '1', reserverId: 'user-2' }, + variables: { listingId: '1', reserverId: 'user-1' }, }, result: { data: { diff --git a/apps/ui-sharethrift/src/components/layouts/app/section-layout.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/section-layout.stories.tsx index 3e60e5674..b4a6768b0 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/section-layout.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/section-layout.stories.tsx @@ -4,7 +4,7 @@ import { MemoryRouter } from 'react-router-dom'; import { ApolloClient, InMemoryCache } from '@apollo/client'; import { ApolloProvider } from '@apollo/client/react'; import { MockLink } from '@apollo/client/testing'; -import { MockAuthWrapper } from '../../../test-utils/storybook-decorators.tsx'; +import { MockAuthWrapper } from '../../../test-utils/storybook-mock-auth-wrappers.tsx'; import { SectionLayout } from './section-layout.tsx'; // Mock Apollo Client with MockLink @@ -14,7 +14,7 @@ const mockApolloClient = new ApolloClient({ }); const meta: Meta = { - title: 'Layouts/SectionLayout', + title: 'Components/Home/Layouts/SectionLayout', component: SectionLayout, parameters: { layout: 'fullscreen', diff --git a/apps/ui-sharethrift/src/components/layouts/app/section-layout.tsx b/apps/ui-sharethrift/src/components/layouts/app/section-layout.tsx index d488805b7..125e09f04 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/section-layout.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/section-layout.tsx @@ -22,7 +22,6 @@ export const SectionLayout: React.FC = () => { const auth = useAuth(); const apolloClient = useApolloClient(); const { isAdmin } = useUserIsAdmin(); - // Map nav keys to routes as defined in index.tsx const routeMap = { home: '', diff --git a/apps/ui-sharethrift/src/components/layouts/login/login-selection.stories.tsx b/apps/ui-sharethrift/src/components/layouts/login/login-selection.stories.tsx new file mode 100644 index 000000000..5d23b1da4 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/login/login-selection.stories.tsx @@ -0,0 +1,263 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within, userEvent } from 'storybook/test'; +import { MemoryRouter } from 'react-router-dom'; +import { MockUnauthWrapper } from '../../../test-utils/storybook-mock-auth-wrappers.tsx'; +import { LoginSelection } from './login-selection.tsx'; +import { withMockApolloClient } from '../../../test-utils/storybook-decorators.tsx'; +import { UseUserIsAdminDocument } from '../../../generated.tsx'; + +const meta: Meta = { + title: 'Pages/Home - Unauthenticated/Login', + component: LoginSelection, + parameters: { + layout: 'fullscreen', + apolloClient: { + mocks: [ + { + request: { + query: UseUserIsAdminDocument, + }, + result: { + data: { + currentUser: { + __typename: 'PersonalUser', + id: 'user-1', + userIsAdmin: false, + }, + }, + }, + }, + ], + }, + }, + decorators: [ + withMockApolloClient, + (Story) => ( + + + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByLabelText('Email'); + await expect(emailInput).toBeInTheDocument(); + + const passwordInput = canvas.getByLabelText('Password'); + await expect(passwordInput).toBeInTheDocument(); + + const personalLoginButton = canvas.getByRole('button', { + name: /Personal Login/i, + }); + await expect(personalLoginButton).toBeInTheDocument(); + + const adminLoginButton = canvas.getByRole('button', { + name: /Admin Login/i, + }); + await expect(adminLoginButton).toBeInTheDocument(); + }, +}; + +export const WithEnvironment: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const signUpButton = canvasElement.querySelector( + '[data-testid="sign-up-button"]', + ); // Using data-testid selector for there are multiple buttons with same name "Sign Up" + await expect(signUpButton).toBeInTheDocument(); + + const backLink = canvas.getByRole('button', { name: /Back to Home/i }); + await expect(backLink).toBeInTheDocument(); + + const forgotLink = canvas.getByRole('button', { name: /Forgot password/i }); + await expect(forgotLink).toBeInTheDocument(); + }, +}; + +/** + * Test complete form fill and verify values + * Verifies that both email and password inputs accept and retain values + */ +export const FillCompleteForm: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByLabelText('Email'); + const passwordInput = canvas.getByLabelText('Password'); + + await userEvent.type(emailInput, 'user@example.com'); + await userEvent.type(passwordInput, 'securePassword123'); + + await expect(emailInput).toHaveValue('user@example.com'); + await expect(passwordInput).toHaveValue('securePassword123'); + }, +}; + +/** + * Test input placeholders + * Verifies that the form inputs have correct placeholder text + */ +export const InputPlaceholders: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByLabelText('Email'); + await expect(emailInput).toHaveAttribute('placeholder', 'johndoe@email.com'); + + const passwordInput = canvas.getByLabelText('Password'); + await expect(passwordInput).toHaveAttribute('placeholder', 'Your Password'); + }, +}; + +/** + * Test input autocomplete attributes + * Verifies that the form inputs have proper autocomplete settings for browser autofill + */ +export const InputAutocomplete: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByLabelText('Email'); + await expect(emailInput).toHaveAttribute('autocomplete', 'email'); + + const passwordInput = canvas.getByLabelText('Password'); + await expect(passwordInput).toHaveAttribute('autocomplete', 'current-password'); + }, +}; + +/** + * Test admin login button validation flow + * Verifies that admin login button triggers form validation and can proceed with valid data + */ +export const AdminLoginValidFlow: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByLabelText('Email'); + const passwordInput = canvas.getByLabelText('Password'); + + await userEvent.type(emailInput, 'admin@example.com'); + await userEvent.type(passwordInput, 'adminPassword'); + + const adminLoginButton = canvas.getByRole('button', { + name: /Admin Login/i, + }); + + await expect(emailInput).toHaveValue('admin@example.com'); + await expect(passwordInput).toHaveValue('adminPassword'); + await expect(adminLoginButton).toBeEnabled(); + }, +}; + +/** + * Test page title rendering + * Verifies that the main heading is displayed correctly + */ +export const PageTitle: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const title = canvas.getByText('Log in or Sign up'); + await expect(title).toBeInTheDocument(); + }, +}; + +/** + * Test divider with 'or' text + * Verifies that the divider separating login form from sign up button is present + */ +export const DividerPresent: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const divider = canvas.getByText('or'); + await expect(divider).toBeInTheDocument(); + }, +}; + +/** + * Test Sign Up button styling and accessibility + * Verifies that the sign up button has the correct test id + */ +export const SignUpButtonAccessibility: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const signUpButton = canvasElement.querySelector( + '[data-testid="sign-up-button"]', + ); + await expect(signUpButton).toBeInTheDocument(); + await expect(signUpButton).toHaveTextContent('Sign Up'); + }, +}; + +/** + * Test email input receives focus on load + * Verifies that the email field is the active element after render + */ +export const EmailInputAutoFocus: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByLabelText('Email'); + // Check that the input is in the document (autoFocus prop is set in JSX) + await expect(emailInput).toBeInTheDocument(); + }, +}; + +/** + * Test button sizing for mobile viewport + * Verifies that responsive styles are applied based on screen size + */ +export const MobileViewport: Story = { + tags: ['!dev'], + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify form is still rendered on mobile + const emailInput = canvas.getByLabelText('Email'); + await expect(emailInput).toBeInTheDocument(); + + const personalLoginButton = canvas.getByRole('button', { + name: /Personal Login/i, + }); + await expect(personalLoginButton).toBeInTheDocument(); + }, +}; + +/** + * Test password field is of type password + * Verifies that the password input has proper type for security + */ +export const PasswordFieldType: Story = { + tags: ['!dev'], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const passwordInput = canvas.getByLabelText('Password'); + await expect(passwordInput).toHaveAttribute('type', 'password'); + }, +}; + diff --git a/apps/ui-sharethrift/src/components/shared/login-selection.tsx b/apps/ui-sharethrift/src/components/layouts/login/login-selection.tsx similarity index 99% rename from apps/ui-sharethrift/src/components/shared/login-selection.tsx rename to apps/ui-sharethrift/src/components/layouts/login/login-selection.tsx index d62d75a88..401a205e6 100644 --- a/apps/ui-sharethrift/src/components/shared/login-selection.tsx +++ b/apps/ui-sharethrift/src/components/layouts/login/login-selection.tsx @@ -244,6 +244,7 @@ export const LoginSelection: React.FC = () => {
@@ -23,6 +23,7 @@ const meta: Meta = { parameters: { layout: 'centered', }, + tags: ["!dev"], // not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; @@ -36,7 +37,7 @@ export const HookTest: Story = { ), - play: async () => { + play: () => { expect(typeof useCreateListingNavigation).toBe('function'); }, }; @@ -56,7 +57,7 @@ export const AuthenticatedNavigation: Story = { ); }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const button = canvas.getByTestId('create-listing-btn'); @@ -81,7 +82,7 @@ export const UnauthenticatedNavigation: Story = { ); }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const button = canvas.getByTestId('create-listing-btn'); diff --git a/apps/ui-sharethrift/src/components/shared/local-storage.stories.tsx b/apps/ui-sharethrift/src/components/shared/local-storage.stories.tsx index 2a65713b7..4813381e8 100644 --- a/apps/ui-sharethrift/src/components/shared/local-storage.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/local-storage.stories.tsx @@ -17,6 +17,7 @@ const meta: Meta = { parameters: { layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; diff --git a/apps/ui-sharethrift/src/components/shared/login-selection.stories.tsx b/apps/ui-sharethrift/src/components/shared/login-selection.stories.tsx deleted file mode 100644 index 4c2efe0d1..000000000 --- a/apps/ui-sharethrift/src/components/shared/login-selection.stories.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { expect, within, userEvent } from 'storybook/test'; -import { MemoryRouter } from 'react-router-dom'; -import { MockAuthWrapper } from '../../test-utils/storybook-decorators.tsx'; -import { LoginSelection } from './login-selection.tsx'; -import { withMockApolloClient } from '../../test-utils/storybook-decorators.tsx'; -import { UseUserIsAdminDocument } from '../../generated.tsx'; - -const meta: Meta = { - title: 'Shared/LoginSelection', - component: LoginSelection, - parameters: { - layout: 'fullscreen', - apolloClient: { - mocks: [ - { - request: { - query: UseUserIsAdminDocument, - }, - result: { - data: { - currentUser: { - __typename: 'PersonalUser', - id: 'user-1', - userIsAdmin: false, - }, - }, - }, - }, - ], - }, - }, - decorators: [ - withMockApolloClient, - (Story) => ( - - - - - - ), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const emailInput = canvas.getByLabelText('Email'); - await expect(emailInput).toBeInTheDocument(); - - const passwordInput = canvas.getByLabelText('Password'); - await expect(passwordInput).toBeInTheDocument(); - - const personalLoginButton = canvas.getByRole('button', { name: /Personal Login/i }); - await expect(personalLoginButton).toBeInTheDocument(); - - const adminLoginButton = canvas.getByRole('button', { name: /Admin Login/i }); - await expect(adminLoginButton).toBeInTheDocument(); - }, -}; - -export const WithEnvironment: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const signUpButton = canvas.getByRole('button', { name: /Sign Up/i }); - await expect(signUpButton).toBeInTheDocument(); - - const backLink = canvas.getByRole('button', { name: /Back to Home/i }); - await expect(backLink).toBeInTheDocument(); - - const forgotLink = canvas.getByRole('button', { name: /Forgot password/i }); - await expect(forgotLink).toBeInTheDocument(); - }, -}; - -export const FillLoginForm: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const emailInput = canvas.getByLabelText('Email'); - await userEvent.type(emailInput, 'test@example.com'); - - const passwordInput = canvas.getByLabelText('Password'); - await userEvent.type(passwordInput, 'password123'); - - await expect(emailInput).toHaveValue('test@example.com'); - }, -}; - -export const ClickBackToHome: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const backLink = canvas.getByRole('button', { name: /Back to Home/i }); - await userEvent.click(backLink); - }, -}; - -export const ClickForgotPassword: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const forgotLink = canvas.getByRole('button', { name: /Forgot password/i }); - await userEvent.click(forgotLink); - }, -}; - -export const ClickSignUp: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const signUpButton = canvas.getByRole('button', { name: /Sign Up/i }); - await userEvent.click(signUpButton); - }, -}; - -export const SubmitFormValidation: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const personalLoginButton = canvas.getByRole('button', { name: /Personal Login/i }); - await userEvent.click(personalLoginButton); - - await expect(personalLoginButton).toBeInTheDocument(); - }, -}; - -export const ClickAdminLogin: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const adminLoginButton = canvas.getByRole('button', { name: /Admin Login/i }); - await expect(adminLoginButton).toBeInTheDocument(); - await expect(adminLoginButton).toBeEnabled(); - }, -}; diff --git a/apps/ui-sharethrift/src/components/shared/payment/billing-address-form-items.stories.tsx b/apps/ui-sharethrift/src/components/shared/payment/billing-address-form-items.stories.tsx index c97afa2ab..1d20d5060 100644 --- a/apps/ui-sharethrift/src/components/shared/payment/billing-address-form-items.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/payment/billing-address-form-items.stories.tsx @@ -4,7 +4,7 @@ import { Form } from 'antd'; import { countriesMockData } from '../../layouts/signup/components/countries-mock-data.ts'; const meta: Meta = { - title: 'Shared/Payment/BillingAddressFormItems', + title: 'Components/Shared/Payment/BillingAddressFormItems', component: BillingAddressFormItems, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/shared/payment/country-change-utils.stories.tsx b/apps/ui-sharethrift/src/components/shared/payment/country-change-utils.stories.tsx index 7a6d41125..36a27d750 100644 --- a/apps/ui-sharethrift/src/components/shared/payment/country-change-utils.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/payment/country-change-utils.stories.tsx @@ -74,6 +74,7 @@ const meta: Meta = { parameters: { layout: 'padded', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; diff --git a/apps/ui-sharethrift/src/components/shared/payment/country-form-item.stories.tsx b/apps/ui-sharethrift/src/components/shared/payment/country-form-item.stories.tsx index 4263d7a23..f95d5902a 100644 --- a/apps/ui-sharethrift/src/components/shared/payment/country-form-item.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/payment/country-form-item.stories.tsx @@ -5,7 +5,7 @@ import { countriesMockData } from '../../layouts/signup/components/countries-moc import { CountryFormItem } from './country-form-item.tsx'; const meta: Meta = { - title: 'Shared/Payment/CountryFormItem', + title: 'Components/Shared/Payment/CountryFormItem', component: CountryFormItem, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/shared/payment/payment-form.stories.tsx b/apps/ui-sharethrift/src/components/shared/payment/payment-form.stories.tsx index 63997707f..afcf80baf 100644 --- a/apps/ui-sharethrift/src/components/shared/payment/payment-form.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/payment/payment-form.stories.tsx @@ -9,7 +9,7 @@ import { expect, within, userEvent, waitFor } from 'storybook/test'; const { Text } = Typography; const meta: Meta = { - title: 'Shared/Payment/PaymentForm', + title: 'Components/Shared/Payment/PaymentForm', component: PaymentForm, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/shared/payment/payment-token-form-items.stories.tsx b/apps/ui-sharethrift/src/components/shared/payment/payment-token-form-items.stories.tsx index 764ecc969..ab2087cb4 100644 --- a/apps/ui-sharethrift/src/components/shared/payment/payment-token-form-items.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/payment/payment-token-form-items.stories.tsx @@ -4,7 +4,7 @@ import { Form } from 'antd'; import { useState } from 'react'; const meta: Meta = { - title: 'Shared/Payment/PaymentTokenFormItems', + title: 'Components/Shared/Payment/PaymentTokenFormItems', component: PaymentTokenFormItems, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/shared/payment/state-province-form-item.stories.tsx b/apps/ui-sharethrift/src/components/shared/payment/state-province-form-item.stories.tsx index 5bc1bfb48..0b04642c9 100644 --- a/apps/ui-sharethrift/src/components/shared/payment/state-province-form-item.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/payment/state-province-form-item.stories.tsx @@ -7,7 +7,7 @@ const usStates = countriesMockData.find(c => c.countryCode === 'US')?.states || const caStates = countriesMockData.find(c => c.countryCode === 'CA')?.states || []; const meta: Meta = { - title: 'Shared/Payment/StateProvinceFormItem', + title: 'Components/Shared/Payment/StateProvinceFormItem', component: StateProvinceFormItem, parameters: { layout: 'padded', diff --git a/apps/ui-sharethrift/src/components/shared/require-auth-admin.stories.tsx b/apps/ui-sharethrift/src/components/shared/require-auth-admin.stories.tsx index 3f458612d..7df836298 100644 --- a/apps/ui-sharethrift/src/components/shared/require-auth-admin.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/require-auth-admin.stories.tsx @@ -1,13 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; -import { - withMockApolloClient, - MockAuthWrapper, -} from '../../test-utils/storybook-decorators.tsx'; +import { withMockApolloClient } from '../../test-utils/storybook-decorators.tsx'; import { MemoryRouter } from 'react-router-dom'; import { AuthContext } from 'react-oidc-context'; import { RequireAuthAdmin } from './require-auth-admin.tsx'; import { UseUserIsAdminDocument } from '../../generated.tsx'; +import { MockAuthWrapper } from '../../test-utils/storybook-mock-auth-wrappers.tsx'; import { createMockAuth } from '../../test/utils/mock-auth.ts'; const meta: Meta = { @@ -34,11 +32,27 @@ const meta: Meta = { }, }, decorators: [withMockApolloClient], + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; type Story = StoryObj; +const UseUserIsAdminMock = { + request: { + query: UseUserIsAdminDocument, + }, + result: { + data: { + currentUser: { + id: 'user-1', + __typename: 'PersonalUser', + userIsAdmin: true, + }, + }, + }, +}; + const AdminProtectedContent = () => (

Admin Dashboard

@@ -60,6 +74,11 @@ export const WithAuthenticationAndAdmin: Story = { children: , }, decorators: [withAuthAndRouter], + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, play: async ({ canvasElement }) => { // MockAuthWrapper provides isAuthenticated: true // Apollo mock provides isAdmin: true @@ -74,6 +93,11 @@ export const WithForceLogin: Story = { forceLogin: true, }, decorators: [withAuthAndRouter], + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, play: async ({ canvasElement }) => { // When forceLogin is true, component will trigger signin redirect if not authenticated // MockAuthWrapper provides isAuthenticated: true, so content should render @@ -87,6 +111,11 @@ export const WithCustomRedirect: Story = { redirectPath: '/custom-login', }, decorators: [withAuthAndRouter], + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, play: async ({ canvasElement }) => { // Component uses custom redirect path when provided // MockAuthWrapper provides isAuthenticated: true, so content should render @@ -126,21 +155,7 @@ export const NotAdmin: Story = { decorators: [withAuthAndRouter], parameters: { apolloClient: { - mocks: [ - { - request: { - query: UseUserIsAdminDocument, - }, - result: { - data: { - currentUser: { - __typename: 'PersonalUser', - userIsAdmin: false, - }, - }, - }, - }, - ], + mocks: [UseUserIsAdminMock], }, }, play: async ({ canvasElement }) => { @@ -154,6 +169,11 @@ export const UnauthenticatedWithError: Story = { args: { children: , }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -182,6 +202,11 @@ export const UnauthenticatedLoading: Story = { args: { children: , }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -214,6 +239,11 @@ export const UnauthenticatedNoForceLogin: Story = { children: , forceLogin: false, }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -243,6 +273,11 @@ export const ForceLoginTriggersSigninRedirect: Story = { children: , forceLogin: true, }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -281,7 +316,7 @@ export const AccessTokenExpiringTriggersSilent: Story = { setTimeout(() => callback(), 100); return () => {}; // Return a no-op unsubscribe function }; - const signinSilent = async () => Promise.resolve(null); + const signinSilent = async () => null; const mockAuth = createMockAuth({ isAuthenticated: true, isLoading: false, @@ -322,6 +357,11 @@ export const AuthenticationErrorNoForceLogin: Story = { children: , forceLogin: false, }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { diff --git a/apps/ui-sharethrift/src/components/shared/require-auth-admin.tsx b/apps/ui-sharethrift/src/components/shared/require-auth-admin.tsx index 19a0171ee..fea39aba0 100644 --- a/apps/ui-sharethrift/src/components/shared/require-auth-admin.tsx +++ b/apps/ui-sharethrift/src/components/shared/require-auth-admin.tsx @@ -33,15 +33,7 @@ export const RequireAuthAdmin: React.FC = (props) => { auth.signinRedirect(); } - }, [ - auth.isAuthenticated, - auth.activeNavigator, - auth.isLoading, - auth.signinRedirect, - auth.error, - props.forceLogin, - redirectPath, - ]); + }, [auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect, auth.error, props.forceLogin, auth]); // automatically refresh token useEffect(() => { @@ -50,7 +42,7 @@ export const RequireAuthAdmin: React.FC = (props) => { redirect_uri: VITE_B2C_REDIRECT_URI ?? '', }); }); - }, [auth.events, auth.signinSilent]); + }, [auth, auth.events, auth.signinSilent]); // Check authentication first if (!auth.isAuthenticated) { diff --git a/apps/ui-sharethrift/src/components/shared/require-auth.stories.tsx b/apps/ui-sharethrift/src/components/shared/require-auth.stories.tsx index ed389140e..1bb4d803e 100644 --- a/apps/ui-sharethrift/src/components/shared/require-auth.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/require-auth.stories.tsx @@ -11,6 +11,8 @@ const meta: Meta = { parameters: { layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags + }; export default meta; diff --git a/apps/ui-sharethrift/src/components/shared/use-onboarding-redirect.stories.tsx b/apps/ui-sharethrift/src/components/shared/use-onboarding-redirect.stories.tsx index b681601e5..fd2e7088c 100644 --- a/apps/ui-sharethrift/src/components/shared/use-onboarding-redirect.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/use-onboarding-redirect.stories.tsx @@ -87,6 +87,8 @@ const meta: Meta = { parameters: { layout: 'centered', }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags + }; export default meta; diff --git a/apps/ui-sharethrift/src/contexts/applicant-id-context.stories.tsx b/apps/ui-sharethrift/src/contexts/applicant-id-context.stories.tsx index bb4fecf7a..6bf1890f7 100644 --- a/apps/ui-sharethrift/src/contexts/applicant-id-context.stories.tsx +++ b/apps/ui-sharethrift/src/contexts/applicant-id-context.stories.tsx @@ -41,6 +41,7 @@ const meta: Meta = { ), ], + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags }; export default meta; diff --git a/apps/ui-sharethrift/src/conversation-list.stories.tsx b/apps/ui-sharethrift/src/conversation-list.stories.tsx deleted file mode 100644 index 691e61af7..000000000 --- a/apps/ui-sharethrift/src/conversation-list.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from 'storybook/test'; -import { ConversationList } from './components/layouts/app/pages/messages/components/conversation-list.tsx'; - -const meta: Meta = { - title: 'Components/Messages/ConversationList', - component: ConversationList, - argTypes: { - onConversationSelect: { action: 'conversation selected' }, - }, -}; -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - onConversationSelect: fn(), - selectedConversationId: '1', - // conversations: mockConversations, - }, -}; - -export const WithConversationSelection: Story = { - args: { - onConversationSelect: fn(), - selectedConversationId: '1', - }, -}; diff --git a/apps/ui-sharethrift/src/hero-section.stories.tsx b/apps/ui-sharethrift/src/hero-section.stories.tsx index 676fa0b31..322f692cc 100644 --- a/apps/ui-sharethrift/src/hero-section.stories.tsx +++ b/apps/ui-sharethrift/src/hero-section.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, within } from 'storybook/test'; const meta: Meta = { - title: 'Listing/Hero', + title: 'Components/Hero', component: HeroSection, parameters: { layout: 'fullscreen', @@ -15,7 +15,7 @@ type Story = StoryObj; export const Default: Story = { render: () => , - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const heading = canvas.getByRole('heading'); @@ -32,8 +32,9 @@ export const Default: Story = { }; export const WithInteraction: Story = { + tags:['!dev'], // not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags render: () => , - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const heading = canvas.getByRole('heading'); @@ -42,7 +43,7 @@ export const WithInteraction: Story = { const buttons = canvasElement.querySelectorAll('button, a[class*="button"]'); expect(buttons.length).toBeGreaterThanOrEqual(0); - const textContent = canvasElement.textContent; + const {textContent} = canvasElement; expect(textContent).toBeTruthy(); }, }; diff --git a/apps/ui-sharethrift/src/settings.stories.tsx b/apps/ui-sharethrift/src/settings.stories.tsx index 6896b703a..2ab59867d 100644 --- a/apps/ui-sharethrift/src/settings.stories.tsx +++ b/apps/ui-sharethrift/src/settings.stories.tsx @@ -3,7 +3,7 @@ import { expect } from 'storybook/test'; // Simple test to verify the file exports correctly const meta = { - title: 'Pages/Account/Settings', + title: 'Pages/Account/Settings - File Exports', parameters: { layout: 'fullscreen', docs: { @@ -12,6 +12,8 @@ const meta = { }, }, }, + tags: ['!dev'], // functional testing story, not rendered in sidebar - https://storybook.js.org/docs/writing-stories/tags + } satisfies Meta; export default meta; @@ -19,6 +21,7 @@ type Story = StoryObj; export const FileExports: Story = { name: 'File Exports', + render: () => (

Settings component file exists and exports correctly

diff --git a/apps/ui-sharethrift/src/test-utils/storybook-decorators.stories.tsx b/apps/ui-sharethrift/src/test-utils/storybook-decorators.stories.tsx new file mode 100644 index 000000000..68a9bec03 --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/storybook-decorators.stories.tsx @@ -0,0 +1,682 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, waitFor } from 'storybook/test'; +import { gql } from '@apollo/client'; +import { useQuery, useApolloClient } from '@apollo/client/react'; +import { useLocation } from 'react-router-dom'; +import { useAuth } from 'react-oidc-context'; +import { useUserId } from '../components/shared/user-context.tsx'; +import { + withMockApolloClient, + withMockUserId, + withMockRouter, + withAuthDecorator, +} from './storybook-decorators.tsx'; + + + +// Test component to verify Apollo Client is provided +const ApolloTestComponent = () => { + const client = useApolloClient(); + return ( +
+ {client ? 'Apollo Client Connected' : 'No Apollo Client'} +
+ ); +}; + +// Test component to verify userId is provided +const UserIdTestComponent = () => { + const userId = useUserId(); + return
{userId || 'No User ID'}
; +}; + +// Test component to verify router is provided +const RouterTestComponent = () => { + const location = useLocation(); + return ( +
+ Current Path: {location.pathname} +
+ ); +}; + +// Test component to verify auth context is provided +const AuthTestComponent = () => { + return
Auth Provider Active
; +}; + +const TEST_QUERY = gql` + query TestQuery { + test + } +`; + +// Test component to verify Apollo mocks are exercised +const ApolloQueryTestComponent = () => { + const { data, loading, error } = useQuery<{ test: string }>(TEST_QUERY); + + if (loading) { + return
Loading
; + } + + if (error) { + return
Error
; + } + + return
{data?.test ?? 'No Data'}
; +}; + +// Test component to verify auth context values are provided +const AuthStateTestComponent = () => { + const { isAuthenticated, user } = useAuth(); + const label = isAuthenticated + ? `Authenticated:${user?.profile?.sub ?? 'unknown'}` + : 'Unauthenticated'; + return
{label}
; +}; + +const meta: Meta = { + title: 'Test Utils/Storybook Decorators', + parameters: { + layout: 'centered', + }, + tags: ['!dev'], // functional testing story, not rendered in sidebar +}; + +export default meta; +type Story = StoryObj; + +/** + * Test withMockApolloClient decorator + * Verifies that the Apollo Client is properly initialized and provided + */ +export const WithMockApolloClient: Story = { + decorators: [withMockApolloClient], + render: () => , + parameters: { + apolloClient: { + mocks: [], + showWarnings: false, + }, + }, + play: ({ canvasElement }) => { + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + expect(apolloTest).toBeTruthy(); + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + }, +}; + +/** + * Test withMockApolloClient with custom mocks + * Verifies that mocks are properly passed to the Apollo Client + */ +export const WithMockApolloClientAndMocks: Story = { + decorators: [withMockApolloClient], + render: () => , + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TEST_QUERY, + }, + result: { + data: { test: 'test data' }, + }, + }, + ], + showWarnings: true, + }, + }, + play: ({ canvasElement }) => { + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + expect(apolloTest).toBeTruthy(); + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + }, +}; + +/** + * Test withMockApolloClient executes queries using mocks + * Verifies that the mocked response resolves through Apollo Client + */ +export const WithMockApolloClientQuery: Story = { + decorators: [withMockApolloClient], + render: () => , + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TEST_QUERY, + }, + result: { + data: { test: 'mocked-result' }, + }, + }, + ], + showWarnings: false, + }, + }, + play: async ({ canvasElement }) => { + await waitFor(() => + expect( + canvasElement.querySelector('[data-testid="apollo-query"]') + ?.textContent, + ).toBe('mocked-result'), + ); + }, +}; + +/** + * Test withMockApolloClient without parameters + * Verifies default behavior when no apolloClient parameters are provided + */ +export const WithMockApolloClientNoParams: Story = { + decorators: [withMockApolloClient], + render: () => , + play: ({ canvasElement }) => { + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + expect(apolloTest).toBeTruthy(); + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + }, +}; + +/** + * Test withMockUserId decorator with default userId + * Verifies that the default user ID ('user-1') is provided + */ +export const WithMockUserIdDefault: Story = { + decorators: [withMockUserId()], + render: () => , + play: ({ canvasElement }) => { + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + expect(userIdTest).toBeTruthy(); + expect(userIdTest?.textContent).toBe('user-1'); + }, +}; + +/** + * Test withMockUserId decorator with custom userId + * Verifies that a custom user ID is properly provided + */ +export const WithMockUserIdCustom: Story = { + decorators: [withMockUserId('custom-user-123')], + render: () => , + play: ({ canvasElement }) => { + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + expect(userIdTest).toBeTruthy(); + expect(userIdTest?.textContent).toBe('custom-user-123'); + }, +}; + +/** + * Test withMockRouter decorator with default route + * Verifies that the router is initialized with the default route ('/') + */ +export const WithMockRouterDefault: Story = { + decorators: [withMockRouter()], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /'); + }, +}; + +/** + * Test withMockRouter decorator with custom route + * Verifies that the router is initialized with a custom route + */ +export const WithMockRouterCustomRoute: Story = { + decorators: [withMockRouter('/account/profile')], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /account/profile'); + }, +}; + +/** + * Test withMockRouter decorator with authenticated user + * Verifies that the router works with authentication enabled (default) + */ +export const WithMockRouterAuthenticated: Story = { + decorators: [withMockRouter('/home', true)], + render: () => ( +
+ + +
+ ), + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + const authState = canvasElement.querySelector('[data-testid="auth-state"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /home'); + expect(authState).toBeTruthy(); + expect(authState?.textContent).toContain('Authenticated:'); + }, +}; + +/** + * Test withMockRouter decorator with unauthenticated user + * Verifies that the router works with authentication disabled + */ +export const WithMockRouterUnauthenticated: Story = { + decorators: [withMockRouter('/login', false)], + render: () => ( +
+ + +
+ ), + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + const authState = canvasElement.querySelector('[data-testid="auth-state"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /login'); + expect(authState).toBeTruthy(); + expect(authState?.textContent).toBe('Unauthenticated'); + }, +}; + +/** + * Test withAuthDecorator + * Verifies that the auth decorator properly wraps components + */ +export const WithAuthDecorator: Story = { + decorators: [withAuthDecorator], + render: () => , + play: ({ canvasElement }) => { + const authTest = canvasElement.querySelector('[data-testid="auth-test"]'); + expect(authTest).toBeTruthy(); + expect(authTest?.textContent).toBe('Auth Provider Active'); + }, +}; + +/** + * Test combined decorators + * Verifies that multiple decorators can be used together + */ +export const CombinedDecorators: Story = { + decorators: [withMockApolloClient, withMockUserId('combined-user'), withMockRouter('/combined')], + render: () => ( +
+ + + +
+ ), + parameters: { + apolloClient: { + mocks: [], + }, + }, + play: ({ canvasElement }) => { + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + + expect(apolloTest).toBeTruthy(); + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + + expect(userIdTest).toBeTruthy(); + expect(userIdTest?.textContent).toBe('combined-user'); + + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /combined'); + }, +}; + +/** + * Test withAuthDecorator with auth state + * Verifies that the auth decorator provides auth context with user data + */ +export const WithAuthDecoratorState: Story = { + decorators: [withAuthDecorator], + render: () => , + play: ({ canvasElement }) => { + const authState = canvasElement.querySelector('[data-testid="auth-state"]'); + expect(authState).toBeTruthy(); + // withAuthDecorator sets up AuthProvider but without actual authentication + // so isAuthenticated should be false initially + expect(authState?.textContent).toContain('Unauthenticated'); + }, +}; + +/** + * Test Apollo error handling + * Verifies that the decorator handles query errors properly + */ +export const WithMockApolloClientError: Story = { + decorators: [withMockApolloClient], + render: () => , + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TEST_QUERY, + }, + error: new Error('Network error'), + }, + ], + showWarnings: false, + }, + }, + play: async ({ canvasElement }) => { + await waitFor(() => + expect( + canvasElement.querySelector('[data-testid="apollo-query"]') + ?.textContent, + ).toBe('Error'), + ); + }, +}; + +/** + * Test Apollo loading state + * Verifies that the decorator properly shows error when no mock matches + */ +export const WithMockApolloClientNoMatch: Story = { + decorators: [withMockApolloClient], + render: () => , + parameters: { + apolloClient: { + mocks: [], + showWarnings: false, + }, + }, + play: async ({ canvasElement }) => { + // When no mock matches, Apollo returns an error + await waitFor(() => { + const apolloQuery = canvasElement.querySelector('[data-testid="apollo-query"]'); + expect(apolloQuery).toBeTruthy(); + expect(apolloQuery?.textContent).toBe('Error'); + }); + }, +}; + +/** + * Test withMockRouter with multiple route variations + * Verifies router handles complex paths + */ +export const WithMockRouterComplexPath: Story = { + decorators: [withMockRouter('/users/123/settings', true)], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /users/123/settings'); + }, +}; + +/** + * Test all decorators together with full integration + * Verifies the complete decorator stack works correctly + */ +export const FullDecoratorStack: Story = { + decorators: [ + withAuthDecorator, + withMockApolloClient, + withMockUserId('stack-user'), + withMockRouter('/full-stack'), + ], + render: () => ( +
+ + + + +
+ ), + parameters: { + apolloClient: { + mocks: [], + }, + }, + play: ({ canvasElement }) => { + const authTest = canvasElement.querySelector('[data-testid="auth-test"]'); + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + + expect(authTest?.textContent).toBe('Auth Provider Active'); + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + expect(userIdTest?.textContent).toBe('stack-user'); + expect(routerTest?.textContent).toContain('Current Path: /full-stack'); + }, +}; + +/** + * Test withMockRouter with empty path + * Verifies that empty paths are handled correctly + */ +export const WithMockRouterEmptyPath: Story = { + decorators: [withMockRouter('', true)], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + // Empty path defaults to root + expect(routerTest?.textContent).toContain('Current Path:'); + }, +}; + +/** + * Test withMockRouter with query parameters + * Verifies that routes with query strings are handled + */ +export const WithMockRouterWithQueryParams: Story = { + decorators: [withMockRouter('/search?q=test&category=tools', true)], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /search'); + }, +}; + +/** + * Test withMockRouter with hash fragments + * Verifies that routes with hash fragments are handled + */ +export const WithMockRouterWithHash: Story = { + decorators: [withMockRouter('/page#section', true)], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /page'); + }, +}; + +/** + * Test withMockUserId with empty string + * Verifies that empty userId is handled + */ +export const WithMockUserIdEmpty: Story = { + decorators: [withMockUserId('')], + render: () => , + play: ({ canvasElement }) => { + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + expect(userIdTest).toBeTruthy(); + expect(userIdTest?.textContent).toBe('No User ID'); + }, +}; + +/** + * Test withMockUserId with special characters + * Verifies that userId with special characters is properly handled + */ +export const WithMockUserIdSpecialChars: Story = { + decorators: [withMockUserId('user-with-special@chars_123')], + render: () => , + play: ({ canvasElement }) => { + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + expect(userIdTest).toBeTruthy(); + expect(userIdTest?.textContent).toBe('user-with-special@chars_123'); + }, +}; + +/** + * Test withMockApolloClient with multiple queries + * Verifies that multiple mocks are properly handled + */ +export const WithMockApolloClientMultipleQueries: Story = { + decorators: [withMockApolloClient], + render: () => ( +
+ + +
+ ), + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TEST_QUERY, + }, + result: { + data: { test: 'first-result' }, + }, + }, + { + request: { + query: TEST_QUERY, + }, + result: { + data: { test: 'second-result' }, + }, + }, + ], + showWarnings: false, + }, + }, + play: async ({ canvasElement }) => { + await waitFor(() => { + const apolloQuery = canvasElement.querySelector('[data-testid="apollo-query"]'); + expect(apolloQuery).toBeTruthy(); + // First mock should be used + expect(apolloQuery?.textContent).toBe('first-result'); + }); + }, +}; + +/** + * Test Apollo InMemoryCache functionality + * Verifies that the cache is properly initialized + */ +export const WithMockApolloClientCache: Story = { + decorators: [withMockApolloClient], + render: () => { + const client = useApolloClient(); + // Verify cache is working by checking if extract() returns an object + const cacheData = client.cache.extract(); + const isCacheValid = cacheData !== null && typeof cacheData === 'object'; + return ( +
+ Cache Initialized: {isCacheValid ? 'Yes' : 'No'} +
+ ); + }, + parameters: { + apolloClient: { + mocks: [], + }, + }, + play: ({ canvasElement }) => { + const cacheTest = canvasElement.querySelector('[data-testid="cache-test"]'); + expect(cacheTest).toBeTruthy(); + expect(cacheTest?.textContent).toContain('Cache Initialized: Yes'); + }, +}; + +/** + * Test withMockRouter and withMockUserId integration + * Verifies that router and userId work together + */ +export const RouterAndUserIdIntegration: Story = { + decorators: [withMockRouter('/user/profile', true), withMockUserId('integration-user-456')], + render: () => ( +
+ + + +
+ ), + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + const authState = canvasElement.querySelector('[data-testid="auth-state"]'); + + expect(routerTest?.textContent).toContain('Current Path: /user/profile'); + expect(userIdTest?.textContent).toBe('integration-user-456'); + expect(authState?.textContent).toContain('Authenticated:'); + }, +}; + +/** + * Test withMockApolloClient with showWarnings enabled + * Verifies that warnings configuration is passed to MockLink + */ +export const WithMockApolloClientShowWarnings: Story = { + decorators: [withMockApolloClient], + render: () => , + parameters: { + apolloClient: { + mocks: [], + showWarnings: true, // Explicitly enable warnings + }, + }, + play: ({ canvasElement }) => { + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + expect(apolloTest).toBeTruthy(); + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + }, +}; + +/** + * Test nested routing scenarios + * Verifies deeply nested paths work correctly + */ +export const WithMockRouterDeeplyNested: Story = { + decorators: [withMockRouter('/app/users/123/settings/security/2fa', true)], + render: () => , + play: ({ canvasElement }) => { + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + expect(routerTest).toBeTruthy(); + expect(routerTest?.textContent).toContain('Current Path: /app/users/123/settings/security/2fa'); + }, +}; + +/** + * Test all three main decorators in different order + * Verifies decorator order doesn't break functionality + */ +export const DecoratorsReversedOrder: Story = { + decorators: [withMockRouter('/reversed'), withMockUserId('reversed-user'), withMockApolloClient], + render: () => ( +
+ + + +
+ ), + parameters: { + apolloClient: { + mocks: [], + }, + }, + play: ({ canvasElement }) => { + const apolloTest = canvasElement.querySelector('[data-testid="apollo-test"]'); + const userIdTest = canvasElement.querySelector('[data-testid="userid-test"]'); + const routerTest = canvasElement.querySelector('[data-testid="router-test"]'); + + expect(apolloTest?.textContent).toBe('Apollo Client Connected'); + expect(userIdTest?.textContent).toBe('reversed-user'); + expect(routerTest?.textContent).toContain('Current Path: /reversed'); + }, +}; diff --git a/apps/ui-sharethrift/src/test-utils/storybook-decorators.tsx b/apps/ui-sharethrift/src/test-utils/storybook-decorators.tsx index 561a895c0..a6485fd28 100644 --- a/apps/ui-sharethrift/src/test-utils/storybook-decorators.tsx +++ b/apps/ui-sharethrift/src/test-utils/storybook-decorators.tsx @@ -1,12 +1,12 @@ -import { type ReactNode, type ReactElement, useMemo } from 'react'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { AuthContext } from 'react-oidc-context'; import { ApolloClient, InMemoryCache } from '@apollo/client'; import { ApolloProvider } from '@apollo/client/react'; import { MockLink } from '@apollo/client/testing'; import type { Decorator, StoryContext } from '@storybook/react'; -import { createMockAuth, createMockUser } from '../test/utils/mock-auth.ts'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { UserIdProvider } from '../components/shared/user-context.tsx'; +import { MockAuthWrapper } from './storybook-mock-auth-wrappers.tsx'; +import { AuthProvider } from 'react-oidc-context'; +import { MockedProvider } from '@apollo/client/testing/react'; /** * Reusable Apollo Client decorator for Storybook stories. @@ -25,81 +25,23 @@ import { UserIdProvider } from '../components/shared/user-context.tsx'; * } as Meta; * ``` */ -export const withMockApolloClient: Decorator = (Story, context: StoryContext) => { +export const withMockApolloClient: Decorator = ( + Story, + context: StoryContext, +) => { const mocks = context.parameters?.['apolloClient']?.['mocks'] || []; - const showWarnings = context.parameters?.['apolloClient']?.['showWarnings'] ?? false; + const showWarnings = + context.parameters?.['apolloClient']?.['showWarnings'] ?? false; const mockLink = new MockLink(mocks, showWarnings); const client = new ApolloClient({ link: mockLink, cache: new InMemoryCache(), }); - return ( - - - - ); -}; - -/** - * Mock authentication wrapper component for Storybook stories. - * Provides a mocked AuthContext that simulates an authenticated user. - * - * NOTE: We cannot use AuthProvider directly because it requires a real OIDC server. - * AuthProvider from react-oidc-context attempts to connect to the authority URL, - * perform OAuth2/OIDC flows, and validate tokens. Since we're using a fake authority - * in Storybook, the authentication fails and useAuth() returns isAuthenticated: false. - * Instead, we use AuthContext.Provider directly with a mocked auth object. - * - * When any child component calls useAuth(), it uses React's useContext(AuthContext) internally. - * By wrapping with , we provide the mock data to - * that context, so components receive our mockAuth object with isAuthenticated: true. - * - * IMPLEMENTATION NOTE: This component uses createMockAuth() and createMockUser() utilities - * from '../test/utils/mockAuth.ts' instead of manually constructing the auth object. - * This eliminates TypeScript 'any' types, ensures type safety, and reuses the shared - * mock implementation that properly types all required OIDC fields (profile, tokens, events, etc.). - */ -export const MockAuthWrapper = ({ - children, -}: { - children: ReactNode; -}): ReactElement => { - const mockAuth = useMemo( - () => - createMockAuth({ - isAuthenticated: true, - user: createMockUser(), - }), - [], - ); - - return {children}; -}; - -/** - * Mock unauthenticated wrapper component for Storybook stories. - * Provides a mocked AuthContext that simulates an unauthenticated user. - * - * Use this when testing components that show different UI for logged-out users - * (e.g., Login/Sign Up buttons in headers). - */ -export const MockUnauthWrapper = ({ - children, -}: { - children: ReactNode; -}): ReactElement => { - const mockAuth = useMemo( - () => - createMockAuth({ - isAuthenticated: false, - user: null, - }), - [], - ); - return ( - {children} + + + ); }; @@ -146,9 +88,9 @@ export const withMockUserId = * ``` */ export const withMockRouter = - (initialRoute = '/'): Decorator => + (initialRoute = '/', isAuthenticated = true): Decorator => (Story) => ( - + } /> @@ -156,3 +98,63 @@ export const withMockRouter = ); + +const mockEnv = { + VITE_FUNCTION_ENDPOINT: 'https://mock-functions.example.com', + VITE_BLOB_STORAGE_CONFIG_URL: 'https://mock-storage.example.com', + VITE_B2C_AUTHORITY: 'https://mock-authority.example.com', + VITE_B2C_CLIENTID: 'mock-client-id', + NODE_ENV: 'development', +}; + +const mockStorage = { + getItem: (key: string) => { + if (key.includes('oidc.user')) { + return JSON.stringify({ + access_token: '', + profile: { sub: 'test-user' }, + }); + } + return null; + }, + setItem: () => Promise.resolve(), + removeItem: () => Promise.resolve(), + clear: () => Promise.resolve(), + key: () => null, + length: 0, + set: () => Promise.resolve(), + get: () => Promise.resolve(null), + remove: () => Promise.resolve(null), + getAllKeys: () => Promise.resolve([]), +}; + +// Apply mocks to global environment for stories +Object.defineProperty(globalThis, 'sessionStorage', { + value: mockStorage, + writable: true, +}); +Object.defineProperty(globalThis, 'localStorage', { + value: mockStorage, + writable: true, +}); + +Object.defineProperty(import.meta, 'env', { + value: mockEnv, + writable: true, +}); + +// StoryFn's runtime signature can vary; accept `any` here so the +// decorator works regardless of Storybook's inferred types. +export const withAuthDecorator: Decorator = (Story) => ( + + + + + +); \ No newline at end of file diff --git a/apps/ui-sharethrift/src/test-utils/storybook-mock-auth-wrappers.tsx b/apps/ui-sharethrift/src/test-utils/storybook-mock-auth-wrappers.tsx new file mode 100644 index 000000000..a3e23d9a0 --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/storybook-mock-auth-wrappers.tsx @@ -0,0 +1,68 @@ +import { type ReactElement, type ReactNode, useMemo } from 'react'; +import { AuthContext } from 'react-oidc-context'; +import { createMockAuth,createMockUser } from '../test/utils/mock-auth'; + +/** + * Mock unauthenticated wrapper component for Storybook stories. + * Provides a mocked AuthContext that simulates an unauthenticated user. + * + * Use this when testing components that show different UI for logged-out users + * (e.g., Login/Sign Up buttons in headers). + */ +export const MockUnauthWrapper = ({ + children, +}: { + children: ReactNode; +}): ReactElement => { + const mockAuth = useMemo( + () => + createMockAuth({ + isAuthenticated: false, + user: null, + }), + [], + ); + + return ( + {children} + ); +}; + + +/** + * Mock authentication wrapper component for Storybook stories. + * Provides a mocked AuthContext that simulates an authenticated user. + * + * NOTE: We cannot use AuthProvider directly because it requires a real OIDC server. + * AuthProvider from react-oidc-context attempts to connect to the authority URL, + * perform OAuth2/OIDC flows, and validate tokens. Since we're using a fake authority + * in Storybook, the authentication fails and useAuth() returns isAuthenticated: false. + * Instead, we use AuthContext.Provider directly with a mocked auth object. + * + * When any child component calls useAuth(), it uses React's useContext(AuthContext) internally. + * By wrapping with , we provide the mock data to + * that context, so components receive our mockAuth object with isAuthenticated: true. + * + * IMPLEMENTATION NOTE: This component uses createMockAuth() and createMockUser() utilities + * from '../test/utils/mockAuth.ts' instead of manually constructing the auth object. + * This eliminates TypeScript 'any' types, ensures type safety, and reuses the shared + * mock implementation that properly types all required OIDC fields (profile, tokens, events, etc.). + */ +export const MockAuthWrapper = ({ + children, + isAuthenticated = true, +}: { + children: ReactNode; + isAuthenticated?: boolean; +}): ReactElement => { + const mockAuth = useMemo( + () => + createMockAuth({ + isAuthenticated: isAuthenticated, + user: createMockUser(), + }), + [isAuthenticated], + ); + + return {children}; +}; diff --git a/apps/ui-sharethrift/src/test-utils/storybook-mock-helpers.ts b/apps/ui-sharethrift/src/test-utils/storybook-mock-helpers.ts new file mode 100644 index 000000000..bd8371772 --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/storybook-mock-helpers.ts @@ -0,0 +1,93 @@ +import { type AccountPlan, UseUserIsAdminDocument } from '../generated'; + +export const UserIsAdminMockRequest = (userId: string, isAdmin: boolean = false) => { + const typename = isAdmin ? 'AdminUser' : 'PersonalUser'; + return { + request: { + query: UseUserIsAdminDocument, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + + result: { + data: { + currentUser: { + __typename: typename, + id: userId, + userIsAdmin: isAdmin, + }, + }, + }, + }; +}; + +export const mockAccountPlans: AccountPlan[] = [ + { + name: 'non-verified-personal', + description: 'Non-Verified Personal', + billingPeriodLength: 0, + billingPeriodUnit: 'month', + billingAmount: 0, + currency: 'USD', + setupFee: 0, + feature: { + activeReservations: 0, + bookmarks: 3, + itemsToShare: 15, + friends: 5, + __typename: 'AccountPlanFeature', + }, + status: null, + cybersourcePlanId: null, + id: '607f1f77bcf86cd799439001', + schemaVersion: '1.0.0', + createdAt: '2023-05-02T10:00:00.000Z', + updatedAt: '2023-05-02T10:00:00.000Z', + __typename: 'AccountPlan', + }, + { + name: 'verified-personal', + description: 'Verified Personal', + billingPeriodLength: 0, + billingPeriodUnit: 'month', + billingAmount: 0, + currency: 'USD', + setupFee: 0, + feature: { + activeReservations: 10, + bookmarks: 10, + itemsToShare: 30, + friends: 10, + __typename: 'AccountPlanFeature', + }, + status: null, + cybersourcePlanId: null, + id: '607f1f77bcf86cd799439002', + schemaVersion: '1.0.0', + createdAt: '2023-05-02T10:00:00.000Z', + updatedAt: '2023-05-02T10:00:00.000Z', + __typename: 'AccountPlan', + }, + { + name: 'verified-personal-plus', + description: 'Verified Personal Plus', + billingPeriodLength: 12, + billingPeriodUnit: 'month', + billingAmount: 4.99, + currency: 'USD', + setupFee: 0, + feature: { + activeReservations: 30, + bookmarks: 30, + itemsToShare: 50, + friends: 30, + __typename: 'AccountPlanFeature', + }, + status: 'active', + cybersourcePlanId: 'cybersource_plan_001', + id: '607f1f77bcf86cd799439000', + schemaVersion: '1.0.0', + createdAt: '2023-05-02T10:00:00.000Z', + updatedAt: '2023-05-02T10:00:00.000Z', + __typename: 'AccountPlan', + }, +]; diff --git a/apps/ui-sharethrift/vitest.config.ts b/apps/ui-sharethrift/vitest.config.ts index 8373504a0..d159e910d 100644 --- a/apps/ui-sharethrift/vitest.config.ts +++ b/apps/ui-sharethrift/vitest.config.ts @@ -14,14 +14,13 @@ export default defineConfig( additionalCoverageExclude: [ '**/index.ts', '**/index.tsx', - '**/Index.tsx', + '**/Index.tsx', 'src/main.tsx', - 'src/test-utils/**', - 'src/config/**', - 'src/test/**', + 'src/config/**', + 'src/test/**', '**/*.d.ts', 'src/generated/**', - 'eslint.config.js' + 'eslint.config.js', ], testTimeout: 60000, hookTimeout: 60000, diff --git a/packages/cellix/ui-core/package.json b/packages/cellix/ui-core/package.json index 0ad428c2c..031e9fa97 100644 --- a/packages/cellix/ui-core/package.json +++ b/packages/cellix/ui-core/package.json @@ -39,19 +39,19 @@ "@cellix/typescript-config": "workspace:*", "@cellix/vitest-config": "workspace:*", "@chromatic-com/storybook": "^4.1.3", - "@storybook/addon-a11y": "^10.1.11", - "@storybook/addon-docs": "^10.1.11", - "@storybook/addon-onboarding": "^10.1.11", - "@storybook/addon-vitest": "^10.1.11", - "@storybook/react": "^10.1.11", - "@storybook/react-vite": "^10.1.11", + "@storybook/addon-a11y": "catalog:", + "@storybook/addon-docs": "catalog:", + "@storybook/addon-onboarding": "catalog:", + "@storybook/addon-vitest": "catalog:", + "@storybook/react": "catalog:", + "@storybook/react-vite": "catalog:", "@types/react": "^19.1.16", "@vitest/coverage-v8": "catalog:", "jsdom": "^26.1.0", "react-oidc-context": "^3.3.0", "react-router-dom": "^7.12.0", "rimraf": "^6.0.1", - "storybook": "^10.2.10", + "storybook": "catalog:", "typescript": "^5.8.3", "vitest": "catalog:" }, diff --git a/packages/cellix/vitest-config/package.json b/packages/cellix/vitest-config/package.json index be6da39e9..16c1f0060 100644 --- a/packages/cellix/vitest-config/package.json +++ b/packages/cellix/vitest-config/package.json @@ -11,7 +11,7 @@ "build": "tsc --build" }, "dependencies": { - "@storybook/addon-vitest": "^10.1.11", + "@storybook/addon-vitest": "catalog:", "@vitest/browser-playwright": "catalog:", "vitest": "catalog:" }, diff --git a/packages/sthrift/ui-components/package.json b/packages/sthrift/ui-components/package.json index 5b57dcf5c..daaf7aca1 100644 --- a/packages/sthrift/ui-components/package.json +++ b/packages/sthrift/ui-components/package.json @@ -60,18 +60,18 @@ "@cellix/typescript-config": "workspace:*", "@cellix/vitest-config": "workspace:*", "@chromatic-com/storybook": "^4.1.3", - "@storybook/addon-a11y": "^10.1.11", - "@storybook/addon-docs": "^10.1.11", - "@storybook/addon-onboarding": "^10.1.11", - "@storybook/addon-vitest": "^10.1.11", - "@storybook/react": "^10.1.11", - "@storybook/react-vite": "^10.1.11", + "@storybook/addon-a11y": "catalog:", + "@storybook/addon-docs": "catalog:", + "@storybook/addon-onboarding": "catalog:", + "@storybook/addon-vitest": "catalog:", + "@storybook/react": "catalog:", + "@storybook/react-vite": "catalog:", "@types/react": "^19.1.11", "@types/react-dom": "^19.1.6", "@vitest/coverage-v8": "catalog:", "jsdom": "^26.1.0", "rimraf": "^6.0.1", - "storybook": "^10.2.10", + "storybook": "catalog:", "typescript": "^5.8.3", "vite": "catalog:", "vitest": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23fa33d5c..46cff5c4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,24 @@ settings: catalogs: default: + '@storybook/addon-a11y': + specifier: 10.2.10 + version: 10.2.10 + '@storybook/addon-docs': + specifier: 10.2.10 + version: 10.2.10 + '@storybook/addon-onboarding': + specifier: 10.2.10 + version: 10.2.10 + '@storybook/addon-vitest': + specifier: 10.2.10 + version: 10.2.10 + '@storybook/react': + specifier: 10.2.10 + version: 10.2.10 + '@storybook/react-vite': + specifier: 10.2.10 + version: 10.2.10 '@vitest/browser-playwright': specifier: ^4.0.15 version: 4.0.17 @@ -18,6 +36,9 @@ catalogs: mongoose: specifier: 8.17.0 version: 8.17.0 + storybook: + specifier: 10.2.10 + version: 10.2.10 typescript: specifier: 5.9.3 version: 5.9.3 @@ -465,20 +486,20 @@ importers: specifier: ^3.2.0 version: 3.2.0(graphql@16.12.0) '@storybook/addon-a11y': - specifier: ^10.1.11 - version: 10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + specifier: 'catalog:' + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-docs': - specifier: ^10.1.11 - version: 10.2.8(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + specifier: 'catalog:' + version: 10.2.10(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@storybook/addon-vitest': - specifier: ^10.1.11 - version: 10.2.8(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) + specifier: 'catalog:' + version: 10.2.10(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) '@storybook/react': - specifier: ^10.1.11 - version: 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) + specifier: 'catalog:' + version: 10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^10.1.11 - version: 10.2.8(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + specifier: 'catalog:' + version: 10.2.10(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -513,7 +534,7 @@ importers: specifier: ^16.3.0 version: 16.5.0 storybook: - specifier: ^10.2.10 + specifier: 'catalog:' version: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: specifier: ~5.8.3 @@ -1053,23 +1074,23 @@ importers: specifier: ^4.1.3 version: 4.1.3(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-a11y': - specifier: ^10.1.11 - version: 10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + specifier: 'catalog:' + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-docs': - specifier: ^10.1.11 - version: 10.2.8(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + specifier: 'catalog:' + version: 10.2.10(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@storybook/addon-onboarding': - specifier: ^10.1.11 - version: 10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + specifier: 'catalog:' + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-vitest': - specifier: ^10.1.11 - version: 10.2.8(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) + specifier: 'catalog:' + version: 10.2.10(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) '@storybook/react': - specifier: ^10.1.11 - version: 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) + specifier: 'catalog:' + version: 10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^10.1.11 - version: 10.2.8(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + specifier: 'catalog:' + version: 10.2.10(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@types/react': specifier: ^19.1.16 version: 19.2.9 @@ -1089,7 +1110,7 @@ importers: specifier: ^6.0.1 version: 6.1.2 storybook: - specifier: ^10.2.10 + specifier: 'catalog:' version: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: specifier: ^5.8.3 @@ -1101,8 +1122,8 @@ importers: packages/cellix/vitest-config: dependencies: '@storybook/addon-vitest': - specifier: ^10.1.11 - version: 10.2.8(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) + specifier: 'catalog:' + version: 10.2.10(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) '@vitest/browser-playwright': specifier: 'catalog:' version: 4.0.17(playwright@1.57.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17) @@ -1432,23 +1453,23 @@ importers: specifier: ^4.1.3 version: 4.1.3(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-a11y': - specifier: ^10.1.11 - version: 10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + specifier: 'catalog:' + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-docs': - specifier: ^10.1.11 - version: 10.2.8(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + specifier: 'catalog:' + version: 10.2.10(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@storybook/addon-onboarding': - specifier: ^10.1.11 - version: 10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + specifier: 'catalog:' + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@storybook/addon-vitest': - specifier: ^10.1.11 - version: 10.2.8(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) + specifier: 'catalog:' + version: 10.2.10(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17) '@storybook/react': - specifier: ^10.1.11 - version: 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) + specifier: 'catalog:' + version: 10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^10.1.11 - version: 10.2.8(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + specifier: 'catalog:' + version: 10.2.10(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@types/react': specifier: ^19.1.11 version: 19.2.9 @@ -1465,7 +1486,7 @@ importers: specifier: ^6.0.1 version: 6.1.2 storybook: - specifier: ^10.2.10 + specifier: 'catalog:' version: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: specifier: ^5.8.3 @@ -4829,28 +4850,28 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-a11y@10.2.8': - resolution: {integrity: sha512-EW5MzPKNzyPorvodd416U2Np+zEdMPe+BSyomjm0oCXoC/6rDurf05H1pa99rZsrTDRrpog+HCz8iVa4XSwN5Q==} + '@storybook/addon-a11y@10.2.10': + resolution: {integrity: sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q==} peerDependencies: - storybook: ^10.2.8 + storybook: ^10.2.10 - '@storybook/addon-docs@10.2.8': - resolution: {integrity: sha512-cEoWqQrLzrxOwZFee5zrD4cYrdEWKV80POb7jUZO0r5vfl2DuslIr3n/+RfLT52runCV4aZcFEfOfP/IWHNPxg==} + '@storybook/addon-docs@10.2.10': + resolution: {integrity: sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ==} peerDependencies: - storybook: ^10.2.8 + storybook: ^10.2.10 - '@storybook/addon-onboarding@10.2.8': - resolution: {integrity: sha512-/+TD055ZDmM325RYrDKqle51P1iT3GiFyDrcCYNOGTUEp3lAu/qplgOC0xMZudiv2y4ExlNYD26lJoGSTNHfHg==} + '@storybook/addon-onboarding@10.2.10': + resolution: {integrity: sha512-DkzZQTXHp99SpHMIQ5plbbHcs4EWVzWhLXlW+icA8sBlKo5Bwj540YcOApKbqB0m/OzWprsznwN7Kv4vfvHu4w==} peerDependencies: - storybook: ^10.2.8 + storybook: ^10.2.10 - '@storybook/addon-vitest@10.2.8': - resolution: {integrity: sha512-D+9QWBqURAlS9+js9fvBToe7RzhIM9/ep/q8lunpVvcTkbOxh5++cYWBD8PVqmzZ+U432w9h2UwPuxPsWD/loQ==} + '@storybook/addon-vitest@10.2.10': + resolution: {integrity: sha512-U2oHw+Ar+Xd06wDTB74VlujhIIW89OHThpJjwgqgM6NWrOC/XLllJ53ILFDyREBkMwpBD7gJQIoQpLEcKBIEhw==} peerDependencies: '@vitest/browser': ^3.0.0 || ^4.0.0 '@vitest/browser-playwright': ^4.0.0 '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^10.2.8 + storybook: ^10.2.10 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: '@vitest/browser': @@ -4862,18 +4883,18 @@ packages: vitest: optional: true - '@storybook/builder-vite@10.2.8': - resolution: {integrity: sha512-+6/Lwi7W0YIbzHDh798GPp0IHUYDwp0yv0Y1eVNK/StZD0tnv4/1C28NKyP+O7JOsFsuWI1qHiDhw8kNURugZw==} + '@storybook/builder-vite@10.2.10': + resolution: {integrity: sha512-Wd6CYL7LvRRNiXMz977x9u/qMm7nmMw/7Dow2BybQo+Xbfy1KhVjIoZ/gOiG515zpojSozctNrJUbM0+jH1jwg==} peerDependencies: - storybook: ^10.2.8 + storybook: ^10.2.10 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/csf-plugin@10.2.8': - resolution: {integrity: sha512-kKkLYhRXb33YtIPdavD2DU25sb14sqPYdcQFpyqu4TaD9truPPqW8P5PLTUgERydt/eRvRlnhauPHavU1kjsnA==} + '@storybook/csf-plugin@10.2.10': + resolution: {integrity: sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.2.8 + storybook: ^10.2.10 vite: '*' webpack: '*' peerDependenciesMeta: @@ -4895,27 +4916,27 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@10.2.8': - resolution: {integrity: sha512-Xde9X3VszFV1pTXfc2ZFM89XOCGRxJD8MUIzDwkcT9xaki5a+8srs/fsXj75fMY6gMYfcL5lNRZvCqg37HOmcQ==} + '@storybook/react-dom-shim@10.2.10': + resolution: {integrity: sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.8 + storybook: ^10.2.10 - '@storybook/react-vite@10.2.8': - resolution: {integrity: sha512-x5kmw+TPhxkQV84n4e9X0q6/rA5T8V2QQFolMuN+U93q1HX1r+GZ6g/nXaaq9ox168PhHUJZQnn+LzSQKGCMBA==} + '@storybook/react-vite@10.2.10': + resolution: {integrity: sha512-C652GhZHXURi+gFqqLKmZPskEq1FQto4VCf/eQea2exmdVS0nOB+FFWQZNCivX6mpkDHza8UxRZNFpDB0mWcJQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.8 + storybook: ^10.2.10 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/react@10.2.8': - resolution: {integrity: sha512-nMFqQFUXq6Zg2O5SeuomyWnrIx61QfpNQMrfor8eCEzHrWNnXrrvVsz2RnHIgXN8RVyaWGDPh1srAECu/kDHXw==} + '@storybook/react@10.2.10': + resolution: {integrity: sha512-PcsChzPI8lhllB9exV7nFb96093i6sTwIl0jpPjaTFPQCRoueR9E/YeP3qSKQL9xt4cmii0cW7F0RUx25rW93Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.8 + storybook: ^10.2.10 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -13255,7 +13276,7 @@ snapshots: '@babel/traverse@7.28.6': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.6 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.6 @@ -15014,7 +15035,7 @@ snapshots: '@graphql-tools/utils': 8.9.0(graphql@16.12.0) dataloader: 2.1.0 graphql: 16.12.0 - tslib: 2.4.1 + tslib: 2.8.1 value-or-promise: 1.0.11 '@graphql-tools/batch-execute@9.0.19(graphql@16.12.0)': @@ -15229,7 +15250,7 @@ snapshots: '@graphql-tools/optimize@2.0.0(graphql@16.12.0)': dependencies: graphql: 16.12.0 - tslib: 2.6.3 + tslib: 2.8.1 '@graphql-tools/prisma-loader@8.0.17(@types/node@24.10.9)(graphql@16.12.0)': dependencies: @@ -15265,7 +15286,7 @@ snapshots: '@ardatan/relay-compiler': 12.0.3(graphql@16.12.0) '@graphql-tools/utils': 11.0.0(graphql@16.12.0) graphql: 16.12.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - encoding @@ -15326,7 +15347,7 @@ snapshots: '@graphql-tools/utils@8.9.0(graphql@16.12.0)': dependencies: graphql: 16.12.0 - tslib: 2.4.1 + tslib: 2.8.1 '@graphql-tools/wrap@10.1.4(graphql@16.12.0)': dependencies: @@ -16541,18 +16562,18 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@storybook/addon-a11y@10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.1 storybook: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/addon-docs@10.2.8(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': + '@storybook/addon-docs@10.2.10(@types/react@19.2.9)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.9)(react@19.2.3) - '@storybook/csf-plugin': 10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + '@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) '@storybook/icons': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-dom-shim': 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/react-dom-shim': 10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) storybook: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -16564,11 +16585,11 @@ snapshots: - vite - webpack - '@storybook/addon-onboarding@10.2.8(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@storybook/addon-onboarding@10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: storybook: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/addon-vitest@10.2.8(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17)': + '@storybook/addon-vitest@10.2.10(@vitest/browser-playwright@4.0.17)(@vitest/browser@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.17))(@vitest/runner@4.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vitest@4.0.17)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -16582,9 +16603,9 @@ snapshots: - react - react-dom - '@storybook/builder-vite@10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': + '@storybook/builder-vite@10.2.10(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': dependencies: - '@storybook/csf-plugin': 10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + '@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) storybook: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ts-dedent: 2.2.0 vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) @@ -16593,7 +16614,7 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': + '@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': dependencies: storybook: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) unplugin: 2.3.11 @@ -16610,18 +16631,18 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@storybook/react-dom-shim@10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@storybook/react-dom-shim@10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) storybook: 10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-vite@10.2.8(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': + '@storybook/react-vite@10.2.10(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.8.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) - '@storybook/react': 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) + '@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.3)) + '@storybook/react': 10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.3 @@ -16638,10 +16659,10 @@ snapshots: - typescript - webpack - '@storybook/react@10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)': + '@storybook/react@10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/react-dom-shim': 10.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.10(@testing-library/dom@10.4.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) react: 19.2.3 react-docgen: 8.0.2 react-dom: 19.2.3(react@19.2.3) @@ -18010,7 +18031,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.6.3 + tslib: 2.8.1 camelcase@5.0.0: {} @@ -18099,7 +18120,7 @@ snapshots: path-case: 3.0.4 sentence-case: 3.0.4 snake-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 char-regex@1.0.2: {} @@ -18324,7 +18345,7 @@ snapshots: constant-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 upper-case: 2.0.2 content-disposition@0.5.2: {} @@ -18454,7 +18475,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.6) postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: webpack: 5.104.1 @@ -18796,7 +18817,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 dot-prop@6.0.1: dependencies: @@ -19881,7 +19902,7 @@ snapshots: header-case@2.0.4: dependencies: capital-case: 1.0.4 - tslib: 2.6.3 + tslib: 2.8.1 history@4.10.1: dependencies: @@ -20253,7 +20274,7 @@ snapshots: is-lower-case@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 is-map@2.0.3: {} @@ -20336,7 +20357,7 @@ snapshots: is-upper-case@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 is-weakmap@2.0.2: {} @@ -20705,11 +20726,11 @@ snapshots: lower-case-first@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 lower-case@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 lowercase-keys@3.0.0: {} @@ -21569,7 +21590,7 @@ snapshots: normalize-package-data@8.0.0: dependencies: hosted-git-info: 9.0.2 - semver: 7.7.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 normalize-path@2.1.1: @@ -21775,7 +21796,7 @@ snapshots: got: 12.6.1 registry-auth-token: 5.1.1 registry-url: 6.0.1 - semver: 7.7.3 + semver: 7.7.4 pad-right@0.2.2: dependencies: @@ -21786,7 +21807,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 parent-module@1.0.1: dependencies: @@ -21810,14 +21831,14 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 parse-json@8.3.0: dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 index-to-position: 1.2.0 type-fest: 4.41.0 @@ -21839,14 +21860,14 @@ snapshots: pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 path-browserify@1.0.1: {} path-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 path-exists@4.0.0: {} @@ -22097,7 +22118,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@5.8.3) jiti: 1.21.7 postcss: 8.5.6 - semver: 7.7.3 + semver: 7.7.4 webpack: 5.104.1 transitivePeerDependencies: - typescript @@ -23396,7 +23417,7 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 semver@5.7.2: {} @@ -23427,7 +23448,7 @@ snapshots: sentence-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 upper-case-first: 2.0.2 seq-queue@0.0.5: {} @@ -23635,7 +23656,7 @@ snapshots: snake-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.6.3 + tslib: 2.8.1 snyk@1.1302.0: dependencies: @@ -23712,7 +23733,7 @@ snapshots: sponge-case@1.0.1: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 sprintf-js@1.0.3: {} @@ -23948,7 +23969,7 @@ snapshots: swap-case@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 symbol-tree@3.2.4: {} @@ -24081,7 +24102,7 @@ snapshots: title-case@3.0.3: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 tldts-core@6.1.86: {} @@ -24437,11 +24458,11 @@ snapshots: upper-case-first@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 upper-case@2.0.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 uri-js@4.4.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b2d8c95f8..1ae19ca17 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,14 @@ catalog: '@vitest/coverage-v8': ^4.0.15 '@vitest/browser-playwright': ^4.0.15 vite: ^7.3.0 + # Storybook 10.x + storybook: 10.2.10 + '@storybook/addon-a11y': 10.2.10 + '@storybook/addon-docs': 10.2.10 + '@storybook/addon-onboarding': 10.2.10 + '@storybook/addon-vitest': 10.2.10 + '@storybook/react': 10.2.10 + '@storybook/react-vite': 10.2.10 overrides: node-forge@<1.3.2: '>=1.3.2'