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/src/App.container.stories.tsx b/apps/ui-sharethrift/src/App.container.stories.tsx index 673da5413..7f1cff8b7 100644 --- a/apps/ui-sharethrift/src/App.container.stories.tsx +++ b/apps/ui-sharethrift/src/App.container.stories.tsx @@ -3,10 +3,60 @@ import { AppContainer } from './App.container.tsx'; import { withMockApolloClient, withMockRouter, - MockUnauthWrapper, } from './test-utils/storybook-decorators.tsx'; -import { AppContainerCurrentUserDocument } from './generated.tsx'; +import { + AppContainerCurrentUserDocument, + ListingsPageContainerGetListingsDocument, + SelectAccountTypeContainerAccountPlansDocument, + SelectAccountTypeCurrentPersonalUserAndCreateIfNotExistsDocument, +} from './generated.tsx'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { MockUnauthWrapper } from './test-utils/storybook-mock-auth-wrappers.tsx'; +import { userIsAdminMockRequest } from './test-utils/storybook-helpers.ts'; + +const mockListings = [ + { + __typename: 'ItemListing', + id: '1', + title: 'Projector', + description: 'High-quality projector for home and office use', + category: 'Tools & Equipment', + location: 'Toronto, ON', + state: 'Active', + images: ['/assets/item-images/projector.png'], + sharingPeriodStart: '2025-01-01', + sharingPeriodEnd: '2025-12-31', + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2025-01-01T00:00:00Z', + schemaVersion: '1.0', + version: '1.0', + reports: 0, + sharingHistory: [], + }, + { + __typename: 'ItemListing', + id: '2', + title: 'Umbrella', + description: 'Umbrella in excellent condition', + category: 'Accessories', + location: 'Vancouver, BC', + state: 'Active', + images: ['/assets/item-images/umbrella.png'], + sharingPeriodStart: '2025-02-01', + sharingPeriodEnd: '2025-06-30', + createdAt: '2025-01-15T00:00:00Z', + updatedAt: '2025-01-15T00:00:00Z', + schemaVersion: '1.0', + version: '1.0', + reports: 0, + sharingHistory: [], + }, +]; + +const buildListingsMock = (listings = mockListings) => ({ + request: { query: ListingsPageContainerGetListingsDocument }, + result: { data: { itemListings: listings } }, +}); const meta: Meta = { title: 'App/AppContainer', @@ -14,31 +64,16 @@ const meta: Meta = { 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; -// Mock for authenticated user who has completed onboarding -const mockAuthenticatedCompletedOnboarding = { - request: { - query: AppContainerCurrentUserDocument, - variables: {}, - }, - result: { - data: { - currentUser: { - __typename: 'PersonalUser' as const, - id: 'user-123', - userType: 'personal-user', - hasCompletedOnboarding: true, - }, - }, - }, -}; - -// Mock for authenticated user who hasn't completed onboarding -const mockAuthenticatedNotCompletedOnboarding = { +// Helper to build a current-user mock +const buildCurrentUserMock = ( + opts: { id?: string; hasCompletedOnboarding?: boolean } = {}, +) => ({ request: { query: AppContainerCurrentUserDocument, variables: {}, @@ -47,19 +82,33 @@ const mockAuthenticatedNotCompletedOnboarding = { data: { currentUserAndCreateIfNotExists: { __typename: 'PersonalUser' as const, - id: 'user-456', + id: opts.id ?? 'user-123', userType: 'personal-user', - hasCompletedOnboarding: false, + hasCompletedOnboarding: opts.hasCompletedOnboarding ?? true, }, }, }, -}; +}); + +// Reusable canned responses +const mockAuthenticatedCompletedOnboarding = buildCurrentUserMock({ + id: 'user-123', + hasCompletedOnboarding: true, +}); +const mockAuthenticatedNotCompletedOnboarding = buildCurrentUserMock({ + id: 'user-456', + hasCompletedOnboarding: false, +}); export const AuthenticatedCompletedOnboarding: Story = { decorators: [withMockApolloClient, withMockRouter('/')], parameters: { apolloClient: { - mocks: [mockAuthenticatedCompletedOnboarding], + mocks: [ + mockAuthenticatedCompletedOnboarding, + buildListingsMock(), + userIsAdminMockRequest('user-123', false), + ], }, }, }; @@ -68,7 +117,105 @@ export const AuthenticatedNotCompletedOnboarding: Story = { decorators: [withMockApolloClient, withMockRouter('/')], parameters: { apolloClient: { - mocks: [mockAuthenticatedNotCompletedOnboarding], + mocks: [ + mockAuthenticatedNotCompletedOnboarding, + { + request: { + query: + SelectAccountTypeCurrentPersonalUserAndCreateIfNotExistsDocument, + }, + result: { + data: { + currentPersonalUserAndCreateIfNotExists: { + id: 'user-456', + account: { + accountType: 'personal-user', + }, + }, + }, + }, + }, + buildListingsMock(), + userIsAdminMockRequest('user-456', false), + { + request: { query: SelectAccountTypeContainerAccountPlansDocument }, + result: { + data: { + accountPlans: [ + { + 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', + }, + ], + }, + }, + }, + ], }, }, }; @@ -88,7 +235,7 @@ export const Unauthenticated: Story = { ], parameters: { apolloClient: { - mocks: [], + mocks: [buildListingsMock(), userIsAdminMockRequest('user-456', false)], }, }, }; diff --git a/apps/ui-sharethrift/src/App.stories.tsx b/apps/ui-sharethrift/src/App.stories.tsx index cf3b50ed4..8ced8a1eb 100644 --- a/apps/ui-sharethrift/src/App.stories.tsx +++ b/apps/ui-sharethrift/src/App.stories.tsx @@ -1,46 +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'; - -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: (_key: string, _value: string) => Promise.resolve(), - removeItem: (_key: string) => Promise.resolve(), - clear: () => Promise.resolve(), - key: () => null, - length: 0, - set: (_key: string, _value: unknown) => Promise.resolve(), - get: (_key: string) => Promise.resolve(null), - remove: (_key: string) => Promise.resolve(null), - getAllKeys: () => Promise.resolve([]), -}; - -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, -}); +import { withAuthDecorator } from './test-utils/storybook-decorators.tsx'; const meta: Meta = { title: 'App/Main Application', @@ -49,6 +11,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: { @@ -58,21 +21,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 712ff7128..0cadce5ed 100644 --- a/apps/ui-sharethrift/src/App.tsx +++ b/apps/ui-sharethrift/src/App.tsx @@ -1,11 +1,11 @@ import { Route, Routes } from 'react-router-dom'; -import { AppRoutes } from './components/layouts/app/index.tsx'; import { SignupRoutes } from './components/layouts/signup/index.tsx'; -import { LoginSelection } from './components/shared/login-selection.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'; import { useOnboardingRedirect } from './components/shared/use-has-completed-onboarding-check.ts'; +import { LoginSelection } from './components/layouts/login/login-selection.tsx'; +import { AppRoutes } from './components/layouts/app/index.tsx'; const signupSection = ( 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 b9cbfec80..bc3e969d0 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,24 +1,23 @@ -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 { withMockApolloClient,withMockRouter } from "../../../test-utils/storybook-decorators.tsx"; import { ListingsPageContainerGetListingsDocument } from "../../../generated.tsx"; -import { withMockApolloClient, withMockRouter } from "../../../test-utils/storybook-decorators.tsx"; -import { expect, within } from 'storybook/test'; 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 }) => { const canvas = within(canvasElement); await expect(canvas.getByRole('main')).toBeInTheDocument(); 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/ProfilePage.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/ProfilePage.stories.tsx index 554036691..9753a1fa3 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/ProfilePage.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/profile/pages/ProfilePage.stories.tsx @@ -7,52 +7,60 @@ import { import { HomeAccountProfileViewContainerCurrentUserDocument, HomeAccountProfileViewContainerUserListingsDocument, - UseUserIsAdminDocument, type ItemListing, type PersonalUser, } from '../../../../../../../../generated.tsx'; import { expect, within } from 'storybook/test'; +import { userIsAdminMockRequest } from '../../../../../../../../test-utils/storybook-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 }) => { - const canvas = within(canvasElement); - await expect(canvas.getByRole('main')).toBeInTheDocument(); - }, 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, @@ -152,6 +144,10 @@ export const DefaultView: Story = { ], }, }, + 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,7 +172,7 @@ export const NoListings: Story = { }, result: { data: { - myListingsAll: { items: [], total: 0, page: 1, pageSize: 100 }, + myListingsAll: { items: [], total: 0, page: 1, pageSize: 100 }, }, }, }, 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 fd51feed1..41cb45dbc 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: { layout: 'fullscreen', apolloClient: { @@ -145,7 +146,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingText = canvas.queryByText(/Loading/i); expect(loadingText || canvasElement).toBeTruthy(); @@ -215,7 +216,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.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/Settings.stories.tsx index 48c318275..3f66a8d7c 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/Settings.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/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/components/layouts/app/pages/account/pages/settings/pages/SettingsPage.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/SettingsPage.stories.tsx index f32345f62..951289c6e 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/SettingsPage.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/account/pages/settings/pages/SettingsPage.stories.tsx @@ -1,138 +1,80 @@ -import type { Meta, StoryFn } from "@storybook/react"; -import { AppRoutes } from "../../../../../index.tsx"; -import { HomeAccountSettingsViewContainerCurrentUserDocument } from "../../../../../../../../generated.tsx"; -import { withMockApolloClient, withMockRouter } from "../../../../../../../../test-utils/storybook-decorators.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 { HomeAccountSettingsViewContainerCurrentUserDocument } from '../../../../../../../../generated.tsx'; +import { userIsAdminMockRequest } from '../../../../../../../../test-utils/storybook-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({}); - -DefaultView.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await expect(canvas.getByRole('main')).toBeInTheDocument(); -}; +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.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, - }, - }, - }, - }, - ], - }, + 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(); + }, }; \ 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.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 e5c8b392f..c7d434a69 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 @@ -16,6 +16,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: { layout: 'fullscreen', apolloClient: { @@ -215,7 +216,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-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-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 206e5fdb5..0a087af00 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, @@ -125,7 +126,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); @@ -160,7 +161,7 @@ export const Empty: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -180,7 +181,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -237,7 +238,7 @@ export const WithBlockedUser: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvasElement).toBeTruthy(); const blockedText = canvas.queryByText(/Blocked/i); @@ -280,7 +281,7 @@ export const BlockUserError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -326,7 +327,7 @@ export const ManyUsers: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -364,7 +365,7 @@ export const UnblockUserError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -384,7 +385,7 @@ export const WithError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1152,7 +1153,7 @@ export const BlockUserMutationNetworkError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1190,7 +1191,7 @@ export const UnblockUserMutationNetworkError: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1225,7 +1226,7 @@ export const SortingWithArrayField: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1260,7 +1261,7 @@ export const SortingWithNullField: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1295,7 +1296,7 @@ export const SearchWithPageReset: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1330,7 +1331,7 @@ export const StatusFilterWithPageReset: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -1365,7 +1366,7 @@ export const TableChangeWithSorter: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); }, }; @@ -2076,7 +2077,7 @@ export const StatusFilterActive: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); // Status filter interaction would be tested here }, @@ -2112,7 +2113,7 @@ export const StatusFilterBlocked: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { expect(canvasElement).toBeTruthy(); // Status filter interaction would be tested here }, @@ -2403,7 +2404,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/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 a42cb3936..db1700fac 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,29 +1,43 @@ 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-helpers'; +import { expect, within } from 'storybook/test'; -const meta: Meta = { - title: 'Pages/AdminDashboardMain', - component: AdminDashboardMain, + + +const meta: Meta = { + title: 'Pages/Admin Dashboard', + component: AppRoutes, parameters: { 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', @@ -31,6 +45,8 @@ const meta: Meta = { }, ], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -38,39 +54,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 7eefcc675..ce99902eb 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', apolloClient: { 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..cbdd5c1d9 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-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/CategoryFilter.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/CategoryFilter.stories.tsx index 9f6a6a424..ca7d2a484 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/CategoryFilter.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/CategoryFilter.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/home/components/HeroSection.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/HeroSection.stories.tsx index eb9f4a7ed..9b79fa968 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/HeroSection.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/home/components/HeroSection.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/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 a0d1d6c32..54c5b5c81 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 @@ -9,6 +9,8 @@ const meta: Meta = { parameters: { 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) => ( @@ -28,7 +30,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 a9333e894..c6e8194f6 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 @@ -9,6 +9,7 @@ const meta: Meta = { parameters: { 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) => ( @@ -27,7 +28,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 235ebc29c..bfdde2411 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: { layout: 'fullscreen', apolloClient: { @@ -146,7 +147,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/homepage-unauthenticated.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/homepage-unauthenticated.stories.tsx new file mode 100644 index 000000000..f1427a61a --- /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-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/components/layouts/app/pages/messages/components/ConversationBoxContainer.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/ConversationBoxContainer.stories.tsx index 757e9686f..b6be87df9 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/ConversationBoxContainer.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/components/ConversationBoxContainer.stories.tsx @@ -192,12 +192,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; @@ -211,7 +212,7 @@ export const Default: Story = { mocks: defaultMocks, }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); // ListingBanner shows "{firstName}'s Listing" await expect( @@ -229,7 +230,7 @@ export const SendMessageSuccess: Story = { mocks: sendMessageSuccessMocks, }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const textArea = await canvas.findByPlaceholderText(/Type a message/i); await userEvent.type(textArea, 'Test message'); @@ -247,7 +248,7 @@ export const SendMessageError: Story = { mocks: sendMessageErrorMocks, }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const textArea = await canvas.findByPlaceholderText(/Type a message/i); await userEvent.type(textArea, 'This will fail'); @@ -265,7 +266,7 @@ export const SendMessageNetworkError: Story = { mocks: sendMessageNetworkErrorMocks, }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const textArea = await canvas.findByPlaceholderText(/Type a message/i); await userEvent.type(textArea, 'Network will fail'); @@ -283,7 +284,7 @@ export const CacheUpdateOnSuccess: Story = { mocks: cacheUpdateMocks, }, }, - play: async ({ canvasElement }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); const textArea = await canvas.findByPlaceholderText(/Type a message/i); await userEvent.type(textArea, 'First message'); 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/MessagesPage.stories.tsx b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/MessagesPage.stories.tsx index 082b0f413..7084eaac9 100644 --- a/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/MessagesPage.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/app/pages/messages/pages/MessagesPage.stories.tsx @@ -1,9 +1,10 @@ -import type { Meta, StoryFn } from '@storybook/react'; +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,212 @@ const meta: Meta = { }; export default meta; +type Story = StoryObj; -const Template: StoryFn = () => ; - -export const DefaultView: StoryFn = Template.bind({}); - -DefaultView.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await expect(canvas.getByRole('main')).toBeInTheDocument(); -}; - -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 5b37e25d0..e78217247 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, @@ -169,7 +171,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Check for loading state const loadingSpinner = 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 d883ebfaf..711d1ac8a 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: { layout: 'fullscreen', @@ -95,7 +97,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, + }, }, }, }, @@ -107,7 +115,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 86943a961..3f3d78a58 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 = { @@ -59,19 +58,6 @@ const meta: Meta = { component: RequestsTableContainer, parameters: { layout: 'fullscreen', - }, - decorators: [withMockApolloClient, withMockRouter(), withMockUserId()], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - currentPage: 1, - onPageChange: fn(), - }, - parameters: { apolloClient: { mocks: [ { @@ -95,19 +81,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, @@ -130,7 +110,7 @@ export const Empty: Story = { }, result: { data: { - myListingsRequests: { items: [], total: 0 }, + myListingsRequests: { items: [], total: 0, page: 1, pageSize: 6 }, }, }, }, @@ -174,7 +154,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); @@ -247,6 +227,8 @@ export const WithSearchFilter: Story = { myListingsRequests: { items: [mockRequests.items[0]], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -297,6 +279,8 @@ export const WithStatusFilter: Story = { myListingsRequests: { items: [mockRequests.items[1]], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -389,6 +373,8 @@ export const Pagination: Story = { myListingsRequests: { items: [], total: 12, + page: 2, + pageSize: 6, }, }, }, @@ -487,7 +473,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: { @@ -504,13 +492,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', @@ -522,6 +512,8 @@ export const DataMappingEdgeCases: Story = { }, ], total: 3, + page: 1, + pageSize: 6, }, }, }, @@ -540,10 +532,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); }, @@ -577,13 +571,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', @@ -595,6 +591,8 @@ export const DateFormatting: Story = { }, ], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -614,11 +612,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); }, }; @@ -650,13 +646,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', @@ -668,6 +666,8 @@ export const StateFilteringInteraction: Story = { }, ], total: 1, + page: 1, + pageSize: 6, }, }, }, @@ -681,14 +681,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); }, }; @@ -720,7 +722,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: { @@ -740,7 +744,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: { @@ -758,6 +764,8 @@ export const SortingInteraction: Story = { }, ], total: 2, + page: 1, + pageSize: 6, }, }, }, @@ -771,15 +779,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..987a4206e 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,6 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; -import { MyListingsMain } from './my-listings.tsx'; import { withMockApolloClient, withMockRouter, @@ -9,6 +8,7 @@ import { HomeAllListingsTableContainerMyListingsAllDocument, HomeRequestsTableContainerMyListingsRequestsDocument, } from '../../../../../../generated.tsx'; +import { AppRoutes } from '../../../index.tsx'; const mockListings = { __typename: 'MyListingsAllResult', @@ -47,9 +47,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: { @@ -85,7 +85,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { @@ -136,13 +136,14 @@ export const EmptyListings: Story = { ], }, }, - 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 f16ce29a4..f6fa00171 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', profile: { @@ -48,7 +48,7 @@ const mockActiveReservations = [ reserver: { __typename: 'PersonalUser', id: 'user-1', - userType: 'personal', + userType: 'personal-user', profile: { firstName: 'John', lastName: 'Doe' }, account: { __typename: 'PersonalUserAccount', @@ -64,6 +64,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: { layout: 'fullscreen', @@ -192,7 +194,7 @@ export const Loading: Story = { ], }, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); const loadingSpinner = canvas.queryByRole('progressbar') ?? canvas.queryByText(/loading/i); @@ -552,7 +554,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 06129119c..c4044ea74 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 @@ -64,6 +64,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: { layout: 'fullscreen', @@ -81,7 +83,8 @@ const meta: Meta = { }, { request: { - query: HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, + query: + HomeMyReservationsReservationsViewHistoryContainerPastReservationsDocument, variables: { userId: 'user-1' }, }, result: { @@ -93,7 +96,10 @@ const meta: Meta = { ], }, }, - decorators: [withMockApolloClient, withMockRouter('/my-reservations/history')], + decorators: [ + withMockApolloClient, + withMockRouter('/my-reservations/history'), + ], }; export default meta; @@ -121,7 +127,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 6cdb9c13b..83eb2bec7 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-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 @@ -17,35 +34,29 @@ const defaultMocks = [ request: { query: ViewListingCurrentUserDocument }, result: { data: { - currentPersonalUserAndCreateIfNotExists: { + currentUser: { __typename: 'PersonalUser', id: STORYBOOK_RESERVATION_USER_ID, + userType: 'personal-user', }, }, }, }, + 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 c38ab9668..0f73796e0 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: { layout: 'padded', 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 6bc099907..40a2ce1ab 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: { layout: 'padded', 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 1c88051df..74a6d9596 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 @@ -37,11 +37,13 @@ const mockListing = { const mockCurrentUser = { __typename: 'PersonalUser', id: 'user-2', + userType: 'personal-user', }; 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', apolloClient: { @@ -151,7 +153,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/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..6cec84093 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 @@ -88,7 +88,7 @@ const mocks = [ ]; const meta: Meta = { - title: 'Home/ViewListing', + title: 'Components/Home/ViewListing', component: ViewListing, parameters: { layout: 'fullscreen', 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 018a80e6e..47429433c 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-helpers.ts'; const mockListing = { __typename: 'ItemListing', @@ -32,16 +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: { @@ -70,7 +78,7 @@ const meta: Meta = { { request: { query: ViewListingActiveReservationRequestForListingDocument, - variables: { listingId: '1', reserverId: 'user-2' }, + variables: { listingId: '1', reserverId: 'user-1' }, }, result: { data: { @@ -78,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', + }, + }, + }, + }, + }, + }, + }, ], }, }, @@ -85,7 +137,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { @@ -97,6 +149,17 @@ export const Loading: Story = { parameters: { apolloClient: { mocks: [ + userIsAdminMockRequest('user-1', false), + { + request: { + query: ViewListingCurrentUserDocument, + }, + result: { + data: { + currentUser: mockCurrentUser, + }, + }, + }, { request: { query: ViewListingDocument, @@ -104,6 +167,17 @@ export const Loading: Story = { }, delay: Infinity, }, + { + request: { + query: ViewListingActiveReservationRequestForListingDocument, + variables: { listingId: '1', reserverId: 'user-1' }, + }, + result: { + data: { + myActiveReservationForListing: null, + }, + }, + }, ], }, }, 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 143da373b..ad5ba6c98 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,15 +1,12 @@ 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'; -import { vi } from 'vitest'; const meta: Meta = { title: 'Shared/RequireAuthAdmin', @@ -35,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

@@ -61,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 @@ -75,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 @@ -88,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 @@ -127,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 }) => { @@ -155,6 +169,11 @@ export const UnauthenticatedWithError: Story = { args: { children: , }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -183,6 +202,11 @@ export const UnauthenticatedLoading: Story = { args: { children: , }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -215,6 +239,11 @@ export const UnauthenticatedNoForceLogin: Story = { children: , forceLogin: false, }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { @@ -244,15 +273,18 @@ export const ForceLoginTriggersSigninRedirect: Story = { children: , forceLogin: true, }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { - const signinRedirect = vi.fn(); const mockAuth = createMockAuth({ isAuthenticated: false, isLoading: false, user: undefined, - signinRedirect, }); return ( @@ -274,31 +306,35 @@ export const AccessTokenExpiringTriggersSilent: Story = { args: { children: , }, + parameters: { + apolloClient: { + mocks: [UseUserIsAdminMock], + }, + }, decorators: [ withMockApolloClient, (Story) => { - const addAccessTokenExpiring = vi.fn((callback) => { + const addAccessTokenExpiring = (callback: () => void) => { // Immediately call the callback to simulate token expiring setTimeout(() => callback(), 100); - return vi.fn(); // Return unsubscribe function - }); - const signinSilent = vi.fn(); + }; + const user = { + profile: { sub: 'admin-1', iss: '', aud: '', exp: 0, iat: 0 }, + access_token: `mock-token-${Date.now()}`, + token_type: 'Bearer', + session_state: `mock-session-${Date.now()}`, + state: `mock-state-${Date.now()}`, + expired: false, + expires_in: 3600, + scopes: [], + toStorageString: () => '', + }; const mockAuth = createMockAuth({ isAuthenticated: true, isLoading: false, - user: { - profile: { sub: 'admin-1', iss: '', aud: '', exp: 0, iat: 0 }, - access_token: `mock-token-${Date.now()}`, - token_type: 'Bearer', - session_state: `mock-session-${Date.now()}`, - state: `mock-state-${Date.now()}`, - expired: false, - expires_in: 3600, - scopes: [], - toStorageString: () => '', - }, + user: user, events: { addAccessTokenExpiring } as any, - signinSilent, + signinSilent: () => Promise.resolve(user), }); return ( @@ -323,6 +359,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/test-utils/storybook-decorators.stories.tsx b/apps/ui-sharethrift/src/test-utils/storybook-decorators.stories.tsx new file mode 100644 index 000000000..6936e857d --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/storybook-decorators.stories.tsx @@ -0,0 +1,681 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, waitFor } from 'storybook/test'; +import { gql } from '@apollo/client'; +import { useQuery } from '@apollo/client/react'; +import { 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-helpers.ts b/apps/ui-sharethrift/src/test-utils/storybook-helpers.ts new file mode 100644 index 000000000..ab4c1f6ec --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/storybook-helpers.ts @@ -0,0 +1,24 @@ +import { 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, + }, + }, + }, + }; +}; 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/vitest.config.ts b/apps/ui-sharethrift/vitest.config.ts index f0c4b5905..c912725c9 100644 --- a/apps/ui-sharethrift/vitest.config.ts +++ b/apps/ui-sharethrift/vitest.config.ts @@ -14,14 +14,14 @@ export default defineConfig( additionalCoverageExclude: [ '**/index.ts', '**/index.tsx', - '**/Index.tsx', + '**/Index.tsx', 'src/main.tsx', - 'src/test-utils/**', - 'src/config/**', - 'src/test/**', + 'src/test-utils/**/*.stories.*', + 'src/config/**', + 'src/test/**', '**/*.d.ts', 'src/generated/**', - 'eslint.config.js' + 'eslint.config.js', ], }), ); diff --git a/packages/sthrift/graphql/package.json b/packages/sthrift/graphql/package.json index 7d0964cde..700fdd4d8 100644 --- a/packages/sthrift/graphql/package.json +++ b/packages/sthrift/graphql/package.json @@ -24,7 +24,7 @@ "clean": "rimraf dist" }, "dependencies": { - "@apollo/server": "^5.2.0", + "@apollo/server": "^5.4.0", "@apollo/utils.withrequired": "^3.0.0", "@as-integrations/azure-functions": "^0.2.0", "@azure/functions": "4.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b00860085..815734c5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -848,14 +848,14 @@ importers: packages/sthrift/graphql: dependencies: '@apollo/server': - specifier: ^5.2.0 - version: 5.2.0(graphql@16.12.0) + specifier: ^5.4.0 + version: 5.4.0(graphql@16.12.0) '@apollo/utils.withrequired': specifier: ^3.0.0 version: 3.0.0 '@as-integrations/azure-functions': specifier: ^0.2.0 - version: 0.2.3(@apollo/server@5.2.0(graphql@16.12.0)) + version: 0.2.3(@apollo/server@5.4.0(graphql@16.12.0)) '@azure/functions': specifier: 4.8.0 version: 4.8.0 @@ -1553,8 +1553,8 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/server@5.2.0': - resolution: {integrity: sha512-OEAl5bwVitkvVkmZlgWksSnQ10FUr6q2qJMdkexs83lsvOGmd/y81X5LoETmKZux8UiQsy/A/xzP00b8hTHH/w==} + '@apollo/server@5.4.0': + resolution: {integrity: sha512-E0/2C5Rqp7bWCjaDh4NzYuEPDZ+dltTf2c0FI6GCKJA6GBetVferX3h1//1rS4+NxD36wrJsGGJK+xyT/M3ysg==} engines: {node: '>=20'} peerDependencies: graphql: ^16.11.0 @@ -12177,7 +12177,7 @@ snapshots: '@apollo/utils.logger': 3.0.0 graphql: 16.12.0 - '@apollo/server@5.2.0(graphql@16.12.0)': + '@apollo/server@5.4.0(graphql@16.12.0)': dependencies: '@apollo/cache-control-types': 1.0.3(graphql@16.12.0) '@apollo/server-gateway-interface': 2.0.0(graphql@16.12.0) @@ -12192,6 +12192,7 @@ snapshots: '@graphql-tools/schema': 10.0.31(graphql@16.12.0) async-retry: 1.3.3 body-parser: 2.2.2 + content-type: 1.0.5 cors: 2.8.5 finalhandler: 2.1.1 graphql: 16.12.0 @@ -12272,9 +12273,9 @@ snapshots: transitivePeerDependencies: - encoding - '@as-integrations/azure-functions@0.2.3(@apollo/server@5.2.0(graphql@16.12.0))': + '@as-integrations/azure-functions@0.2.3(@apollo/server@5.4.0(graphql@16.12.0))': dependencies: - '@apollo/server': 5.2.0(graphql@16.12.0) + '@apollo/server': 5.4.0(graphql@16.12.0) '@azure/functions': 3.5.1 '@azure/functions-v4': '@azure/functions@4.8.0'