From f238ee9ceabdc130d04b0bbbfb93d9a5d1212052 Mon Sep 17 00:00:00 2001 From: Lian Date: Wed, 17 Dec 2025 15:42:39 -0500 Subject: [PATCH 01/20] implement confirmation button and logic for canceling reservation requests --- .../listing-information.container.graphql | 12 ++ .../listing-information.container.tsx | 34 ++++ .../listing-information.stories.tsx | 17 +- .../listing-information.tsx | 40 ++-- .../components/reservation-actions.tsx | 25 ++- .../reservation-request/cancel.test.ts | 186 ++++++++++++++++++ .../reservation-request/cancel.ts | 31 +++ .../features/cancel.feature | 23 +++ .../reservation-request/index.ts | 108 +++++++--- .../reservation-request.resolvers.ts | 23 +++ 10 files changed, 442 insertions(+), 57 deletions(-) create mode 100644 packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts create mode 100644 packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts create mode 100644 packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.graphql b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.graphql index e35cc1c6a..7a3b37744 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.graphql +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.graphql @@ -42,3 +42,15 @@ mutation HomeListingInformationCreateReservationRequest( updatedAt } } + +mutation HomeListingInformationCancelReservationRequest( + $input: CancelReservationInput! +) { + cancelReservation(input: $input) { + id + state + updatedAt + closeRequestedBySharer + closeRequestedByReserver + } +} diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx index b3486c6b4..566ef0ae3 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx @@ -5,6 +5,7 @@ import { ListingInformation } from './listing-information.tsx'; import { HomeListingInformationCreateReservationRequestDocument, + HomeListingInformationCancelReservationRequestDocument, type CreateReservationRequestInput, ViewListingCurrentUserDocument, type ViewListingCurrentUserQuery, @@ -86,6 +87,19 @@ export const ListingInformationContainer: React.FC< }, }); + const [cancelReservationRequestMutation, { loading: cancelLoading }] = + useMutation(HomeListingInformationCancelReservationRequestDocument, { + onCompleted: () => { + message.success('Reservation request cancelled successfully'); + client.refetchQueries({ + include: [ViewListingActiveReservationRequestForListingDocument], + }); + }, + onError: (error) => { + message.error(error.message || 'Failed to cancel reservation request'); + }, + }); + const handleReserveClick = async () => { if (!reservationDates.startDate || !reservationDates.endDate) { message.warning( @@ -108,6 +122,24 @@ export const ListingInformationContainer: React.FC< } }; + const handleCancelClick = async () => { + if (!userReservationRequest?.id) { + message.error('No reservation request to cancel'); + return; + } + try { + await cancelReservationRequestMutation({ + variables: { + input: { + id: userReservationRequest.id, + }, + }, + }); + } catch (error) { + console.error('Error cancelling reservation request:', error); + } + }; + return ( setTimeout(resolve, 100)); + const confirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-primary', + ); + if (confirmButton) { + await userEvent.click(confirmButton as HTMLElement); + expect(args.onCancelClick).toHaveBeenCalled(); + } } }, }; @@ -275,7 +283,9 @@ export const ClickLoginToReserve: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - const loginButton = canvas.queryByRole('button', { name: /Log in to Reserve/i }); + const loginButton = canvas.queryByRole('button', { + name: /Log in to Reserve/i, + }); if (loginButton) { await userEvent.click(loginButton); } @@ -310,4 +320,3 @@ export const ClearDateSelection: Story = { await expect(canvasElement).toBeTruthy(); }, }; - diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx index 1895b30b0..5fa7d647e 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx @@ -1,4 +1,4 @@ -import { Row, Col, DatePicker, Button } from 'antd'; +import { Row, Col, DatePicker, Button, Popconfirm } from 'antd'; import dayjs from 'dayjs'; import type { Dayjs } from 'dayjs'; import type { @@ -47,6 +47,7 @@ interface ListingInformationProps { endDate: Date | null; }) => void; reservationLoading?: boolean; + cancelLoading?: boolean; otherReservationsLoading?: boolean; otherReservationsError?: Error; otherReservations?: ViewListingQueryActiveByListingIdQuery['queryActiveByListingId']; @@ -63,6 +64,7 @@ export const ListingInformation: React.FC = ({ reservationDates, onReservationDatesChange, reservationLoading = false, + cancelLoading = false, otherReservationsLoading = false, otherReservationsError, otherReservations, @@ -309,28 +311,30 @@ export const ListingInformation: React.FC = ({ {(() => { if (!userIsSharer && isAuthenticated) { + if (reservationRequestStatus === 'Requested') { + return ( + + + + ); + } return ( ); } diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx index 094c17bc4..15bc29795 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { Space } from 'antd'; +import { Space, Popconfirm } from 'antd'; import { ReservationActionButton } from './reservation-action-button.tsx'; import type { ReservationActionStatus } from '../utils/reservation-status.utils.ts'; @@ -24,12 +24,22 @@ export const ReservationActions: React.FC = ({ switch (status) { case 'REQUESTED': return [ - , + + + + + , = ({ return {actions}; }; - diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts new file mode 100644 index 000000000..07654a57f --- /dev/null +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -0,0 +1,186 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; +import type { DataSources } from '@sthrift/persistence'; +import { expect, vi } from 'vitest'; +import { cancel, type ReservationRequestCancelCommand } from './cancel.ts'; + +const test = { for: describeFeature }; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const feature = await loadFeature( + path.resolve(__dirname, 'features/cancel.feature'), +); + +test.for(feature, ({ Scenario, BeforeEachScenario }) => { + let mockDataSources: DataSources; + let command: ReservationRequestCancelCommand; + // biome-ignore lint/suspicious/noExplicitAny: Test mock variable + let result: any; + // biome-ignore lint/suspicious/noExplicitAny: Test mock variable + let error: any; + + BeforeEachScenario(() => { + mockDataSources = { + domainDataSource: { + ReservationRequest: { + ReservationRequest: { + ReservationRequestUnitOfWork: { + withScopedTransaction: vi.fn(async (callback) => { + const mockRepo = { + getById: vi.fn(), + save: vi.fn(), + }; + await callback(mockRepo); + }), + }, + }, + }, + }, + // biome-ignore lint/suspicious/noExplicitAny: Test mock type assertion + } as any; + + command = { id: 'reservation-123' }; + result = undefined; + error = undefined; + }); + + Scenario( + 'Successfully cancelling a requested reservation', + ({ Given, And, When, Then }) => { + Given('a valid reservation request ID "reservation-123"', () => { + command = { id: 'reservation-123' }; + }); + + And('the reservation request exists and is in requested state', () => { + const mockReservationRequest = { + id: 'reservation-123', + state: 'Requested', + }; + + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + mockDataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(mockReservationRequest), + save: vi + .fn() + .mockResolvedValue({ + ...mockReservationRequest, + state: 'Cancelled', + }), + }; + await callback(mockRepo); + }, + ); + }); + + When('the cancel command is executed', async () => { + const cancelFn = cancel(mockDataSources); + try { + result = await cancelFn(command); + } catch (err) { + error = err; + } + }); + + Then('the reservation request should be cancelled', () => { + expect(error).toBeUndefined(); + expect(result).toBeDefined(); + expect(result.state).toBe('Cancelled'); + }); + }, + ); + + Scenario( + 'Attempting to cancel a non-existent reservation request', + ({ Given, And, When, Then }) => { + Given('a reservation request ID "reservation-999"', () => { + command = { id: 'reservation-999' }; + }); + + And('the reservation request does not exist', () => { + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + mockDataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(undefined), + save: vi.fn(), + }; + await callback(mockRepo); + }, + ); + }); + + When('the cancel command is executed', async () => { + const cancelFn = cancel(mockDataSources); + try { + result = await cancelFn(command); + } catch (err) { + error = err; + } + }); + + Then('an error "Reservation request not found" should be thrown', () => { + expect(error).toBeDefined(); + expect(error.message).toBe('Reservation request not found'); + }); + }, + ); + + Scenario( + 'Cancel fails when save returns undefined', + ({ Given, And, When, Then }) => { + Given('a valid reservation request ID "reservation-456"', () => { + command = { id: 'reservation-456' }; + }); + + And('the reservation request exists', () => { + // Reservation request exists check + }); + + And('save returns undefined', () => { + const mockReservationRequest = { + id: 'reservation-456', + state: 'Requested', + }; + + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + mockDataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(mockReservationRequest), + save: vi.fn().mockResolvedValue(undefined), + }; + await callback(mockRepo); + }, + ); + }); + + When('the cancel command is executed', async () => { + const cancelFn = cancel(mockDataSources); + try { + result = await cancelFn(command); + } catch (err) { + error = err; + } + }); + + Then( + 'an error "Reservation request not cancelled" should be thrown', + () => { + expect(error).toBeDefined(); + expect(error.message).toBe('Reservation request not cancelled'); + }, + ); + }, + ); +}); diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts new file mode 100644 index 000000000..cb7368725 --- /dev/null +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts @@ -0,0 +1,31 @@ +import type { Domain } from '@sthrift/domain'; +import type { DataSources } from '@sthrift/persistence'; + +export interface ReservationRequestCancelCommand { + id: string; +} + +export const cancel = (dataSources: DataSources) => { + return async ( + command: ReservationRequestCancelCommand, + ): Promise => { + let reservationRequestToReturn: + | Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference + | undefined; + await dataSources.domainDataSource.ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction( + async (repo) => { + const reservationRequest = await repo.getById(command.id); + if (!reservationRequest) { + throw new Error('Reservation request not found'); + } + + reservationRequest.state = 'Cancelled'; + reservationRequestToReturn = await repo.save(reservationRequest); + }, + ); + if (!reservationRequestToReturn) { + throw new Error('Reservation request not cancelled'); + } + return reservationRequestToReturn; + }; +}; diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature new file mode 100644 index 000000000..206975da0 --- /dev/null +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature @@ -0,0 +1,23 @@ +Feature: Cancel Reservation Request + As a reserver + I want to cancel my reservation request + So that I can withdraw my request before it is accepted + + Scenario: Successfully cancelling a requested reservation + Given a valid reservation request ID "reservation-123" + And the reservation request exists and is in requested state + When the cancel command is executed + Then the reservation request should be cancelled + + Scenario: Attempting to cancel a non-existent reservation request + Given a reservation request ID "reservation-999" + And the reservation request does not exist + When the cancel command is executed + Then an error "Reservation request not found" should be thrown + + Scenario: Cancel fails when save returns undefined + Given a valid reservation request ID "reservation-456" + And the reservation request exists + And save returns undefined + When the cancel command is executed + Then an error "Reservation request not cancelled" should be thrown diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts index e459f01a2..a91d1d37a 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts @@ -1,36 +1,90 @@ import type { Domain } from '@sthrift/domain'; import type { DataSources } from '@sthrift/persistence'; -import { type ReservationRequestQueryActiveByReserverIdCommand, queryActiveByReserverId } from './query-active-by-reserver-id.ts'; -import { type ReservationRequestQueryPastByReserverIdCommand, queryPastByReserverId } from './query-past-by-reserver-id.ts'; -import { type ReservationRequestQueryActiveByReserverIdAndListingIdCommand, queryActiveByReserverIdAndListingId } from './query-active-by-reserver-id-and-listing-id.ts'; -import { type ReservationRequestQueryByIdCommand, queryById } from './query-by-id.ts'; +import { + type ReservationRequestQueryActiveByReserverIdCommand, + queryActiveByReserverId, +} from './query-active-by-reserver-id.ts'; +import { + type ReservationRequestQueryPastByReserverIdCommand, + queryPastByReserverId, +} from './query-past-by-reserver-id.ts'; +import { + type ReservationRequestQueryActiveByReserverIdAndListingIdCommand, + queryActiveByReserverIdAndListingId, +} from './query-active-by-reserver-id-and-listing-id.ts'; +import { + type ReservationRequestQueryByIdCommand, + queryById, +} from './query-by-id.ts'; import { type ReservationRequestCreateCommand, create } from './create.ts'; -import { type ReservationRequestQueryOverlapByListingIdAndReservationPeriodCommand, queryOverlapByListingIdAndReservationPeriod } from './query-overlap-by-listing-id-and-reservation-period.ts'; -import { type ReservationRequestQueryActiveByListingIdCommand, queryActiveByListingId } from './query-active-by-listing-id.ts'; -import { type ReservationRequestQueryListingRequestsBySharerIdCommand, queryListingRequestsBySharerId } from './query-listing-requests-by-sharer-id.ts'; +import { type ReservationRequestCancelCommand, cancel } from './cancel.ts'; +import { + type ReservationRequestQueryOverlapByListingIdAndReservationPeriodCommand, + queryOverlapByListingIdAndReservationPeriod, +} from './query-overlap-by-listing-id-and-reservation-period.ts'; +import { + type ReservationRequestQueryActiveByListingIdCommand, + queryActiveByListingId, +} from './query-active-by-listing-id.ts'; +import { + type ReservationRequestQueryListingRequestsBySharerIdCommand, + queryListingRequestsBySharerId, +} from './query-listing-requests-by-sharer-id.ts'; export interface ReservationRequestApplicationService { - queryById: (command: ReservationRequestQueryByIdCommand) => Promise, - queryActiveByReserverId: (command: ReservationRequestQueryActiveByReserverIdCommand) => Promise, - queryPastByReserverId: (command: ReservationRequestQueryPastByReserverIdCommand) => Promise, - queryActiveByReserverIdAndListingId: (command: ReservationRequestQueryActiveByReserverIdAndListingIdCommand) => Promise, - queryOverlapByListingIdAndReservationPeriod: (command: ReservationRequestQueryOverlapByListingIdAndReservationPeriodCommand) => Promise, - queryActiveByListingId: (command: ReservationRequestQueryActiveByListingIdCommand) => Promise, - queryListingRequestsBySharerId: (command: ReservationRequestQueryListingRequestsBySharerIdCommand) => Promise, - create: (command: ReservationRequestCreateCommand) => Promise, + queryById: ( + command: ReservationRequestQueryByIdCommand, + ) => Promise; + queryActiveByReserverId: ( + command: ReservationRequestQueryActiveByReserverIdCommand, + ) => Promise< + Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference[] + >; + queryPastByReserverId: ( + command: ReservationRequestQueryPastByReserverIdCommand, + ) => Promise< + Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference[] + >; + queryActiveByReserverIdAndListingId: ( + command: ReservationRequestQueryActiveByReserverIdAndListingIdCommand, + ) => Promise; + queryOverlapByListingIdAndReservationPeriod: ( + command: ReservationRequestQueryOverlapByListingIdAndReservationPeriodCommand, + ) => Promise< + Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference[] + >; + queryActiveByListingId: ( + command: ReservationRequestQueryActiveByListingIdCommand, + ) => Promise< + Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference[] + >; + queryListingRequestsBySharerId: ( + command: ReservationRequestQueryListingRequestsBySharerIdCommand, + ) => Promise< + Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference[] + >; + create: ( + command: ReservationRequestCreateCommand, + ) => Promise; + cancel: ( + command: ReservationRequestCancelCommand, + ) => Promise; } export const ReservationRequest = ( - dataSources: DataSources + dataSources: DataSources, ): ReservationRequestApplicationService => { - return { - queryById: queryById(dataSources), - queryActiveByReserverId: queryActiveByReserverId(dataSources), - queryPastByReserverId: queryPastByReserverId(dataSources), - queryActiveByReserverIdAndListingId: queryActiveByReserverIdAndListingId(dataSources), - queryOverlapByListingIdAndReservationPeriod: queryOverlapByListingIdAndReservationPeriod(dataSources), - queryActiveByListingId: queryActiveByListingId(dataSources), - queryListingRequestsBySharerId: queryListingRequestsBySharerId(dataSources), - create: create(dataSources), - } -} \ No newline at end of file + return { + queryById: queryById(dataSources), + queryActiveByReserverId: queryActiveByReserverId(dataSources), + queryPastByReserverId: queryPastByReserverId(dataSources), + queryActiveByReserverIdAndListingId: + queryActiveByReserverIdAndListingId(dataSources), + queryOverlapByListingIdAndReservationPeriod: + queryOverlapByListingIdAndReservationPeriod(dataSources), + queryActiveByListingId: queryActiveByListingId(dataSources), + queryListingRequestsBySharerId: queryListingRequestsBySharerId(dataSources), + create: create(dataSources), + cancel: cancel(dataSources), + }; +}; diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index cdd60676d..de16ca9de 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -219,6 +219,29 @@ const reservationRequest: Resolvers = { }, ); }, + cancelReservation: async ( + _parent: unknown, + args: { + input: { + id: string; + }; + }, + context: GraphContext, + _info: GraphQLResolveInfo, + ) => { + const verifiedJwt = context.applicationServices.verifiedUser?.verifiedJwt; + if (!verifiedJwt) { + throw new Error( + 'User must be authenticated to cancel a reservation request', + ); + } + + return await context.applicationServices.ReservationRequest.ReservationRequest.cancel( + { + id: args.input.id, + }, + ); + }, }, }; From a412aec46aca7fba8226007ed2c0ad06cae4ff47 Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 10:25:11 -0500 Subject: [PATCH 02/20] added additional test coverage and verify only reserver can cancel reservation request --- .../listing-information.container.stories.tsx | 242 ++++++++++- .../listing-information.stories.tsx | 130 ++++++ .../components/reservation-actions.tsx | 10 +- .../stories/reservation-actions.stories.tsx | 401 +++++++++++++----- .../reservation-request/cancel.test.ts | 68 ++- .../reservation-request/cancel.ts | 8 + .../features/cancel.feature | 6 + .../reservation-request.resolvers.feature | 12 +- .../reservation-request.resolvers.test.ts | 83 ++++ .../reservation-request.resolvers.ts | 1 + 10 files changed, 837 insertions(+), 124 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx index 8ede5f57e..f5d714e58 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect } from 'storybook/test'; +import { expect, userEvent, within } from 'storybook/test'; import { ListingInformationContainer } from './listing-information.container.tsx'; import { withMockApolloClient, @@ -9,6 +9,8 @@ import { ViewListingCurrentUserDocument, ViewListingQueryActiveByListingIdDocument, HomeListingInformationCreateReservationRequestDocument, + HomeListingInformationCancelReservationRequestDocument, + ViewListingActiveReservationRequestForListingDocument, } from '../../../../../../generated.tsx'; const mockListing = { @@ -150,3 +152,241 @@ export const WithExistingReservation: Story = { await expect(canvasElement).toBeTruthy(); }, }; + +export const CancelReservationSuccess: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-cancel-1', + state: 'Requested' as const, + reservationPeriodStart: '2025-02-01', + reservationPeriodEnd: '2025-02-10', + }, + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: ViewListingCurrentUserDocument, + }, + result: { + data: { + currentUser: mockCurrentUser, + }, + }, + }, + { + request: { + query: ViewListingQueryActiveByListingIdDocument, + variables: { listingId: '1' }, + }, + result: { + data: { + queryActiveByListingId: [], + }, + }, + }, + { + request: { + query: HomeListingInformationCancelReservationRequestDocument, + variables: { + input: { + id: 'res-cancel-1', + }, + }, + }, + result: { + data: { + cancelReservation: { + __typename: 'ReservationRequest', + id: 'res-cancel-1', + state: 'Cancelled', + }, + }, + }, + }, + { + request: { + query: ViewListingActiveReservationRequestForListingDocument, + }, + result: { + data: { + myActiveReservationForListing: null, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for cancel button to appear + await new Promise((resolve) => setTimeout(resolve, 500)); + + const cancelButton = canvas.queryByRole('button', { name: /Cancel/i }); + if (cancelButton) { + await userEvent.click(cancelButton); + + // Wait for Popconfirm to render + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Find and click the confirm button in the Popconfirm + const confirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-primary', + ); + if (confirmButton) { + await userEvent.click(confirmButton as HTMLElement); + await new Promise((resolve) => setTimeout(resolve, 300)); + } + } + }, +}; + +export const CancelReservationError: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-cancel-error', + state: 'Requested' as const, + reservationPeriodStart: '2025-02-01', + reservationPeriodEnd: '2025-02-10', + }, + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: ViewListingCurrentUserDocument, + }, + result: { + data: { + currentUser: mockCurrentUser, + }, + }, + }, + { + request: { + query: ViewListingQueryActiveByListingIdDocument, + variables: { listingId: '1' }, + }, + result: { + data: { + queryActiveByListingId: [], + }, + }, + }, + { + request: { + query: HomeListingInformationCancelReservationRequestDocument, + variables: { + input: { + id: 'res-cancel-error', + }, + }, + }, + error: new Error( + 'Only the reserver can cancel their reservation request', + ), + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for cancel button to appear + await new Promise((resolve) => setTimeout(resolve, 500)); + + const cancelButton = canvas.queryByRole('button', { name: /Cancel/i }); + if (cancelButton) { + await userEvent.click(cancelButton); + + // Wait for Popconfirm + await new Promise((resolve) => setTimeout(resolve, 200)); + + const confirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-primary', + ); + if (confirmButton) { + await userEvent.click(confirmButton as HTMLElement); + await new Promise((resolve) => setTimeout(resolve, 300)); + } + } + }, +}; + +export const CancelReservationLoading: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-cancel-loading', + state: 'Requested' as const, + reservationPeriodStart: '2025-02-01', + reservationPeriodEnd: '2025-02-10', + }, + }, + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: ViewListingCurrentUserDocument, + }, + result: { + data: { + currentUser: mockCurrentUser, + }, + }, + }, + { + request: { + query: ViewListingQueryActiveByListingIdDocument, + variables: { listingId: '1' }, + }, + result: { + data: { + queryActiveByListingId: [], + }, + }, + }, + { + request: { + query: HomeListingInformationCancelReservationRequestDocument, + variables: { + input: { + id: 'res-cancel-loading', + }, + }, + }, + delay: 3000, // Simulate slow response + result: { + data: { + cancelReservation: { + __typename: 'ReservationRequest', + id: 'res-cancel-loading', + state: 'Cancelled', + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx index 4831e3564..bfb2eab2a 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx @@ -320,3 +320,133 @@ export const ClearDateSelection: Story = { await expect(canvasElement).toBeTruthy(); }, }; + +export const CancelButtonWithPopconfirm: Story = { + args: { + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-1', + state: 'Requested' as const, + reservationPeriodStart: '1738368000000', + reservationPeriodEnd: '1739145600000', + }, + onCancelClick: fn(), + cancelLoading: false, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Verify cancel button is present + const cancelButton = canvas.queryByRole('button', { + name: /Cancel Request/i, + }); + expect(cancelButton).toBeTruthy(); + + if (cancelButton) { + // Click cancel button to trigger Popconfirm + await userEvent.click(cancelButton); + + // Wait for Popconfirm to appear + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify Popconfirm title appears + const popconfirmTitle = document.querySelector('.ant-popconfirm-title'); + expect(popconfirmTitle?.textContent).toContain( + 'Cancel Reservation Request', + ); + + // Click confirm button + const confirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-primary', + ) as HTMLElement; + if (confirmButton) { + await userEvent.click(confirmButton); + + // Verify onCancelClick was called + expect(args.onCancelClick).toHaveBeenCalled(); + } + } + }, +}; + +export const CancelButtonLoading: Story = { + args: { + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-1', + state: 'Requested' as const, + reservationPeriodStart: '1738368000000', + reservationPeriodEnd: '1739145600000', + }, + cancelLoading: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Verify button is present (loading prop doesn't disable Ant Design Button) + const cancelButton = canvas.queryByRole('button', { + name: /Cancel Request/i, + }); + expect(cancelButton).toBeTruthy(); + }, +}; + +export const NoCancelButtonForAcceptedReservation: Story = { + args: { + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-1', + state: 'Accepted' as const, + reservationPeriodStart: '1738368000000', + reservationPeriodEnd: '1739145600000', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Verify cancel button is NOT present for accepted reservations + const cancelButton = canvas.queryByRole('button', { name: /Cancel/i }); + expect(cancelButton).toBeNull(); + }, +}; + +export const PopconfirmCancelButton: Story = { + args: { + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: 'res-1', + state: 'Requested' as const, + reservationPeriodStart: '1738368000000', + reservationPeriodEnd: '1739145600000', + }, + onCancelClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + const cancelButton = canvas.queryByRole('button', { + name: /Cancel Request/i, + }); + if (cancelButton) { + await userEvent.click(cancelButton); + + // Wait for Popconfirm + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Click the "No" button to cancel the Popconfirm + const cancelPopconfirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', + ) as HTMLElement; + if (cancelPopconfirmButton) { + await userEvent.click(cancelPopconfirmButton); + + // Verify onCancelClick was NOT called + expect(args.onCancelClick).not.toHaveBeenCalled(); + } + } + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx index 15bc29795..1b9e5daad 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx @@ -63,14 +63,8 @@ export const ReservationActions: React.FC = ({ ]; case 'REJECTED': - return [ - , - ]; + // No actions for rejected reservations - they've already been dismissed by the owner + return []; default: // No actions for cancelled or closed reservations diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index f98f2d282..650b24576 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -3,129 +3,320 @@ import { ReservationActions } from '../components/reservation-actions.js'; import { expect, fn, userEvent, within } from 'storybook/test'; const meta: Meta = { - title: 'Molecules/ReservationActions', - component: ReservationActions, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: { - status: { - control: 'select', - options: ['REQUESTED', 'ACCEPTED', 'REJECTED', 'CLOSED', 'CANCELLED'], - }, - cancelLoading: { - control: 'boolean', - }, - closeLoading: { - control: 'boolean', - }, - onCancel: { action: 'cancel clicked' }, - onClose: { action: 'close clicked' }, - onMessage: { action: 'message clicked' }, - }, + title: 'Molecules/ReservationActions', + component: ReservationActions, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + status: { + control: 'select', + options: ['REQUESTED', 'ACCEPTED', 'REJECTED', 'CLOSED', 'CANCELLED'], + }, + cancelLoading: { + control: 'boolean', + }, + closeLoading: { + control: 'boolean', + }, + onCancel: { action: 'cancel clicked' }, + onClose: { action: 'close clicked' }, + onMessage: { action: 'message clicked' }, + }, }; export default meta; type Story = StoryObj; export const Requested: Story = { - args: { - status: 'REQUESTED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify action buttons are present - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - - // Verify buttons are visible - for (const button of buttons) { - expect(button).toBeVisible(); - } - }, + args: { + status: 'REQUESTED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify action buttons are present + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Verify buttons are visible + for (const button of buttons) { + expect(button).toBeVisible(); + } + }, }; export const Accepted: Story = { - args: { - status: 'ACCEPTED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify buttons are rendered for accepted state - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - }, + args: { + status: 'ACCEPTED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify buttons are rendered for accepted state + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + }, }; export const ButtonInteraction: Story = { - args: { - status: 'REQUESTED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - // Get all buttons - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - - // Click the first button (typically cancel or message) - if (buttons[0]) { - await userEvent.click(buttons[0]); - // Verify the callback was called - const callbacks = [args.onCancel, args.onClose, args.onMessage]; - const called = callbacks.some(cb => cb && (cb as any).mock?.calls?.length > 0); - expect(called || true).toBe(true); // Allow pass if callbacks are called - } - }, + args: { + status: 'REQUESTED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + // Get all buttons + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Click the first button (typically cancel or message) + if (buttons[0]) { + await userEvent.click(buttons[0]); + // Verify the callback was called + const callbacks = [args.onCancel, args.onClose, args.onMessage]; + const called = callbacks.some( + (cb) => cb && (cb as any).mock?.calls?.length > 0, + ); + expect(called || true).toBe(true); // Allow pass if callbacks are called + } + }, }; export const Rejected: Story = { - args: { - status: 'REJECTED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, + args: { + status: 'REJECTED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, }; export const Cancelled: Story = { - args: { - status: 'CANCELLED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, + args: { + status: 'CANCELLED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, }; export const LoadingStates: Story = { - args: { - status: 'REQUESTED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - cancelLoading: true, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify loading state is rendered - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - - // Check if any button shows loading state (might be disabled) - const disabledButtons = buttons.filter(b => b.hasAttribute('disabled')); - expect(disabledButtons.length).toBeGreaterThanOrEqual(0); - }, -}; \ No newline at end of file + args: { + status: 'REQUESTED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + cancelLoading: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify loading state is rendered + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Check if any button shows loading state (might be disabled) + const disabledButtons = buttons.filter((b) => b.hasAttribute('disabled')); + expect(disabledButtons.length).toBeGreaterThanOrEqual(0); + }, +}; + +export const RequestedWithPopconfirm: Story = { + args: { + status: 'REQUESTED', + onCancel: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + // Get all buttons + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Find cancel button (first button in REQUESTED state) + const cancelButton = buttons[0]; + if (cancelButton) { + // Click to trigger Popconfirm + await userEvent.click(cancelButton); + + // Wait for Popconfirm + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify Popconfirm appears + const popconfirmTitle = document.querySelector('.ant-popconfirm-title'); + expect(popconfirmTitle?.textContent).toContain( + 'Cancel Reservation Request', + ); + + // Verify description + const popconfirmDesc = document.querySelector( + '.ant-popconfirm-description', + ); + expect(popconfirmDesc?.textContent).toContain('Are you sure'); + + // Click confirm + const confirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-primary', + ) as HTMLElement; + if (confirmButton) { + await userEvent.click(confirmButton); + + // Verify callback was called + expect(args.onCancel).toHaveBeenCalled(); + } + } + }, +}; + +export const PopconfirmCancelAction: Story = { + args: { + status: 'REQUESTED', + onCancel: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const buttons = canvas.getAllByRole('button'); + const cancelButton = buttons[0]; + + if (cancelButton) { + await userEvent.click(cancelButton); + + // Wait for Popconfirm + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Click "No" button to cancel + const cancelPopconfirmButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', + ) as HTMLElement; + if (cancelPopconfirmButton) { + await userEvent.click(cancelPopconfirmButton); + + // Verify onCancel was NOT called + expect(args.onCancel).not.toHaveBeenCalled(); + } + } + }, +}; + +export const RejectedNoActions: Story = { + args: { + status: 'REJECTED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify no buttons are rendered for REJECTED status + const buttons = canvas.queryAllByRole('button'); + expect(buttons.length).toBe(0); + + // Component should return null and render nothing + expect(canvasElement.children.length).toBe(0); + }, +}; + +export const CancelledNoActions: Story = { + args: { + status: 'CANCELLED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify no buttons for cancelled state + const buttons = canvas.queryAllByRole('button'); + expect(buttons.length).toBe(0); + }, +}; + +export const ClosedNoActions: Story = { + args: { + status: 'CLOSED', + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify no buttons for closed state + const buttons = canvas.queryAllByRole('button'); + expect(buttons.length).toBe(0); + }, +}; + +export const AcceptedActions: Story = { + args: { + status: 'ACCEPTED', + onClose: fn(), + onMessage: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify actions are present for accepted status + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Should have Close and Message buttons + expect(buttons.length).toBe(2); + }, +}; + +export const CancelLoadingState: Story = { + args: { + status: 'REQUESTED', + onCancel: fn(), + onMessage: fn(), + cancelLoading: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify loading state renders buttons + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Verify buttons are present (loading prop on ReservationActionButton) + const cancelButton = buttons[0]; + expect(cancelButton).toBeTruthy(); + }, +}; + +export const CloseLoadingState: Story = { + args: { + status: 'ACCEPTED', + onClose: fn(), + onMessage: fn(), + closeLoading: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify loading state renders buttons + const buttons = canvas.getAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + + // Verify buttons are present (loading prop on ReservationActionButton) + const closeButton = buttons[0]; + expect(closeButton).toBeTruthy(); + }, +}; diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts index 07654a57f..b3f2e8234 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -39,7 +39,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { // biome-ignore lint/suspicious/noExplicitAny: Test mock type assertion } as any; - command = { id: 'reservation-123' }; + command = { id: 'reservation-123', callerId: 'user-123' }; result = undefined; error = undefined; }); @@ -48,13 +48,14 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { 'Successfully cancelling a requested reservation', ({ Given, And, When, Then }) => { Given('a valid reservation request ID "reservation-123"', () => { - command = { id: 'reservation-123' }; + command = { id: 'reservation-123', callerId: 'user-123' }; }); And('the reservation request exists and is in requested state', () => { const mockReservationRequest = { id: 'reservation-123', state: 'Requested', + loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), }; ( @@ -65,12 +66,10 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { async (callback: any) => { const mockRepo = { getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi - .fn() - .mockResolvedValue({ - ...mockReservationRequest, - state: 'Cancelled', - }), + save: vi.fn().mockResolvedValue({ + ...mockReservationRequest, + state: 'Cancelled', + }), }; await callback(mockRepo); }, @@ -137,7 +136,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { 'Cancel fails when save returns undefined', ({ Given, And, When, Then }) => { Given('a valid reservation request ID "reservation-456"', () => { - command = { id: 'reservation-456' }; + command = { id: 'reservation-456', callerId: 'user-123' }; }); And('the reservation request exists', () => { @@ -148,6 +147,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { const mockReservationRequest = { id: 'reservation-456', state: 'Requested', + loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), }; ( @@ -183,4 +183,54 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { ); }, ); + + Scenario( + 'Authorization failure when caller is not the reserver', + ({ Given, And, When, Then }) => { + Given('a reservation request ID "reservation-789"', () => { + command = { id: 'reservation-789', callerId: 'user-999' }; + }); + + And('the reservation request belongs to a different user', () => { + const mockReservationRequest = { + id: 'reservation-789', + state: 'Requested', + loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), + }; + + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + mockDataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(mockReservationRequest), + save: vi.fn(), + }; + await callback(mockRepo); + }, + ); + }); + + When('the cancel command is executed', async () => { + const cancelFn = cancel(mockDataSources); + try { + result = await cancelFn(command); + } catch (err) { + error = err; + } + }); + + Then( + 'an error "Only the reserver can cancel their reservation request" should be thrown', + () => { + expect(error).toBeDefined(); + expect(error.message).toBe( + 'Only the reserver can cancel their reservation request', + ); + }, + ); + }, + ); }); diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts index cb7368725..d8f509356 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts @@ -3,6 +3,7 @@ import type { DataSources } from '@sthrift/persistence'; export interface ReservationRequestCancelCommand { id: string; + callerId: string; } export const cancel = (dataSources: DataSources) => { @@ -19,6 +20,13 @@ export const cancel = (dataSources: DataSources) => { throw new Error('Reservation request not found'); } + const reserver = await reservationRequest.loadReserver(); + if (reserver.id !== command.callerId) { + throw new Error( + 'Only the reserver can cancel their reservation request', + ); + } + reservationRequest.state = 'Cancelled'; reservationRequestToReturn = await repo.save(reservationRequest); }, diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature index 206975da0..4e54a1d1c 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature @@ -21,3 +21,9 @@ Feature: Cancel Reservation Request And save returns undefined When the cancel command is executed Then an error "Reservation request not cancelled" should be thrown + + Scenario: Authorization failure when caller is not the reserver + Given a reservation request ID "reservation-789" + And the reservation request belongs to a different user + When the cancel command is executed + Then an error "Only the reserver can cancel their reservation request" should be thrown diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature b/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature index 68ddb228e..970a29f84 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature @@ -141,4 +141,14 @@ So that I can view my reservations and make new ones through the GraphQL API Given multiple listing requests with varying titles And sorter field "title" with order "ascend" When paginateAndFilterListingRequests is called - Then the results should be sorted alphabetically by title \ No newline at end of file + Then the results should be sorted alphabetically by title + + Scenario: Cancel reservation request successfully + Given an authenticated user + When cancelReservation mutation is called + Then the reservation should be cancelled + + Scenario: Cancel reservation without authentication + Given an unauthenticated user + When cancelReservation mutation is called + Then an authentication error should be thrown \ No newline at end of file diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts index 4a20f5c4f..00a625d45 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts @@ -1153,4 +1153,87 @@ test.for(feature, ({ Scenario }) => { }); }, ); + + Scenario( + 'Cancel reservation request successfully', + ({ Given, When, Then }) => { + Given('an authenticated user', () => { + context = { + applicationServices: { + verifiedUser: { + verifiedJwt: { sub: 'user-123' }, + }, + ReservationRequest: { + ReservationRequest: { + cancel: vi.fn(), + }, + }, + }, + } as never; + }); + + When('cancelReservation mutation is called', async () => { + vi.mocked( + context.applicationServices.ReservationRequest.ReservationRequest + .cancel, + ).mockResolvedValue( + createMockReservationRequest({ id: 'res-123', state: 'Cancelled' }), + ); + + const resolver = reservationRequestResolvers.Mutation + ?.cancelReservation as TestResolver<{ + input: { id: string }; + }>; + result = await resolver( + {}, + { input: { id: 'res-123' } }, + context, + {} as never, + ); + }); + + Then('the reservation should be cancelled', () => { + expect( + context.applicationServices.ReservationRequest.ReservationRequest + .cancel, + ).toHaveBeenCalledWith({ id: 'res-123', callerId: 'user-123' }); + expect((result as { state: string }).state).toBe('Cancelled'); + }); + }, + ); + + Scenario( + 'Cancel reservation without authentication', + ({ Given, When, Then }) => { + Given('an unauthenticated user', () => { + context = { + applicationServices: { + verifiedUser: undefined, + }, + } as never; + }); + + When('cancelReservation mutation is called', async () => { + const resolver = reservationRequestResolvers.Mutation + ?.cancelReservation as TestResolver<{ + input: { id: string }; + }>; + try { + await resolver( + {}, + { input: { id: 'res-123' } }, + context, + {} as never, + ); + } catch (err) { + error = err as Error; + } + }); + + Then('an authentication error should be thrown', () => { + expect(error).toBeDefined(); + expect((error as Error).message).toContain('authenticated'); + }); + }, + ); }); diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index de16ca9de..b7fb5d73f 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -239,6 +239,7 @@ const reservationRequest: Resolvers = { return await context.applicationServices.ReservationRequest.ReservationRequest.cancel( { id: args.input.id, + callerId: verifiedJwt.sub, }, ); }, From 3b356d7ed46300bb2085513ac7b68c1e19f57b7b Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 11:53:56 -0500 Subject: [PATCH 03/20] resolved sourcery comment to remove deduplication in stories files --- .../listing-information.container.stories.tsx | 167 ++++++---------- .../listing-information.stories.tsx | 179 ++++++++---------- .../stories/reservation-actions.stories.tsx | 159 ++++++++-------- .../reservation-request/cancel.test.ts | 54 +++++- .../features/cancel.feature | 6 + 5 files changed, 286 insertions(+), 279 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx index f5d714e58..150d31968 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect, userEvent, within } from 'storybook/test'; +import { expect, userEvent, waitFor, within } from 'storybook/test'; import { ListingInformationContainer } from './listing-information.container.tsx'; import { withMockApolloClient, @@ -13,6 +13,62 @@ import { ViewListingActiveReservationRequestForListingDocument, } from '../../../../../../generated.tsx'; +const POPCONFIRM_SELECTORS = { + confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', +} as const; + +const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { + const canvas = within(canvasElement); + + const cancelButton = await waitFor( + () => { + const btn = canvas.queryByRole('button', { name: /Cancel/i }); + if (!btn) throw new Error('Cancel button not found yet'); + return btn; + }, + { timeout: 1000 }, + ); + + await userEvent.click(cancelButton); + + const confirmButton = await waitFor( + () => { + const btn = document.querySelector( + POPCONFIRM_SELECTORS.confirmButton, + ) as HTMLElement | null; + if (!btn) throw new Error('Confirm button not found yet'); + return btn; + }, + { timeout: 1000 }, + ); + + await userEvent.click(confirmButton); +}; + +const buildBaseListingMocks = () => [ + { + request: { + query: ViewListingCurrentUserDocument, + }, + result: { + data: { + currentUser: mockCurrentUser, + }, + }, + }, + { + request: { + query: ViewListingQueryActiveByListingIdDocument, + variables: { listingId: '1' }, + }, + result: { + data: { + queryActiveByListingId: [], + }, + }, + }, +]; + const mockListing = { __typename: 'ItemListing' as const, listingType: 'item-listing' as const, @@ -169,27 +225,7 @@ export const CancelReservationSuccess: Story = { parameters: { apolloClient: { mocks: [ - { - request: { - query: ViewListingCurrentUserDocument, - }, - result: { - data: { - currentUser: mockCurrentUser, - }, - }, - }, - { - request: { - query: ViewListingQueryActiveByListingIdDocument, - variables: { listingId: '1' }, - }, - result: { - data: { - queryActiveByListingId: [], - }, - }, - }, + ...buildBaseListingMocks(), { request: { query: HomeListingInformationCancelReservationRequestDocument, @@ -223,28 +259,8 @@ export const CancelReservationSuccess: Story = { }, }, play: async ({ canvasElement }) => { - const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - - // Wait for cancel button to appear - await new Promise((resolve) => setTimeout(resolve, 500)); - - const cancelButton = canvas.queryByRole('button', { name: /Cancel/i }); - if (cancelButton) { - await userEvent.click(cancelButton); - - // Wait for Popconfirm to render - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Find and click the confirm button in the Popconfirm - const confirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn-primary', - ); - if (confirmButton) { - await userEvent.click(confirmButton as HTMLElement); - await new Promise((resolve) => setTimeout(resolve, 300)); - } - } + await clickCancelThenConfirm(canvasElement); }, }; @@ -264,27 +280,7 @@ export const CancelReservationError: Story = { parameters: { apolloClient: { mocks: [ - { - request: { - query: ViewListingCurrentUserDocument, - }, - result: { - data: { - currentUser: mockCurrentUser, - }, - }, - }, - { - request: { - query: ViewListingQueryActiveByListingIdDocument, - variables: { listingId: '1' }, - }, - result: { - data: { - queryActiveByListingId: [], - }, - }, - }, + ...buildBaseListingMocks(), { request: { query: HomeListingInformationCancelReservationRequestDocument, @@ -302,27 +298,8 @@ export const CancelReservationError: Story = { }, }, play: async ({ canvasElement }) => { - const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - - // Wait for cancel button to appear - await new Promise((resolve) => setTimeout(resolve, 500)); - - const cancelButton = canvas.queryByRole('button', { name: /Cancel/i }); - if (cancelButton) { - await userEvent.click(cancelButton); - - // Wait for Popconfirm - await new Promise((resolve) => setTimeout(resolve, 200)); - - const confirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn-primary', - ); - if (confirmButton) { - await userEvent.click(confirmButton as HTMLElement); - await new Promise((resolve) => setTimeout(resolve, 300)); - } - } + await clickCancelThenConfirm(canvasElement); }, }; @@ -342,27 +319,7 @@ export const CancelReservationLoading: Story = { parameters: { apolloClient: { mocks: [ - { - request: { - query: ViewListingCurrentUserDocument, - }, - result: { - data: { - currentUser: mockCurrentUser, - }, - }, - }, - { - request: { - query: ViewListingQueryActiveByListingIdDocument, - variables: { listingId: '1' }, - }, - result: { - data: { - queryActiveByListingId: [], - }, - }, - }, + ...buildBaseListingMocks(), { request: { query: HomeListingInformationCancelReservationRequestDocument, diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx index bfb2eab2a..6a45fb24d 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx @@ -1,8 +1,70 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect, within, userEvent, fn } from 'storybook/test'; +import { expect, within, userEvent, fn, waitFor } from 'storybook/test'; import { ListingInformation } from './listing-information.tsx'; import { withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; +const POPCONFIRM_SELECTORS = { + title: '.ant-popconfirm-title', + confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', + cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', +} as const; + +const baseReservationRequest = { + __typename: 'ReservationRequest' as const, + id: 'res-1', + reservationPeriodStart: '1738368000000', + reservationPeriodEnd: '1739145600000', +}; + +const createUserReservationRequest = (state: 'Requested' | 'Accepted') => ({ + ...baseReservationRequest, + state, +}); + +const waitForPopconfirm = async () => + waitFor( + () => { + const title = document.querySelector(POPCONFIRM_SELECTORS.title); + if (!title) throw new Error('Popconfirm not found'); + return title; + }, + { timeout: 1000 }, + ); + +const openCancelRequestPopconfirm = async ( + canvas: ReturnType, +) => { + const cancelButton = canvas.queryByRole('button', { + name: /Cancel Request/i, + }); + expect(cancelButton).toBeTruthy(); + if (cancelButton) { + await userEvent.click(cancelButton); + await waitForPopconfirm(); + } + return cancelButton; +}; + +const confirmPopconfirm = async () => { + const confirmButton = document.querySelector( + POPCONFIRM_SELECTORS.confirmButton, + ) as HTMLElement | null; + if (confirmButton) { + await userEvent.click(confirmButton); + } + return confirmButton; +}; + +const cancelPopconfirm = async () => { + const cancelButton = document.querySelector( + POPCONFIRM_SELECTORS.cancelButton, + ) as HTMLElement | null; + if (cancelButton) { + await userEvent.click(cancelButton); + } + return cancelButton; +}; + const mockListing = { __typename: 'ItemListing' as const, listingType: 'item-listing' as const, @@ -187,29 +249,15 @@ export const ClickReserveButton: Story = { export const ClickCancelButton: Story = { args: { onCancelClick: fn(), - userReservationRequest: { - __typename: 'ReservationRequest' as const, - id: 'res-1', - state: 'Requested' as const, - reservationPeriodStart: '1738368000000', - reservationPeriodEnd: '1739145600000', - }, + userReservationRequest: createUserReservationRequest('Requested'), }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - const cancelButton = canvas.queryByRole('button', { name: /Cancel/i }); - if (cancelButton) { - await userEvent.click(cancelButton); - await new Promise((resolve) => setTimeout(resolve, 100)); - const confirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn-primary', - ); - if (confirmButton) { - await userEvent.click(confirmButton as HTMLElement); - expect(args.onCancelClick).toHaveBeenCalled(); - } - } + + await openCancelRequestPopconfirm(canvas); + await confirmPopconfirm(); + expect(args.onCancelClick).toHaveBeenCalled(); }, }; @@ -323,13 +371,7 @@ export const ClearDateSelection: Story = { export const CancelButtonWithPopconfirm: Story = { args: { - userReservationRequest: { - __typename: 'ReservationRequest' as const, - id: 'res-1', - state: 'Requested' as const, - reservationPeriodStart: '1738368000000', - reservationPeriodEnd: '1739145600000', - }, + userReservationRequest: createUserReservationRequest('Requested'), onCancelClick: fn(), cancelLoading: false, }, @@ -337,48 +379,19 @@ export const CancelButtonWithPopconfirm: Story = { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - // Verify cancel button is present - const cancelButton = canvas.queryByRole('button', { - name: /Cancel Request/i, - }); - expect(cancelButton).toBeTruthy(); + await openCancelRequestPopconfirm(canvas); - if (cancelButton) { - // Click cancel button to trigger Popconfirm - await userEvent.click(cancelButton); - - // Wait for Popconfirm to appear - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Verify Popconfirm title appears - const popconfirmTitle = document.querySelector('.ant-popconfirm-title'); - expect(popconfirmTitle?.textContent).toContain( - 'Cancel Reservation Request', - ); - - // Click confirm button - const confirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn-primary', - ) as HTMLElement; - if (confirmButton) { - await userEvent.click(confirmButton); - - // Verify onCancelClick was called - expect(args.onCancelClick).toHaveBeenCalled(); - } - } + const title = document.querySelector(POPCONFIRM_SELECTORS.title); + expect(title?.textContent).toContain('Cancel Reservation Request'); + + await confirmPopconfirm(); + expect(args.onCancelClick).toHaveBeenCalled(); }, }; export const CancelButtonLoading: Story = { args: { - userReservationRequest: { - __typename: 'ReservationRequest' as const, - id: 'res-1', - state: 'Requested' as const, - reservationPeriodStart: '1738368000000', - reservationPeriodEnd: '1739145600000', - }, + userReservationRequest: createUserReservationRequest('Requested'), cancelLoading: true, }, play: async ({ canvasElement }) => { @@ -395,13 +408,7 @@ export const CancelButtonLoading: Story = { export const NoCancelButtonForAcceptedReservation: Story = { args: { - userReservationRequest: { - __typename: 'ReservationRequest' as const, - id: 'res-1', - state: 'Accepted' as const, - reservationPeriodStart: '1738368000000', - reservationPeriodEnd: '1739145600000', - }, + userReservationRequest: createUserReservationRequest('Accepted'), }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -415,38 +422,16 @@ export const NoCancelButtonForAcceptedReservation: Story = { export const PopconfirmCancelButton: Story = { args: { - userReservationRequest: { - __typename: 'ReservationRequest' as const, - id: 'res-1', - state: 'Requested' as const, - reservationPeriodStart: '1738368000000', - reservationPeriodEnd: '1739145600000', - }, + userReservationRequest: createUserReservationRequest('Requested'), onCancelClick: fn(), }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - const cancelButton = canvas.queryByRole('button', { - name: /Cancel Request/i, - }); - if (cancelButton) { - await userEvent.click(cancelButton); - - // Wait for Popconfirm - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Click the "No" button to cancel the Popconfirm - const cancelPopconfirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', - ) as HTMLElement; - if (cancelPopconfirmButton) { - await userEvent.click(cancelPopconfirmButton); - - // Verify onCancelClick was NOT called - expect(args.onCancelClick).not.toHaveBeenCalled(); - } - } + await openCancelRequestPopconfirm(canvas); + await cancelPopconfirm(); + + expect(args.onCancelClick).not.toHaveBeenCalled(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index 650b24576..dd0ddd2bc 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -1,6 +1,49 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ReservationActions } from '../components/reservation-actions.js'; -import { expect, fn, userEvent, within } from 'storybook/test'; +import { expect, fn, userEvent, waitFor, within } from 'storybook/test'; + +const POPCONFIRM_SELECTORS = { + title: '.ant-popconfirm-title', + description: '.ant-popconfirm-description', + confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', + cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', +} as const; + +type Canvas = ReturnType; + +const getButtons = (canvas: Canvas) => canvas.getAllByRole('button'); +const queryButtons = (canvas: Canvas) => canvas.queryAllByRole('button'); +const getFirstButton = (canvas: Canvas) => getButtons(canvas)[0]; + +const waitForPopconfirm = async () => + waitFor( + () => { + const title = document.querySelector(POPCONFIRM_SELECTORS.title); + if (!title) throw new Error('Popconfirm not found'); + return title; + }, + { timeout: 1000 }, + ); + +const getPopconfirmElements = () => ({ + title: document.querySelector(POPCONFIRM_SELECTORS.title), + description: document.querySelector(POPCONFIRM_SELECTORS.description), + confirmButton: document.querySelector( + POPCONFIRM_SELECTORS.confirmButton, + ) as HTMLElement | null, + cancelButton: document.querySelector( + POPCONFIRM_SELECTORS.cancelButton, + ) as HTMLElement | null, +}); + +const expectNoButtons = (canvas: Canvas) => { + const buttons = queryButtons(canvas); + expect(buttons.length).toBe(0); +}; + +const expectEmptyCanvas = (canvasElement: HTMLElement) => { + expect(canvasElement.children.length).toBe(0); +}; const meta: Meta = { title: 'Molecules/ReservationActions', @@ -36,7 +79,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 +100,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 @@ -77,18 +120,15 @@ export const ButtonInteraction: Story = { const canvas = within(canvasElement); // Get all buttons - const buttons = canvas.getAllByRole('button'); + const buttons = getButtons(canvas); expect(buttons.length).toBeGreaterThan(0); - // Click the first button (typically cancel or message) - if (buttons[0]) { - await userEvent.click(buttons[0]); - // Verify the callback was called - const callbacks = [args.onCancel, args.onClose, args.onMessage]; - const called = callbacks.some( - (cb) => cb && (cb as any).mock?.calls?.length > 0, - ); - expect(called || true).toBe(true); // Allow pass if callbacks are called + // Click the message button (second button in REQUESTED state - first is cancel with Popconfirm) + const messageButton = buttons[1]; + if (messageButton) { + await userEvent.click(messageButton); + // Verify the message callback was called (message button doesn't have Popconfirm) + expect(args.onMessage).toHaveBeenCalled(); } }, }; @@ -119,7 +159,7 @@ export const LoadingStates: Story = { onMessage: fn(), cancelLoading: true, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify loading state is rendered @@ -141,39 +181,20 @@ export const RequestedWithPopconfirm: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - // Get all buttons - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); + const cancelButton = getFirstButton(canvas); + expect(cancelButton).toBeTruthy(); - // Find cancel button (first button in REQUESTED state) - const cancelButton = buttons[0]; if (cancelButton) { - // Click to trigger Popconfirm await userEvent.click(cancelButton); + await waitForPopconfirm(); + + const { title, description, confirmButton } = getPopconfirmElements(); + + expect(title?.textContent).toContain('Cancel Reservation Request'); + expect(description?.textContent).toContain('Are you sure'); - // Wait for Popconfirm - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Verify Popconfirm appears - const popconfirmTitle = document.querySelector('.ant-popconfirm-title'); - expect(popconfirmTitle?.textContent).toContain( - 'Cancel Reservation Request', - ); - - // Verify description - const popconfirmDesc = document.querySelector( - '.ant-popconfirm-description', - ); - expect(popconfirmDesc?.textContent).toContain('Are you sure'); - - // Click confirm - const confirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn-primary', - ) as HTMLElement; if (confirmButton) { await userEvent.click(confirmButton); - - // Verify callback was called expect(args.onCancel).toHaveBeenCalled(); } } @@ -189,23 +210,17 @@ export const PopconfirmCancelAction: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const buttons = canvas.getAllByRole('button'); - const cancelButton = buttons[0]; + const cancelButton = getFirstButton(canvas); + expect(cancelButton).toBeTruthy(); if (cancelButton) { await userEvent.click(cancelButton); + await waitForPopconfirm(); - // Wait for Popconfirm - await new Promise((resolve) => setTimeout(resolve, 100)); + const { cancelButton: cancelPopconfirmButton } = getPopconfirmElements(); - // Click "No" button to cancel - const cancelPopconfirmButton = document.querySelector( - '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', - ) as HTMLElement; if (cancelPopconfirmButton) { await userEvent.click(cancelPopconfirmButton); - - // Verify onCancel was NOT called expect(args.onCancel).not.toHaveBeenCalled(); } } @@ -219,15 +234,11 @@ export const RejectedNoActions: Story = { onClose: fn(), onMessage: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); - // Verify no buttons are rendered for REJECTED status - const buttons = canvas.queryAllByRole('button'); - expect(buttons.length).toBe(0); - - // Component should return null and render nothing - expect(canvasElement.children.length).toBe(0); + expectNoButtons(canvas); + expectEmptyCanvas(canvasElement); }, }; @@ -238,12 +249,10 @@ export const CancelledNoActions: Story = { onClose: fn(), onMessage: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); - // Verify no buttons for cancelled state - const buttons = canvas.queryAllByRole('button'); - expect(buttons.length).toBe(0); + expectNoButtons(canvas); }, }; @@ -254,12 +263,10 @@ export const ClosedNoActions: Story = { onClose: fn(), onMessage: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); - // Verify no buttons for closed state - const buttons = canvas.queryAllByRole('button'); - expect(buttons.length).toBe(0); + expectNoButtons(canvas); }, }; @@ -269,11 +276,11 @@ export const AcceptedActions: Story = { onClose: fn(), onMessage: fn(), }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify actions are present for accepted status - const buttons = canvas.getAllByRole('button'); + const buttons = getButtons(canvas); expect(buttons.length).toBeGreaterThan(0); // Should have Close and Message buttons @@ -288,15 +295,15 @@ export const CancelLoadingState: Story = { onMessage: fn(), cancelLoading: true, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify loading state renders buttons - const buttons = canvas.getAllByRole('button'); + const buttons = getButtons(canvas); expect(buttons.length).toBeGreaterThan(0); - // Verify buttons are present (loading prop on ReservationActionButton) - const cancelButton = buttons[0]; + // Verify cancel button is present with loading state + const cancelButton = getFirstButton(canvas); expect(cancelButton).toBeTruthy(); }, }; @@ -308,15 +315,15 @@ export const CloseLoadingState: Story = { onMessage: fn(), closeLoading: true, }, - play: async ({ canvasElement }) => { + play: ({ canvasElement }) => { const canvas = within(canvasElement); // Verify loading state renders buttons - const buttons = canvas.getAllByRole('button'); + const buttons = getButtons(canvas); expect(buttons.length).toBeGreaterThan(0); - // Verify buttons are present (loading prop on ReservationActionButton) - const closeButton = buttons[0]; + // Verify close button is present with loading state + const closeButton = getFirstButton(canvas); expect(closeButton).toBeTruthy(); }, }; diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts index b3f2e8234..cdd1e11bd 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -97,7 +97,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { 'Attempting to cancel a non-existent reservation request', ({ Given, And, When, Then }) => { Given('a reservation request ID "reservation-999"', () => { - command = { id: 'reservation-999' }; + command = { id: 'reservation-999', callerId: 'user-123' }; }); And('the reservation request does not exist', () => { @@ -184,6 +184,58 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }, ); + Scenario( + 'Cancellation fails when reservation is in Accepted state', + ({ Given, And, When, Then }) => { + Given('a reservation request ID "reservation-accepted"', () => { + command = { id: 'reservation-accepted', callerId: 'user-123' }; + }); + + And('the reservation request is in Accepted state', () => { + const mockReservationRequest = { + id: 'reservation-accepted', + state: 'Accepted', + loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), + }; + + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + mockDataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(mockReservationRequest), + save: vi.fn().mockImplementation(() => { + throw new Error('Cannot cancel reservation in current state'); + }), + }; + await callback(mockRepo); + }, + ); + }); + + When('the cancel command is executed', async () => { + const cancelFn = cancel(mockDataSources); + try { + result = await cancelFn(command); + } catch (err) { + error = err; + } + }); + + Then( + 'an error "Cannot cancel reservation in current state" should be thrown', + () => { + expect(error).toBeDefined(); + expect(error.message).toBe( + 'Cannot cancel reservation in current state', + ); + }, + ); + }, + ); + Scenario( 'Authorization failure when caller is not the reserver', ({ Given, And, When, Then }) => { diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature index 4e54a1d1c..e5a7b1cc1 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature @@ -22,6 +22,12 @@ Feature: Cancel Reservation Request When the cancel command is executed Then an error "Reservation request not cancelled" should be thrown + Scenario: Cancellation fails when reservation is in Accepted state + Given a reservation request ID "reservation-accepted" + And the reservation request is in Accepted state + When the cancel command is executed + Then an error "Cannot cancel reservation in current state" should be thrown + Scenario: Authorization failure when caller is not the reserver Given a reservation request ID "reservation-789" And the reservation request belongs to a different user From e08f84b6b8b56164a5f5959c3c1fabad1d289ebe Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 14:28:33 -0500 Subject: [PATCH 04/20] added correct validation for reserver canceling a reservation request and shared popconfirm test utilities --- .../listing-information.container.stories.tsx | 37 +-- .../listing-information.stories.tsx | 44 +--- .../components/reservation-actions.tsx | 21 +- .../stories/reservation-actions.stories.tsx | 70 +++--- .../popconfirm-test-utils.stories.tsx | 224 ++++++++++++++++++ .../src/test-utils/popconfirm-test-utils.ts | 133 +++++++++++ .../reservation-request/cancel.test.ts | 53 ++++- .../reservation-request/cancel.ts | 10 + .../features/cancel.feature | 6 + .../reservation-request.resolvers.test.ts | 7 +- .../reservation-request.resolvers.ts | 8 +- 11 files changed, 494 insertions(+), 119 deletions(-) create mode 100644 apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx create mode 100644 apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx index 150d31968..13948e695 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect, userEvent, waitFor, within } from 'storybook/test'; +import { expect } from 'storybook/test'; import { ListingInformationContainer } from './listing-information.container.tsx'; import { withMockApolloClient, @@ -12,38 +12,7 @@ import { HomeListingInformationCancelReservationRequestDocument, ViewListingActiveReservationRequestForListingDocument, } from '../../../../../../generated.tsx'; - -const POPCONFIRM_SELECTORS = { - confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', -} as const; - -const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { - const canvas = within(canvasElement); - - const cancelButton = await waitFor( - () => { - const btn = canvas.queryByRole('button', { name: /Cancel/i }); - if (!btn) throw new Error('Cancel button not found yet'); - return btn; - }, - { timeout: 1000 }, - ); - - await userEvent.click(cancelButton); - - const confirmButton = await waitFor( - () => { - const btn = document.querySelector( - POPCONFIRM_SELECTORS.confirmButton, - ) as HTMLElement | null; - if (!btn) throw new Error('Confirm button not found yet'); - return btn; - }, - { timeout: 1000 }, - ); - - await userEvent.click(confirmButton); -}; +import { clickCancelThenConfirm } from '../../../../../../test-utils/popconfirm-test-utils.ts'; const buildBaseListingMocks = () => [ { @@ -329,7 +298,7 @@ export const CancelReservationLoading: Story = { }, }, }, - delay: 3000, // Simulate slow response + delay: 200, // Brief delay to verify loading state without slowing tests result: { data: { cancelReservation: { diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx index 6a45fb24d..a1dd01d5c 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx @@ -1,13 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect, within, userEvent, fn, waitFor } from 'storybook/test'; +import { expect, within, userEvent, fn } from 'storybook/test'; import { ListingInformation } from './listing-information.tsx'; import { withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; - -const POPCONFIRM_SELECTORS = { - title: '.ant-popconfirm-title', - confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', - cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', -} as const; +import { + POPCONFIRM_SELECTORS, + waitForPopconfirm, + confirmPopconfirm, + cancelPopconfirm, +} from '../../../../../../test-utils/popconfirm-test-utils.ts'; const baseReservationRequest = { __typename: 'ReservationRequest' as const, @@ -21,16 +21,6 @@ const createUserReservationRequest = (state: 'Requested' | 'Accepted') => ({ state, }); -const waitForPopconfirm = async () => - waitFor( - () => { - const title = document.querySelector(POPCONFIRM_SELECTORS.title); - if (!title) throw new Error('Popconfirm not found'); - return title; - }, - { timeout: 1000 }, - ); - const openCancelRequestPopconfirm = async ( canvas: ReturnType, ) => { @@ -45,26 +35,6 @@ const openCancelRequestPopconfirm = async ( return cancelButton; }; -const confirmPopconfirm = async () => { - const confirmButton = document.querySelector( - POPCONFIRM_SELECTORS.confirmButton, - ) as HTMLElement | null; - if (confirmButton) { - await userEvent.click(confirmButton); - } - return confirmButton; -}; - -const cancelPopconfirm = async () => { - const cancelButton = document.querySelector( - POPCONFIRM_SELECTORS.cancelButton, - ) as HTMLElement | null; - if (cancelButton) { - await userEvent.click(cancelButton); - } - return cancelButton; -}; - const mockListing = { __typename: 'ItemListing' as const, listingType: 'item-listing' as const, diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx index 1b9e5daad..15e58b5a0 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx @@ -63,9 +63,24 @@ export const ReservationActions: React.FC = ({ ]; case 'REJECTED': - // No actions for rejected reservations - they've already been dismissed by the owner - return []; - + return [ + + + + + , + ]; default: // No actions for cancelled or closed reservations return []; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index dd0ddd2bc..c56c96a27 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -1,50 +1,21 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ReservationActions } from '../components/reservation-actions.js'; -import { expect, fn, userEvent, waitFor, within } from 'storybook/test'; - -const POPCONFIRM_SELECTORS = { - title: '.ant-popconfirm-title', - description: '.ant-popconfirm-description', - confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', - cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', -} as const; +import { expect, fn, userEvent, within } from 'storybook/test'; +import { + canvasUtils, + waitForPopconfirm, + getPopconfirmElements, +} from '../../../../../test-utils/popconfirm-test-utils.ts'; type Canvas = ReturnType; -const getButtons = (canvas: Canvas) => canvas.getAllByRole('button'); -const queryButtons = (canvas: Canvas) => canvas.queryAllByRole('button'); -const getFirstButton = (canvas: Canvas) => getButtons(canvas)[0]; - -const waitForPopconfirm = async () => - waitFor( - () => { - const title = document.querySelector(POPCONFIRM_SELECTORS.title); - if (!title) throw new Error('Popconfirm not found'); - return title; - }, - { timeout: 1000 }, - ); - -const getPopconfirmElements = () => ({ - title: document.querySelector(POPCONFIRM_SELECTORS.title), - description: document.querySelector(POPCONFIRM_SELECTORS.description), - confirmButton: document.querySelector( - POPCONFIRM_SELECTORS.confirmButton, - ) as HTMLElement | null, - cancelButton: document.querySelector( - POPCONFIRM_SELECTORS.cancelButton, - ) as HTMLElement | null, -}); +const { getButtons, queryButtons, getFirstButton } = canvasUtils; const expectNoButtons = (canvas: Canvas) => { const buttons = queryButtons(canvas); expect(buttons.length).toBe(0); }; -const expectEmptyCanvas = (canvasElement: HTMLElement) => { - expect(canvasElement.children.length).toBe(0); -}; - const meta: Meta = { title: 'Molecules/ReservationActions', component: ReservationActions, @@ -227,18 +198,37 @@ export const PopconfirmCancelAction: Story = { }, }; -export const RejectedNoActions: Story = { +export const RejectedWithCancel: Story = { args: { status: 'REJECTED', onCancel: fn(), onClose: fn(), onMessage: fn(), }, - play: ({ canvasElement }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - expectNoButtons(canvas); - expectEmptyCanvas(canvasElement); + // REJECTED status should have Cancel button (domain allows cancellation from Rejected state) + const buttons = getButtons(canvas); + expect(buttons.length).toBe(1); + + const cancelButton = getFirstButton(canvas); + expect(cancelButton).toBeTruthy(); + + if (cancelButton) { + await userEvent.click(cancelButton); + await waitForPopconfirm(); + + const { title, description, confirmButton } = getPopconfirmElements(); + + expect(title?.textContent).toContain('Cancel Reservation Request'); + expect(description?.textContent).toContain('Are you sure'); + + if (confirmButton) { + await userEvent.click(confirmButton); + expect(args.onCancel).toHaveBeenCalled(); + } + } }, }; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx new file mode 100644 index 000000000..5c585fe71 --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx @@ -0,0 +1,224 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from 'storybook/test'; +import { Button, Popconfirm, Space } from 'antd'; +import type React from 'react'; +import { + canvasUtils, + waitForPopconfirm, + getPopconfirmElements, + confirmPopconfirm, + cancelPopconfirm, + triggerPopconfirmAnd, + POPCONFIRM_SELECTORS, +} from './popconfirm-test-utils.ts'; + +interface PopconfirmTestProps { + onConfirm?: () => void; + onCancel?: () => void; + showMultipleButtons?: boolean; +} + +const PopconfirmTestComponent: React.FC = ({ + onConfirm, + onCancel, + showMultipleButtons = false, +}) => ( + + + + + {showMultipleButtons && ( + <> + + + + )} + +); + +const meta: Meta = { + title: 'Test Utilities/PopconfirmTestUtils', + component: PopconfirmTestComponent, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const CanvasUtilsGetButtons: Story = { + args: { + showMultipleButtons: true, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + const buttons = canvasUtils.getButtons(canvas); + expect(buttons.length).toBe(3); + }, +}; + +export const CanvasUtilsQueryButtons: Story = { + args: { + showMultipleButtons: false, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + const buttons = canvasUtils.queryButtons(canvas); + expect(buttons.length).toBe(1); + }, +}; + +export const CanvasUtilsGetFirstButton: Story = { + args: { + showMultipleButtons: true, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + const firstButton = canvasUtils.getFirstButton(canvas); + expect(firstButton).toBeTruthy(); + expect(firstButton.textContent).toBe('Trigger Popconfirm'); + }, +}; + +export const CanvasUtilsAssertButtonCount: Story = { + args: { + showMultipleButtons: true, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + canvasUtils.assertButtonCount(canvas, 3); + }, +}; + +export const CanvasUtilsAssertHasButtons: Story = { + args: {}, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + canvasUtils.assertHasButtons(canvas); + }, +}; + +export const WaitForPopconfirmSuccess: Story = { + args: { + onConfirm: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const triggerButton = canvasUtils.getFirstButton(canvas); + await userEvent.click(triggerButton); + + const title = await waitForPopconfirm(); + expect(title).toBeTruthy(); + expect(title.textContent).toContain('Test Confirmation'); + }, +}; + +export const GetPopconfirmElementsSuccess: Story = { + args: { + onConfirm: fn(), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const triggerButton = canvasUtils.getFirstButton(canvas); + await userEvent.click(triggerButton); + await waitForPopconfirm(); + + const elements = getPopconfirmElements(); + expect(elements.title?.textContent).toContain('Test Confirmation'); + expect(elements.description?.textContent).toContain('Are you sure'); + expect(elements.confirmButton).toBeTruthy(); + expect(elements.cancelButton).toBeTruthy(); + }, +}; + +export const ConfirmPopconfirmSuccess: Story = { + args: { + onConfirm: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const triggerButton = canvasUtils.getFirstButton(canvas); + await userEvent.click(triggerButton); + await waitForPopconfirm(); + + await confirmPopconfirm(); + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; + +export const CancelPopconfirmSuccess: Story = { + args: { + onCancel: fn(), + onConfirm: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const triggerButton = canvasUtils.getFirstButton(canvas); + await userEvent.click(triggerButton); + await waitForPopconfirm(); + + await cancelPopconfirm(); + // onConfirm should NOT be called when cancelling + expect(args.onConfirm).not.toHaveBeenCalled(); + }, +}; + +export const TriggerPopconfirmAndConfirm: Story = { + args: { + onConfirm: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'confirm', { + expectedTitle: 'Test Confirmation', + expectedDescription: 'Are you sure', + }); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; + +export const TriggerPopconfirmAndCancel: Story = { + args: { + onConfirm: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'cancel', { + expectedTitle: 'Test Confirmation', + }); + + expect(args.onConfirm).not.toHaveBeenCalled(); + }, +}; + +export const PopconfirmSelectorsExist: Story = { + args: {}, + play: () => { + // Verify all selectors are defined + expect(POPCONFIRM_SELECTORS.title).toBe('.ant-popconfirm-title'); + expect(POPCONFIRM_SELECTORS.description).toBe( + '.ant-popconfirm-description', + ); + expect(POPCONFIRM_SELECTORS.confirmButton).toBe( + '.ant-popconfirm-buttons .ant-btn-primary', + ); + expect(POPCONFIRM_SELECTORS.cancelButton).toBe( + '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', + ); + }, +}; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts new file mode 100644 index 000000000..e7f7297ef --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts @@ -0,0 +1,133 @@ +import { expect, userEvent, waitFor, within } from 'storybook/test'; + +export const POPCONFIRM_SELECTORS = { + title: '.ant-popconfirm-title', + description: '.ant-popconfirm-description', + confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', + cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', +} as const; + +export type Canvas = ReturnType; +export type PopconfirmAction = 'confirm' | 'cancel'; + +export const canvasUtils = { + getButtons: (canvas: Canvas) => canvas.getAllByRole('button'), + queryButtons: (canvas: Canvas) => canvas.queryAllByRole('button'), + getFirstButton: (canvas: Canvas) => canvas.getAllByRole('button')[0], + assertNoButtons: (canvas: Canvas) => + expect(canvas.queryAllByRole('button').length).toBe(0), + assertButtonCount: (canvas: Canvas, count: number) => + expect(canvas.getAllByRole('button').length).toBe(count), + assertHasButtons: (canvas: Canvas) => + expect(canvas.getAllByRole('button').length).toBeGreaterThan(0), +}; + +export const waitForPopconfirm = async () => + waitFor( + () => { + const title = document.querySelector(POPCONFIRM_SELECTORS.title); + if (!title) throw new Error('Popconfirm not found'); + return title; + }, + { timeout: 1000 }, + ); + +export const getPopconfirmElements = () => ({ + title: document.querySelector(POPCONFIRM_SELECTORS.title), + description: document.querySelector(POPCONFIRM_SELECTORS.description), + confirmButton: document.querySelector( + POPCONFIRM_SELECTORS.confirmButton, + ) as HTMLElement | null, + cancelButton: document.querySelector( + POPCONFIRM_SELECTORS.cancelButton, + ) as HTMLElement | null, +}); + +export const confirmPopconfirm = async () => { + const confirmButton = document.querySelector( + POPCONFIRM_SELECTORS.confirmButton, + ) as HTMLElement | null; + if (confirmButton) { + await userEvent.click(confirmButton); + } + return confirmButton; +}; + +export const cancelPopconfirm = async () => { + const cancelButton = document.querySelector( + POPCONFIRM_SELECTORS.cancelButton, + ) as HTMLElement | null; + if (cancelButton) { + await userEvent.click(cancelButton); + } + return cancelButton; +}; + +export const triggerPopconfirmAnd = async ( + canvas: Canvas, + action: PopconfirmAction, + options?: { + triggerButtonIndex?: number; + expectedTitle?: string; + expectedDescription?: string; + }, +) => { + const { + triggerButtonIndex = 0, + expectedTitle, + expectedDescription, + } = options ?? {}; + + const buttons = canvas.getAllByRole('button'); + const triggerButton = buttons[triggerButtonIndex]; + expect(triggerButton).toBeTruthy(); + + if (!triggerButton) return; + + await userEvent.click(triggerButton); + await waitForPopconfirm(); + + const { title, description, confirmButton, cancelButton } = + getPopconfirmElements(); + + if (expectedTitle) { + expect(title?.textContent).toContain(expectedTitle); + } + if (expectedDescription) { + expect(description?.textContent).toContain(expectedDescription); + } + + const target = action === 'confirm' ? confirmButton : cancelButton; + + if (target) { + await userEvent.click(target); + } +}; + +export const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { + const canvas = within(canvasElement); + + const cancelButton = await waitFor( + () => { + const btn = canvas.queryByRole('button', { name: /Cancel/i }); + if (!btn) throw new Error('Cancel button not found yet'); + return btn; + }, + { timeout: 1000 }, + ); + + await userEvent.click(cancelButton); + + const confirmButton = await waitFor( + () => { + const btn = document.querySelector( + POPCONFIRM_SELECTORS.confirmButton, + ) as HTMLElement | null; + if (!btn) throw new Error('Confirm button not found yet'); + return btn; + }, + { timeout: 1000 }, + ); + + await userEvent.click(confirmButton); +}; diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts index cdd1e11bd..0d57e00fa 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -93,6 +93,55 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }, ); + Scenario( + 'Successfully cancelling a rejected reservation', + ({ Given, And, When, Then }) => { + Given('a reservation request ID "reservation-rejected"', () => { + command = { id: 'reservation-rejected', callerId: 'user-123' }; + }); + + And('the reservation request exists and is in rejected state', () => { + const mockReservationRequest = { + id: 'reservation-rejected', + state: 'Rejected', + loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), + }; + + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + mockDataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(mockReservationRequest), + save: vi.fn().mockResolvedValue({ + ...mockReservationRequest, + state: 'Cancelled', + }), + }; + await callback(mockRepo); + }, + ); + }); + + When('the cancel command is executed', async () => { + const cancelFn = cancel(mockDataSources); + try { + result = await cancelFn(command); + } catch (err) { + error = err; + } + }); + + Then('the reservation request should be cancelled', () => { + expect(error).toBeUndefined(); + expect(result).toBeDefined(); + expect(result.state).toBe('Cancelled'); + }); + }, + ); + Scenario( 'Attempting to cancel a non-existent reservation request', ({ Given, And, When, Then }) => { @@ -206,9 +255,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { async (callback: any) => { const mockRepo = { getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi.fn().mockImplementation(() => { - throw new Error('Cannot cancel reservation in current state'); - }), + save: vi.fn(), }; await callback(mockRepo); }, diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts index d8f509356..c7b246878 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts @@ -6,6 +6,8 @@ export interface ReservationRequestCancelCommand { callerId: string; } +const CANCELLABLE_STATES = ['Requested', 'Rejected'] as const; + export const cancel = (dataSources: DataSources) => { return async ( command: ReservationRequestCancelCommand, @@ -27,6 +29,14 @@ export const cancel = (dataSources: DataSources) => { ); } + if ( + !CANCELLABLE_STATES.includes( + reservationRequest.state as (typeof CANCELLABLE_STATES)[number], + ) + ) { + throw new Error('Cannot cancel reservation in current state'); + } + reservationRequest.state = 'Cancelled'; reservationRequestToReturn = await repo.save(reservationRequest); }, diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature index e5a7b1cc1..c44acbb68 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/cancel.feature @@ -9,6 +9,12 @@ Feature: Cancel Reservation Request When the cancel command is executed Then the reservation request should be cancelled + Scenario: Successfully cancelling a rejected reservation + Given a reservation request ID "reservation-rejected" + And the reservation request exists and is in rejected state + When the cancel command is executed + Then the reservation request should be cancelled + Scenario: Attempting to cancel a non-existent reservation request Given a reservation request ID "reservation-999" And the reservation request does not exist diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts index 00a625d45..bd6b7436a 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts @@ -1161,7 +1161,12 @@ test.for(feature, ({ Scenario }) => { context = { applicationServices: { verifiedUser: { - verifiedJwt: { sub: 'user-123' }, + verifiedJwt: { sub: 'user-123', email: 'test@example.com' }, + }, + User: { + PersonalUser: { + queryByEmail: vi.fn().mockResolvedValue({ id: 'user-123' }), + }, }, ReservationRequest: { ReservationRequest: { diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index b7fb5d73f..776f60c42 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -2,6 +2,7 @@ import type { GraphContext } from '../../../init/context.ts'; import type { GraphQLResolveInfo } from 'graphql'; import type { Resolvers } from '../../builder/generated.ts'; import { + getUserByEmail, PopulateItemListingFromField, PopulateUserFromField, } from '../../resolver-helper.ts'; @@ -236,10 +237,15 @@ const reservationRequest: Resolvers = { ); } + const currentUser = await getUserByEmail(verifiedJwt.email, context); + if (!currentUser) { + throw new Error('User not found'); + } + return await context.applicationServices.ReservationRequest.ReservationRequest.cancel( { id: args.input.id, - callerId: verifiedJwt.sub, + callerId: currentUser.id, }, ); }, From 262a49b7d19c6bc7749afa1c95fa60592aad3c2c Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 14:47:19 -0500 Subject: [PATCH 05/20] upgrade storybook in ui-sharethrift package.json and cellix vitest-config to 9.1.3 --- apps/ui-sharethrift/package.json | 12 ++++++------ packages/cellix/vitest-config/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/ui-sharethrift/package.json b/apps/ui-sharethrift/package.json index 12c0e7c42..196bf1af7 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -37,11 +37,11 @@ "@chromatic-com/storybook": "^4.1.0", "@eslint/js": "^9.30.1", "@graphql-typed-document-node/core": "^3.2.0", - "@storybook/addon-a11y": "^9.1.1", - "@storybook/addon-docs": "^9.1.1", - "@storybook/addon-vitest": "^9.1.1", - "@storybook/react": "^9.1.10", - "@storybook/react-vite": "^9.1.1", + "@storybook/addon-a11y": "^9.1.3", + "@storybook/addon-docs": "^9.1.3", + "@storybook/addon-vitest": "^9.1.3", + "@storybook/react": "^9.1.3", + "@storybook/react-vite": "^9.1.3", "@testing-library/jest-dom": "^6.9.1", "@types/lodash": "^4.17.20", "@types/react": "^19.1.9", @@ -53,7 +53,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", - "storybook": "^9.1.1", + "storybook": "^9.1.3", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^7.1.2", diff --git a/packages/cellix/vitest-config/package.json b/packages/cellix/vitest-config/package.json index 1f76a6e4e..02e7bf712 100644 --- a/packages/cellix/vitest-config/package.json +++ b/packages/cellix/vitest-config/package.json @@ -11,7 +11,7 @@ "build": "tsc --build" }, "dependencies": { - "@storybook/addon-vitest": "^9.1.10", + "@storybook/addon-vitest": "^9.1.3", "vitest": "^3.2.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f2312a78..c55fd2b79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,19 +297,19 @@ importers: specifier: ^3.2.0 version: 3.2.0(graphql@16.11.0) '@storybook/addon-a11y': - specifier: ^9.1.1 + specifier: ^9.1.3 version: 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-docs': - specifier: ^9.1.1 + specifier: ^9.1.3 version: 9.1.16(@types/react@19.2.2)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-vitest': - specifier: ^9.1.1 + specifier: ^9.1.3 version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) '@storybook/react': - specifier: ^9.1.10 + specifier: ^9.1.3 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^9.1.1 + specifier: ^9.1.3 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@testing-library/jest-dom': specifier: ^6.9.1 @@ -345,7 +345,7 @@ importers: specifier: ^16.3.0 version: 16.4.0 storybook: - specifier: ^9.1.1 + specifier: ^9.1.3 version: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) typescript: specifier: ~5.8.3 @@ -636,7 +636,7 @@ importers: packages/cellix/vitest-config: dependencies: '@storybook/addon-vitest': - specifier: ^9.1.10 + specifier: ^9.1.3 version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) vitest: specifier: ^3.2.4 From 67e3e13d5de4749c60b0d871cc1e2b8176c260da Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 15:08:15 -0500 Subject: [PATCH 06/20] changed storybook version to 9.1.17 in ui-sharethrift, cellix ui-core and vitest-config, sthrift ui-components --- apps/ui-sharethrift/package.json | 12 +- packages/cellix/ui-core/package.json | 12 +- packages/cellix/vitest-config/package.json | 2 +- packages/sthrift/ui-components/package.json | 12 +- pnpm-lock.yaml | 1995 +++++++++++++++++-- 5 files changed, 1839 insertions(+), 194 deletions(-) diff --git a/apps/ui-sharethrift/package.json b/apps/ui-sharethrift/package.json index 196bf1af7..68befb844 100644 --- a/apps/ui-sharethrift/package.json +++ b/apps/ui-sharethrift/package.json @@ -37,11 +37,11 @@ "@chromatic-com/storybook": "^4.1.0", "@eslint/js": "^9.30.1", "@graphql-typed-document-node/core": "^3.2.0", - "@storybook/addon-a11y": "^9.1.3", - "@storybook/addon-docs": "^9.1.3", - "@storybook/addon-vitest": "^9.1.3", - "@storybook/react": "^9.1.3", - "@storybook/react-vite": "^9.1.3", + "@storybook/addon-a11y": "^9.1.17", + "@storybook/addon-docs": "^9.1.17", + "@storybook/addon-vitest": "^9.1.17", + "@storybook/react": "^9.1.17", + "@storybook/react-vite": "^9.1.17", "@testing-library/jest-dom": "^6.9.1", "@types/lodash": "^4.17.20", "@types/react": "^19.1.9", @@ -53,7 +53,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", - "storybook": "^9.1.3", + "storybook": "^9.1.17", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^7.1.2", diff --git a/packages/cellix/ui-core/package.json b/packages/cellix/ui-core/package.json index 7e272c191..20678d18d 100644 --- a/packages/cellix/ui-core/package.json +++ b/packages/cellix/ui-core/package.json @@ -39,12 +39,12 @@ "@cellix/typescript-config": "workspace:*", "@cellix/vitest-config": "workspace:*", "@chromatic-com/storybook": "^4.1.1", - "@storybook/addon-a11y": "^9.1.3", - "@storybook/addon-docs": "^9.1.3", + "@storybook/addon-a11y": "^9.1.17", + "@storybook/addon-docs": "^9.1.17", "@storybook/addon-onboarding": "^9.1.3", - "@storybook/addon-vitest": "^9.1.3", - "@storybook/react": "^9.1.9", - "@storybook/react-vite": "^9.1.3", + "@storybook/addon-vitest": "^9.1.17", + "@storybook/react": "^9.1.17", + "@storybook/react-vite": "^9.1.17", "@types/react": "^19.1.16", "@vitest/browser": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", @@ -52,7 +52,7 @@ "react-oidc-context": "^3.3.0", "react-router-dom": "^7.9.3", "rimraf": "^6.0.1", - "storybook": "^9.1.3", + "storybook": "^9.1.17", "typescript": "^5.8.3", "vitest": "^3.2.4" }, diff --git a/packages/cellix/vitest-config/package.json b/packages/cellix/vitest-config/package.json index 02e7bf712..7ae765a33 100644 --- a/packages/cellix/vitest-config/package.json +++ b/packages/cellix/vitest-config/package.json @@ -11,7 +11,7 @@ "build": "tsc --build" }, "dependencies": { - "@storybook/addon-vitest": "^9.1.3", + "@storybook/addon-vitest": "^9.1.17", "vitest": "^3.2.4" }, "devDependencies": { diff --git a/packages/sthrift/ui-components/package.json b/packages/sthrift/ui-components/package.json index 433db20b9..0774a89c2 100644 --- a/packages/sthrift/ui-components/package.json +++ b/packages/sthrift/ui-components/package.json @@ -59,19 +59,19 @@ "@cellix/typescript-config": "workspace:*", "@cellix/vitest-config": "workspace:*", "@chromatic-com/storybook": "^4.1.1", - "@storybook/addon-a11y": "^9.1.3", - "@storybook/addon-docs": "^9.1.3", + "@storybook/addon-a11y": "^9.1.17", + "@storybook/addon-docs": "^9.1.17", "@storybook/addon-onboarding": "^9.1.3", - "@storybook/addon-vitest": "^9.1.3", - "@storybook/react": "^9.1.10", - "@storybook/react-vite": "^9.1.3", + "@storybook/addon-vitest": "^9.1.17", + "@storybook/react": "^9.1.17", + "@storybook/react-vite": "^9.1.17", "@types/react": "^19.1.11", "@types/react-dom": "^19.1.6", "@vitest/browser": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", "jsdom": "^26.1.0", "rimraf": "^6.0.1", - "storybook": "^9.1.3", + "storybook": "^9.1.17", "typescript": "^5.8.3", "vite": "^7.0.4", "vitest": "^3.2.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c55fd2b79..c3819031c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,7 +234,7 @@ importers: version: 5.6.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) apps/ui-sharethrift: dependencies: @@ -289,7 +289,7 @@ importers: version: link:../../packages/cellix/vitest-config '@chromatic-com/storybook': specifier: ^4.1.0 - version: 4.1.2(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 4.1.2(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@eslint/js': specifier: ^9.30.1 version: 9.38.0 @@ -297,20 +297,20 @@ importers: specifier: ^3.2.0 version: 3.2.0(graphql@16.11.0) '@storybook/addon-a11y': - specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + specifier: ^9.1.17 + version: 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-docs': - specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.2)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + specifier: ^9.1.17 + version: 9.1.17(@types/react@19.2.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-vitest': - specifier: ^9.1.3 - version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) + specifier: ^9.1.17 + version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) '@storybook/react': - specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) + specifier: ^9.1.17 + version: 9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^9.1.17 + version: 9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.53.5)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -325,10 +325,10 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.7.0(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/browser': specifier: 3.2.4 - version: 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) @@ -345,8 +345,8 @@ importers: specifier: ^16.3.0 version: 16.4.0 storybook: - specifier: ^9.1.3 - version: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^9.1.17 + version: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) typescript: specifier: ~5.8.3 version: 5.8.3 @@ -355,10 +355,10 @@ importers: version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3) vite: specifier: ^7.1.2 - version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) packages/cellix/api-services-spec: devDependencies: @@ -512,10 +512,10 @@ importers: version: 9.0.10 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@24.9.2)(typescript@5.8.3) + version: 10.9.2(@types/node@24.10.4)(typescript@5.8.3) ts-node-dev: specifier: ^2.0.0 - version: 2.0.0(@types/node@24.9.2)(typescript@5.8.3) + version: 2.0.0(@types/node@24.10.4)(typescript@5.8.3) tsc-watch: specifier: ^7.1.1 version: 7.2.0(typescript@5.8.3) @@ -567,13 +567,13 @@ importers: dependencies: antd: specifier: '>=5.0.0' - version: 5.27.6(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 5.27.6(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: '>=18.0.0' - version: 19.2.0 + version: 19.2.3 react-dom: specifier: '>=18.0.0' - version: 19.2.0(react@19.2.0) + version: 19.2.3(react@19.2.3) devDependencies: '@cellix/typescript-config': specifier: workspace:* @@ -583,31 +583,31 @@ importers: version: link:../vitest-config '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.2(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 4.1.2(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-a11y': - specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + specifier: ^9.1.17 + version: 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-docs': - specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.2)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + specifier: ^9.1.17 + version: 9.1.17(@types/react@19.2.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 9.1.16(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-vitest': - specifier: ^9.1.3 - version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) + specifier: ^9.1.17 + version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) '@storybook/react': - specifier: ^9.1.9 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) + specifier: ^9.1.17 + version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^9.1.17 + version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.5)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@types/react': specifier: ^19.1.16 version: 19.2.2 '@vitest/browser': specifier: ^3.2.4 - version: 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 3.2.4(playwright@1.56.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) @@ -616,31 +616,31 @@ importers: version: 26.1.0 react-oidc-context: specifier: ^3.3.0 - version: 3.3.0(oidc-client-ts@3.3.0)(react@19.2.0) + version: 3.3.0(oidc-client-ts@3.3.0)(react@19.2.3) react-router-dom: specifier: ^7.9.3 - version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rimraf: specifier: ^6.0.1 version: 6.0.1 storybook: - specifier: ^9.1.3 - version: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^9.1.17 + version: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) typescript: specifier: ^5.8.3 version: 5.8.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) packages/cellix/vitest-config: dependencies: '@storybook/addon-vitest': - specifier: ^9.1.3 - version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) + specifier: ^9.1.17 + version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) devDependencies: '@cellix/typescript-config': specifier: workspace:* @@ -1285,25 +1285,25 @@ importers: version: link:../../cellix/vitest-config '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.2(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 4.1.2(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-a11y': - specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + specifier: ^9.1.17 + version: 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-docs': - specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.2)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + specifier: ^9.1.17 + version: 9.1.17(@types/react@19.2.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 9.1.16(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) '@storybook/addon-vitest': - specifier: ^9.1.3 - version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) + specifier: ^9.1.17 + version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) '@storybook/react': - specifier: ^9.1.10 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) + specifier: ^9.1.17 + version: 9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^9.1.17 + version: 9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.53.5)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@types/react': specifier: ^19.1.11 version: 19.2.2 @@ -1312,7 +1312,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitest/browser': specifier: ^3.2.4 - version: 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) @@ -1323,17 +1323,17 @@ importers: specifier: ^6.0.1 version: 6.0.1 storybook: - specifier: ^9.1.3 - version: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + specifier: ^9.1.17 + version: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^7.0.4 - version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) packages: @@ -2938,156 +2938,468 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.11': resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.11': resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.11': resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.11': resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.11': resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.11': resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.11': resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.11': resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.11': resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.11': resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.11': resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.11': resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.11': resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.11': resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.11': resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.11': resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.11': resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.11': resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.11': resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.11': resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.11': resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.11': resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.11': resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.11': resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.11': resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4169,111 +4481,221 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.53.5': + resolution: {integrity: sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.52.5': resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.53.5': + resolution: {integrity: sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.52.5': resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.53.5': + resolution: {integrity: sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.52.5': resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.53.5': + resolution: {integrity: sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.52.5': resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.53.5': + resolution: {integrity: sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.52.5': resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.53.5': + resolution: {integrity: sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.5': + '@rollup/rollup-linux-arm-gnueabihf@4.53.5': + resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.53.5': + resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.52.5': resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.53.5': + resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.52.5': resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.53.5': + resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.52.5': resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.53.5': + resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.52.5': resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.53.5': + resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.52.5': resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.53.5': + resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.52.5': resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.53.5': + resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.52.5': resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.53.5': + resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.52.5': resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.53.5': + resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.52.5': resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.53.5': + resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-openharmony-arm64@4.52.5': resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.53.5': + resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.52.5': resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.53.5': + resolution: {integrity: sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.52.5': resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.53.5': + resolution: {integrity: sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.52.5': resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.53.5': + resolution: {integrity: sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.52.5': resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.53.5': + resolution: {integrity: sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==} + cpu: [x64] + os: [win32] + '@sendgrid/client@8.1.6': resolution: {integrity: sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==} engines: {node: '>=12.*'} @@ -4384,28 +4806,28 @@ packages: engines: {node: '>= 18'} hasBin: true - '@storybook/addon-a11y@9.1.16': - resolution: {integrity: sha512-DpUqAMOgkC/K/DgB9osqbBYmiWWj7V444HeYLHcx7GdPtg2guq1jAcalsOnQeU3wXgUE+wNuyMm6qZKm7of11g==} + '@storybook/addon-a11y@9.1.17': + resolution: {integrity: sha512-xP2Nb+idph2r0wE2Lc3z7LjtyXxTS+U+mJWmS8hw5w0oU2TkVdV7Ew/V7/iNl5jIWMXIp9HCRmcJuKSSGuertA==} peerDependencies: - storybook: ^9.1.16 + storybook: ^9.1.17 - '@storybook/addon-docs@9.1.16': - resolution: {integrity: sha512-JfaUD6fC7ySLg5duRdaWZ0FUUXrgUvqbZe/agCbSyOaIHOtJdhGaPjOC3vuXTAcV8/8/wWmbu0iXFMD08iKvdw==} + '@storybook/addon-docs@9.1.17': + resolution: {integrity: sha512-yc4hlgkrwNi045qk210dRuIMijkgbLmo3ft6F4lOdpPRn4IUnPDj7FfZR8syGzUzKidxRfNtLx5m0yHIz83xtA==} peerDependencies: - storybook: ^9.1.16 + storybook: ^9.1.17 '@storybook/addon-onboarding@9.1.16': resolution: {integrity: sha512-vOACUkIRVQWH/RZyn0vvvu8f54j5JCXXjotzqpB4jWwi3SLSMAJLgSn01aOT9Z9rAHo7cXkN9WkG6xUFDG7YLA==} peerDependencies: storybook: ^9.1.16 - '@storybook/addon-vitest@9.1.16': - resolution: {integrity: sha512-X0rOOUMb5UHbfekcjnTeiDTarZdsg5irXXPxxL//8QQCFyCLF6Bdm1YNlCdF560PtwaaQPXzlxByD0FfGbtdWA==} + '@storybook/addon-vitest@9.1.17': + resolution: {integrity: sha512-2EIvZPz0N+mnIUnUHW3+GIgwJRIqjZrK5BFyHsi82NhOQ1LCh/1GqbcB+kNoaiXioRcAgOsHUDWbQZrvyx3GhQ==} peerDependencies: '@vitest/browser': ^3.0.0 || ^4.0.0 '@vitest/browser-playwright': ^4.0.0 '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^9.1.16 + storybook: ^9.1.17 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: '@vitest/browser': @@ -4417,16 +4839,16 @@ packages: vitest: optional: true - '@storybook/builder-vite@9.1.16': - resolution: {integrity: sha512-CyvYA5w1BKeSVaRavKi+euWxLffshq0v9Rz/5E9MKCitbYtjwkDH6UMIYmcbTs906mEBuYqrbz3nygDP0ppodw==} + '@storybook/builder-vite@9.1.17': + resolution: {integrity: sha512-OQCYaFWoTBvovN2IJmkAW+7FgHMJiih1WA/xqgpKIx0ImZjB4z5FrKgzQeXsrYcLEsynyaj+xN3JFUKsz5bzGQ==} peerDependencies: - storybook: ^9.1.16 + storybook: ^9.1.17 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/csf-plugin@9.1.16': - resolution: {integrity: sha512-GKlNNlmWeFBQxhQY5hZOSnFGbeKq69jal0dYNWoSImTjor28eYRHb9iQkDzRpijLPizBaB9MlxLsLrgFDp7adA==} + '@storybook/csf-plugin@9.1.17': + resolution: {integrity: sha512-o+ebQDdSfZHDRDhu2hNDGhCLIazEB4vEAqJcHgz1VsURq+l++bgZUcKojPMCAbeblptSEz2bwS0eYAOvG7aSXg==} peerDependencies: - storybook: ^9.1.16 + storybook: ^9.1.17 '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -4438,29 +4860,29 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - '@storybook/react-dom-shim@9.1.16': - resolution: {integrity: sha512-MsI4qTxdT6lMXQmo3IXhw3EaCC+vsZboyEZBx4pOJ+K/5cDJ6ZoQ3f0d4yGpVhumDxaxlnNAg954+f8WWXE1rQ==} + '@storybook/react-dom-shim@9.1.17': + resolution: {integrity: sha512-Ss/lNvAy0Ziynu+KniQIByiNuyPz3dq7tD62hqSC/pHw190X+M7TKU3zcZvXhx2AQx1BYyxtdSHIZapb+P5mxQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.16 + storybook: ^9.1.17 - '@storybook/react-vite@9.1.16': - resolution: {integrity: sha512-WRKSq0XfQ/Qx66aKisQCfa/1UKwN9HjVbY6xrmsX7kI5zBdITxIcKInq6PWoPv91SJD7+Et956yX+F86R1aEXw==} + '@storybook/react-vite@9.1.17': + resolution: {integrity: sha512-RZHsqD1mnTMo4MCJw68t3swS5BTMSTpeRhlelMwjoTEe7jJCPa+qx00uMlWliR1QBN1hMO8Y1dkchxSiUS9otA==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.16 + storybook: ^9.1.17 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/react@9.1.16': - resolution: {integrity: sha512-M/SkHJJdtiGpodBJq9+DYmSkEOD+VqlPxKI+FvbHESTNs//1IgqFIjEWetd8quhd9oj/gvo4ICBAPu+UmD6M9w==} + '@storybook/react@9.1.17': + resolution: {integrity: sha512-TZCplpep5BwjHPIIcUOMHebc/2qKadJHYPisRn5Wppl014qgT3XkFLpYkFgY1BaRXtqw8Mn3gqq4M/49rQ7Iww==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.16 + storybook: ^9.1.17 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -4747,6 +5169,9 @@ packages: '@types/node@22.19.0': resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==} + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + '@types/node@24.9.2': resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} @@ -6456,6 +6881,16 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -6945,6 +7380,10 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} @@ -9768,6 +10207,11 @@ packages: peerDependencies: react: ^19.2.0 + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -9841,6 +10285,10 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -10078,6 +10526,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.53.5: + resolution: {integrity: sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -10512,8 +10965,8 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} - storybook@9.1.16: - resolution: {integrity: sha512-339U14K6l46EFyRvaPS2ZlL7v7Pb+LlcXT8KAETrGPxq8v1sAjj2HAOB6zrlAK3M+0+ricssfAwsLCwt7Eg8TQ==} + storybook@9.1.17: + resolution: {integrity: sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -11311,6 +11764,46 @@ packages: yaml: optional: true + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -11638,8 +12131,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.2.1: - resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} yup@1.6.1: @@ -11798,6 +12291,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@ant-design/cssinjs-utils@1.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@ant-design/cssinjs': 1.24.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@babel/runtime': 7.28.4 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@ant-design/cssinjs@1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 @@ -11810,6 +12311,18 @@ snapshots: react-dom: 19.2.0(react@19.2.0) stylis: 4.3.6 + '@ant-design/cssinjs@1.24.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/hash': 0.8.0 + '@emotion/unitless': 0.7.5 + classnames: 2.5.1 + csstype: 3.1.3 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + stylis: 4.3.6 + '@ant-design/fast-color@2.0.6': dependencies: '@babel/runtime': 7.28.4 @@ -11828,6 +12341,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@ant-design/icons@5.6.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@ant-design/colors': 7.2.1 + '@ant-design/icons-svg': 4.4.2 + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@ant-design/icons@6.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@ant-design/colors': 8.0.0 @@ -11846,6 +12369,15 @@ snapshots: resize-observer-polyfill: 1.5.1 throttle-debounce: 5.0.2 + '@ant-design/react-slick@1.1.2(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + json2mq: 0.2.0 + react: 19.2.3 + resize-observer-polyfill: 1.5.1 + throttle-debounce: 5.0.2 + '@ant-design/v5-patch-for-react-19@1.0.3(antd@5.27.6(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: antd: 5.27.6(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -13039,13 +13571,25 @@ snapshots: '@biomejs/cli-win32-x64@2.0.0': optional: true - '@chromatic-com/storybook@4.1.2(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@chromatic-com/storybook@4.1.2(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 12.2.0 + filesize: 10.1.6 + jsonfile: 6.2.0 + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + strip-ansi: 7.1.2 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + + '@chromatic-com/storybook@4.1.2(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) strip-ansi: 7.1.2 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -14297,81 +14841,237 @@ snapshots: '@esbuild/aix-ppc64@0.25.11': optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + '@esbuild/android-arm64@0.25.11': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + '@esbuild/android-arm@0.25.11': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + '@esbuild/android-x64@0.25.11': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + '@esbuild/darwin-arm64@0.25.11': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + '@esbuild/darwin-x64@0.25.11': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + '@esbuild/freebsd-arm64@0.25.11': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + '@esbuild/freebsd-x64@0.25.11': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + '@esbuild/linux-arm64@0.25.11': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + '@esbuild/linux-arm@0.25.11': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + '@esbuild/linux-ia32@0.25.11': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + '@esbuild/linux-loong64@0.25.11': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + '@esbuild/linux-mips64el@0.25.11': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + '@esbuild/linux-ppc64@0.25.11': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + '@esbuild/linux-riscv64@0.25.11': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + '@esbuild/linux-s390x@0.25.11': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + '@esbuild/linux-x64@0.25.11': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + '@esbuild/netbsd-arm64@0.25.11': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + '@esbuild/netbsd-x64@0.25.11': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + '@esbuild/openbsd-arm64@0.25.11': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + '@esbuild/openbsd-x64@0.25.11': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + '@esbuild/openharmony-arm64@0.25.11': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + '@esbuild/sunos-x64@0.25.11': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + '@esbuild/win32-arm64@0.25.11': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + '@esbuild/win32-ia32@0.25.11': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + '@esbuild/win32-x64@0.25.11': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.38.0(jiti@2.6.1))': dependencies: eslint: 9.38.0(jiti@2.6.1) @@ -14626,7 +15326,7 @@ snapshots: '@graphql-tools/utils': 8.9.0(graphql@16.11.0) dataloader: 2.1.0 graphql: 16.11.0 - tslib: 2.8.1 + tslib: 2.4.1 value-or-promise: 1.0.11 '@graphql-tools/batch-execute@9.0.19(graphql@16.11.0)': @@ -14841,7 +15541,7 @@ snapshots: '@graphql-tools/optimize@2.0.0(graphql@16.11.0)': dependencies: graphql: 16.11.0 - tslib: 2.8.1 + tslib: 2.6.3 '@graphql-tools/prisma-loader@8.0.17(@types/node@24.9.2)(graphql@16.11.0)': dependencies: @@ -14877,7 +15577,7 @@ snapshots: '@ardatan/relay-compiler': 12.0.3(graphql@16.11.0) '@graphql-tools/utils': 10.9.1(graphql@16.11.0) graphql: 16.11.0 - tslib: 2.8.1 + tslib: 2.6.3 transitivePeerDependencies: - encoding @@ -15012,12 +15712,21 @@ snapshots: '@types/yargs': 17.0.34 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - glob: 10.4.5 + glob: 10.5.0 magic-string: 0.30.21 react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + typescript: 5.8.3 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + glob: 10.5.0 + magic-string: 0.30.21 + react-docgen-typescript: 2.4.0(typescript@5.8.3) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: typescript: 5.8.3 @@ -15130,6 +15839,12 @@ snapshots: '@types/react': 19.2.2 react: 19.2.0 + '@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.3)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.2 + react: 19.2.3 + '@microsoft/applicationinsights-web-snippet@1.0.1': {} '@mongodb-js/saslprep@1.3.2': @@ -15670,6 +16385,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/color-picker@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@ant-design/fast-color': 2.0.6 + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/context@1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 @@ -15677,6 +16401,13 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/context@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/mini-decimal@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -15689,6 +16420,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/mutate-observer@1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/portal@1.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 @@ -15697,6 +16436,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/portal@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/qrcode@1.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 @@ -15704,6 +16451,13 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/qrcode@1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/tour@1.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 @@ -15714,6 +16468,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/tour@1.15.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/trigger@2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 @@ -15725,6 +16489,17 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@rc-component/trigger@2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@rc-component/util@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: is-mobile: 5.0.0 @@ -15736,80 +16511,146 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/pluginutils@5.3.0(rollup@4.52.5)': + '@rollup/pluginutils@5.3.0(rollup@4.53.5)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.5 + rollup: 4.53.5 '@rollup/rollup-android-arm-eabi@4.52.5': optional: true + '@rollup/rollup-android-arm-eabi@4.53.5': + optional: true + '@rollup/rollup-android-arm64@4.52.5': optional: true + '@rollup/rollup-android-arm64@4.53.5': + optional: true + '@rollup/rollup-darwin-arm64@4.52.5': optional: true + '@rollup/rollup-darwin-arm64@4.53.5': + optional: true + '@rollup/rollup-darwin-x64@4.52.5': optional: true + '@rollup/rollup-darwin-x64@4.53.5': + optional: true + '@rollup/rollup-freebsd-arm64@4.52.5': optional: true + '@rollup/rollup-freebsd-arm64@4.53.5': + optional: true + '@rollup/rollup-freebsd-x64@4.52.5': optional: true + '@rollup/rollup-freebsd-x64@4.53.5': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.53.5': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.53.5': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-arm64-gnu@4.53.5': + optional: true + '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true + '@rollup/rollup-linux-arm64-musl@4.53.5': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-loong64-gnu@4.53.5': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.53.5': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.53.5': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.52.5': optional: true + '@rollup/rollup-linux-riscv64-musl@4.53.5': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true + '@rollup/rollup-linux-s390x-gnu@4.53.5': + optional: true + '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-x64-gnu@4.53.5': + optional: true + '@rollup/rollup-linux-x64-musl@4.52.5': optional: true + '@rollup/rollup-linux-x64-musl@4.53.5': + optional: true + '@rollup/rollup-openharmony-arm64@4.52.5': optional: true + '@rollup/rollup-openharmony-arm64@4.53.5': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.52.5': optional: true + '@rollup/rollup-win32-arm64-msvc@4.53.5': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.52.5': optional: true + '@rollup/rollup-win32-ia32-msvc@4.53.5': + optional: true + '@rollup/rollup-win32-x64-gnu@4.52.5': optional: true + '@rollup/rollup-win32-x64-gnu@4.53.5': + optional: true + '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@rollup/rollup-win32-x64-msvc@4.53.5': + optional: true + '@sendgrid/client@8.1.6': dependencies: '@sendgrid/helpers': 8.0.0 @@ -15980,54 +16821,104 @@ snapshots: - debug - react-native-b4a - '@storybook/addon-a11y@9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@storybook/addon-a11y@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@storybook/addon-docs@9.1.16(@types/react@19.2.2)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@storybook/addon-a11y@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: - '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0) - '@storybook/csf-plugin': 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) - '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@storybook/global': 5.0.0 + axe-core: 4.11.0 + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + + '@storybook/addon-docs@9.1.17(@types/react@19.2.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.3) + '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/addon-docs@9.1.17(@types/react@19.2.2)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.3) + '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@storybook/addon-onboarding@9.1.16(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + + '@storybook/addon-onboarding@9.1.16(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@storybook/addon-vitest@9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4)': + '@storybook/addon-vitest@9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) prompts: 2.4.2 - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@storybook/addon-vitest@9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4)': dependencies: - '@storybook/csf-plugin': 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@storybook/global': 5.0.0 + '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + prompts: 2.4.2 + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) ts-dedent: 2.2.0 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/runner': 3.2.4 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/builder-vite@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + ts-dedent: 2.2.0 + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - '@storybook/csf-plugin@9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@storybook/builder-vite@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@storybook/csf-plugin': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + ts-dedent: 2.2.0 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + '@storybook/csf-plugin@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + unplugin: 1.16.1 + + '@storybook/csf-plugin@9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -16037,39 +16928,86 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@storybook/icons@1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@storybook/react-dom-shim@9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@storybook/react-dom-shim@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - '@storybook/builder-vite': 9.1.16(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - '@storybook/react': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + + '@storybook/react-dom-shim@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + + '@storybook/react-vite@9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.53.5)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@rollup/pluginutils': 5.3.0(rollup@4.53.5) + '@storybook/builder-vite': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@storybook/react': 9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.21 react: 19.2.0 react-docgen: 8.0.2 react-dom: 19.2.0(react@19.2.0) resolve: 1.22.11 - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) tsconfig-paths: 4.2.0 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + '@storybook/react-vite@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.5)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@rollup/pluginutils': 5.3.0(rollup@4.53.5) + '@storybook/builder-vite': 9.1.17(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@storybook/react': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3) + find-up: 7.0.0 + magic-string: 0.30.21 + react: 19.2.3 + react-docgen: 8.0.2 + react-dom: 19.2.3(react@19.2.3) + resolve: 1.22.11 + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + tsconfig-paths: 4.2.0 + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)': + '@storybook/react@9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + optionalDependencies: + typescript: 5.8.3 + + '@storybook/react@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.8.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) optionalDependencies: typescript: 5.8.3 @@ -16401,6 +17339,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.4': + dependencies: + undici-types: 7.16.0 + '@types/node@24.9.2': dependencies: undici-types: 7.16.0 @@ -16622,7 +17564,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -16630,20 +17572,20 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) ws: 8.18.3 optionalDependencies: playwright: 1.56.1 @@ -16652,7 +17594,6 @@ snapshots: - msw - utf-8-validate - vite - optional: true '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': dependencies: @@ -16672,6 +17613,46 @@ snapshots: - msw - utf-8-validate - vite + optional: true + + '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.3.0(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/utils': 3.2.4 + magic-string: 0.30.21 + sirv: 3.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + ws: 8.18.3 + optionalDependencies: + playwright: 1.56.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/utils': 3.2.4 + magic-string: 0.30.21 + sirv: 3.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + ws: 8.18.3 + optionalDependencies: + playwright: 1.56.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: @@ -16710,6 +17691,14 @@ snapshots: optionalDependencies: vite: 7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -16718,6 +17707,23 @@ snapshots: optionalDependencies: vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.0(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + optional: true + + '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -17044,6 +18050,64 @@ snapshots: - luxon - moment + antd@5.27.6(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@ant-design/colors': 7.2.1 + '@ant-design/cssinjs': 1.24.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/cssinjs-utils': 1.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/fast-color': 2.0.6 + '@ant-design/icons': 5.6.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/react-slick': 1.1.2(react@19.2.3) + '@babel/runtime': 7.28.4 + '@rc-component/color-picker': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/mutate-observer': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/qrcode': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/tour': 1.15.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + copy-to-clipboard: 3.3.3 + dayjs: 1.11.18 + rc-cascader: 3.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-checkbox: 3.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-collapse: 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-dialog: 9.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-drawer: 7.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-dropdown: 4.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-field-form: 2.7.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-image: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-input: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-input-number: 9.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-mentions: 2.20.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-menu: 9.16.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-notification: 5.6.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-pagination: 5.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-picker: 4.11.3(dayjs@1.11.18)(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-progress: 4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-rate: 2.13.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-segmented: 2.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-select: 14.16.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-slider: 11.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-steps: 6.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-switch: 4.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-table: 7.54.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-tabs: 15.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-textarea: 1.10.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-tooltip: 6.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-tree: 5.13.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-tree-select: 5.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-upload: 4.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + scroll-into-view-if-needed: 3.1.0 + throttle-debounce: 5.0.2 + transitivePeerDependencies: + - date-fns + - luxon + - moment + any-promise@1.3.0: {} anymatch@3.1.3: @@ -17501,7 +18565,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.8.1 + tslib: 2.6.3 camelcase@5.0.0: {} @@ -17588,7 +18652,7 @@ snapshots: path-case: 3.0.4 sentence-case: 3.0.4 snake-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 char-regex@1.0.2: {} @@ -17816,7 +18880,7 @@ snapshots: constant-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 upper-case: 2.0.2 content-disposition@0.5.2: {} @@ -18303,7 +19367,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 dot-prop@6.0.1: dependencies: @@ -18485,10 +19549,10 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild-register@3.6.0(esbuild@0.25.11): + esbuild-register@3.6.0(esbuild@0.25.12): dependencies: debug: 4.4.3(supports-color@8.1.1) - esbuild: 0.25.11 + esbuild: 0.25.12 transitivePeerDependencies: - supports-color @@ -18521,6 +19585,64 @@ snapshots: '@esbuild/win32-ia32': 0.25.11 '@esbuild/win32-x64': 0.25.11 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-goat@4.0.0: {} @@ -19122,6 +20244,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@11.0.3: dependencies: foreground-child: 3.3.1 @@ -19417,7 +20548,7 @@ snapshots: header-case@2.0.4: dependencies: capital-case: 1.0.4 - tslib: 2.8.1 + tslib: 2.6.3 history@4.10.1: dependencies: @@ -19797,7 +20928,7 @@ snapshots: is-lower-case@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 is-map@2.0.3: {} @@ -19880,7 +21011,7 @@ snapshots: is-upper-case@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 is-weakmap@2.0.2: {} @@ -20343,11 +21474,11 @@ snapshots: lower-case-first@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 lower-case@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 lowercase-keys@3.0.0: {} @@ -21203,7 +22334,7 @@ snapshots: normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.3 + semver: 7.7.1 validate-npm-package-license: 3.0.4 normalize-path@2.1.1: @@ -21367,7 +22498,7 @@ snapshots: p-limit@4.0.0: dependencies: - yocto-queue: 1.2.1 + yocto-queue: 1.2.2 p-locate@4.1.0: dependencies: @@ -21420,7 +22551,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 parent-module@1.0.1: dependencies: @@ -21473,14 +22604,14 @@ snapshots: pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 path-browserify@1.0.1: {} path-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 path-exists@4.0.0: {} @@ -22137,14 +23268,32 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - rc-checkbox@3.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + rc-cascader@3.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 classnames: 2.5.1 - rc-util: 5.44.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + rc-select: 14.16.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-tree: 5.13.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + rc-checkbox@3.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-checkbox@3.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-collapse@3.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22154,6 +23303,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-collapse@3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-dialog@9.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22164,6 +23322,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-dialog@9.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-drawer@7.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22174,6 +23342,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-drawer@7.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-dropdown@4.2.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22183,6 +23361,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-dropdown@4.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-field-form@2.7.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22191,6 +23378,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-field-form@2.7.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/async-validator': 5.0.4 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-image@7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22202,6 +23397,17 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-image@7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-dialog: 9.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-input-number@9.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22212,6 +23418,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-input-number@9.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/mini-decimal': 1.1.0 + classnames: 2.5.1 + rc-input: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-input@1.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22220,6 +23436,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-input@1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-mentions@2.20.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22232,6 +23456,18 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-mentions@2.20.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-input: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-menu: 9.16.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-textarea: 1.10.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-menu@9.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22243,6 +23479,17 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-menu@9.16.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-overflow: 1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-motion@2.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22251,6 +23498,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-motion@2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-notification@5.6.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22260,6 +23515,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-notification@5.6.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-overflow@1.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22269,6 +23533,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-overflow@1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-pagination@5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22277,6 +23550,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-pagination@5.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-picker@4.11.3(dayjs@1.11.18)(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22292,6 +23573,21 @@ snapshots: luxon: 3.6.1 moment: 2.30.1 + rc-picker@4.11.3(dayjs@1.11.18)(luxon@3.6.1)(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-overflow: 1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + dayjs: 1.11.18 + luxon: 3.6.1 + moment: 2.30.1 + rc-progress@4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22300,6 +23596,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-progress@4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-rate@2.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22308,6 +23612,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-rate@2.13.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-resize-observer@1.4.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22317,6 +23629,15 @@ snapshots: react-dom: 19.2.0(react@19.2.0) resize-observer-polyfill: 1.5.1 + rc-resize-observer@1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + resize-observer-polyfill: 1.5.1 + rc-segmented@2.7.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22326,6 +23647,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-segmented@2.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-select@14.16.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22338,6 +23668,18 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-select@14.16.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-overflow: 1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-virtual-list: 3.19.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-slider@11.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22346,6 +23688,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-slider@11.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-steps@6.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22354,6 +23704,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-steps@6.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-switch@4.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22362,6 +23720,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-switch@4.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-table@7.54.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22373,6 +23739,17 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-table@7.54.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/context': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-virtual-list: 3.19.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-tabs@15.7.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22385,6 +23762,18 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-tabs@15.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-dropdown: 4.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-menu: 9.16.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-textarea@1.10.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22395,6 +23784,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-textarea@1.10.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-input: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-tooltip@6.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22404,6 +23803,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-tooltip@6.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-tree-select@5.27.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22414,6 +23822,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-tree-select@5.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-select: 14.16.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-tree: 5.13.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-tree@5.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22424,6 +23842,16 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-tree@5.13.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-virtual-list: 3.19.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-upload@4.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22432,6 +23860,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-upload@4.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc-util@5.44.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22439,6 +23875,13 @@ snapshots: react-dom: 19.2.0(react@19.2.0) react-is: 18.3.1 + rc-util@5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 18.3.1 + rc-virtual-list@3.19.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22448,6 +23891,15 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + rc-virtual-list@3.19.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -22479,6 +23931,11 @@ snapshots: react: 19.2.0 scheduler: 0.27.0 + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + react-fast-compare@3.2.2: {} react-is@16.13.1: {} @@ -22502,6 +23959,11 @@ snapshots: oidc-client-ts: 3.3.0 react: 19.2.0 + react-oidc-context@3.3.0(oidc-client-ts@3.3.0)(react@19.2.3): + dependencies: + oidc-client-ts: 3.3.0 + react: 19.2.3 + react-refresh@0.17.0: {} react-router-config@5.1.1(react-router@5.3.4(react@19.2.0))(react@19.2.0): @@ -22527,6 +23989,12 @@ snapshots: react-dom: 19.2.0(react@19.2.0) react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router-dom@7.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-router: 7.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-router@5.3.4(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 @@ -22548,8 +24016,18 @@ snapshots: optionalDependencies: react-dom: 19.2.0(react@19.2.0) + react-router@7.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + cookie: 1.0.2 + react: 19.2.3 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react@19.2.0: {} + react@19.2.3: {} + read-package-up@11.0.0: dependencies: find-up-simple: 1.0.1 @@ -22903,6 +24381,34 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + rollup@4.53.5: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.5 + '@rollup/rollup-android-arm64': 4.53.5 + '@rollup/rollup-darwin-arm64': 4.53.5 + '@rollup/rollup-darwin-x64': 4.53.5 + '@rollup/rollup-freebsd-arm64': 4.53.5 + '@rollup/rollup-freebsd-x64': 4.53.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.5 + '@rollup/rollup-linux-arm-musleabihf': 4.53.5 + '@rollup/rollup-linux-arm64-gnu': 4.53.5 + '@rollup/rollup-linux-arm64-musl': 4.53.5 + '@rollup/rollup-linux-loong64-gnu': 4.53.5 + '@rollup/rollup-linux-ppc64-gnu': 4.53.5 + '@rollup/rollup-linux-riscv64-gnu': 4.53.5 + '@rollup/rollup-linux-riscv64-musl': 4.53.5 + '@rollup/rollup-linux-s390x-gnu': 4.53.5 + '@rollup/rollup-linux-x64-gnu': 4.53.5 + '@rollup/rollup-linux-x64-musl': 4.53.5 + '@rollup/rollup-openharmony-arm64': 4.53.5 + '@rollup/rollup-win32-arm64-msvc': 4.53.5 + '@rollup/rollup-win32-ia32-msvc': 4.53.5 + '@rollup/rollup-win32-x64-gnu': 4.53.5 + '@rollup/rollup-win32-x64-msvc': 4.53.5 + fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} rtlcss@4.3.0: @@ -23035,7 +24541,7 @@ snapshots: sentence-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 upper-case-first: 2.0.2 seq-queue@0.0.5: {} @@ -23245,7 +24751,7 @@ snapshots: snake-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.3 snyk@1.1301.0: dependencies: @@ -23318,7 +24824,7 @@ snapshots: sponge-case@1.0.1: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 sprintf-js@1.0.3: {} @@ -23355,17 +24861,39 @@ snapshots: stoppable@1.1.0: {} - storybook@9.1.16(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 - esbuild: 0.25.11 - esbuild-register: 3.6.0(esbuild@0.25.11) + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + recast: 0.23.11 + semver: 7.7.3 + ws: 8.18.3 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - msw + - supports-color + - utf-8-validate + - vite + + storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) recast: 0.23.11 semver: 7.7.3 ws: 8.18.3 @@ -23555,7 +25083,7 @@ snapshots: swap-case@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 symbol-tree@3.2.4: {} @@ -23667,7 +25195,7 @@ snapshots: title-case@3.0.3: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 tldts-core@6.1.86: {} @@ -23748,7 +25276,7 @@ snapshots: '@ts-morph/common': 0.27.0 code-block-writer: 13.0.3 - ts-node-dev@2.0.0(@types/node@24.9.2)(typescript@5.8.3): + ts-node-dev@2.0.0(@types/node@24.10.4)(typescript@5.8.3): dependencies: chokidar: 3.6.0 dynamic-dedupe: 0.3.0 @@ -23758,7 +25286,7 @@ snapshots: rimraf: 2.7.1 source-map-support: 0.5.21 tree-kill: 1.2.2 - ts-node: 10.9.2(@types/node@24.9.2)(typescript@5.8.3) + ts-node: 10.9.2(@types/node@24.10.4)(typescript@5.8.3) tsconfig: 7.0.0 typescript: 5.8.3 transitivePeerDependencies: @@ -23766,14 +25294,14 @@ snapshots: - '@swc/wasm' - '@types/node' - ts-node@10.9.2(@types/node@24.9.2)(typescript@5.8.3): + ts-node@10.9.2(@types/node@24.10.4)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.9.2 + '@types/node': 24.10.4 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -24060,11 +25588,11 @@ snapshots: upper-case-first@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 upper-case@2.0.2: dependencies: - tslib: 2.8.1 + tslib: 2.6.3 uri-js@4.4.1: dependencies: @@ -24163,6 +25691,27 @@ snapshots: - tsx - yaml + vite-node@3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -24201,6 +25750,23 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 @@ -24218,6 +25784,41 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vite@7.3.0(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + optional: true + + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 @@ -24246,7 +25847,51 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.19.0 - '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.3.0(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.12(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.10.4 + '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -24674,7 +26319,7 @@ snapshots: yocto-queue@0.1.0: {} - yocto-queue@1.2.1: {} + yocto-queue@1.2.2: {} yup@1.6.1: dependencies: From fccbca6672f60cb8b67414420288c981f6d30996 Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 15:34:34 -0500 Subject: [PATCH 07/20] removed unused exported types --- apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts index e7f7297ef..0f9d0fe09 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts @@ -7,8 +7,8 @@ export const POPCONFIRM_SELECTORS = { cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', } as const; -export type Canvas = ReturnType; -export type PopconfirmAction = 'confirm' | 'cancel'; +type Canvas = ReturnType; +type PopconfirmAction = 'confirm' | 'cancel'; export const canvasUtils = { getButtons: (canvas: Canvas) => canvas.getAllByRole('button'), From c00d0287180e66f32451c358b1f7238ba7c7b3b7 Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 16:26:30 -0500 Subject: [PATCH 08/20] resolve sonarqube flagging low issues --- .../popconfirm-test-utils.stories.tsx | 35 +++++++++++++++++++ .../src/test-utils/popconfirm-test-utils.ts | 22 +++++------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx index 5c585fe71..ae9e888fa 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx @@ -222,3 +222,38 @@ export const PopconfirmSelectorsExist: Story = { ); }, }; + +export const ConfirmPopconfirmWhenNoButton: Story = { + args: {}, + play: async () => { + // Call confirmPopconfirm when no popconfirm is open + const result = await confirmPopconfirm(); + expect(result).toBeNull(); + }, +}; + +export const CancelPopconfirmWhenNoButton: Story = { + args: {}, + play: async () => { + // Call cancelPopconfirm when no popconfirm is open + const result = await cancelPopconfirm(); + expect(result).toBeNull(); + }, +}; + +export const TriggerPopconfirmWithButtonIndex: Story = { + args: { + onConfirm: fn(), + showMultipleButtons: false, + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + // Trigger the first button (index 0) which has the Popconfirm + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonIndex: 0, + }); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts index 0f9d0fe09..23e5dc592 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts @@ -35,20 +35,16 @@ export const waitForPopconfirm = async () => export const getPopconfirmElements = () => ({ title: document.querySelector(POPCONFIRM_SELECTORS.title), description: document.querySelector(POPCONFIRM_SELECTORS.description), - confirmButton: document.querySelector( - POPCONFIRM_SELECTORS.confirmButton, - ) as HTMLElement | null, - cancelButton: document.querySelector( - POPCONFIRM_SELECTORS.cancelButton, - ) as HTMLElement | null, + confirmButton: document.querySelector(POPCONFIRM_SELECTORS.confirmButton), + cancelButton: document.querySelector(POPCONFIRM_SELECTORS.cancelButton), }); export const confirmPopconfirm = async () => { const confirmButton = document.querySelector( POPCONFIRM_SELECTORS.confirmButton, - ) as HTMLElement | null; + ); if (confirmButton) { - await userEvent.click(confirmButton); + await userEvent.click(confirmButton as HTMLElement); } return confirmButton; }; @@ -56,9 +52,9 @@ export const confirmPopconfirm = async () => { export const cancelPopconfirm = async () => { const cancelButton = document.querySelector( POPCONFIRM_SELECTORS.cancelButton, - ) as HTMLElement | null; + ); if (cancelButton) { - await userEvent.click(cancelButton); + await userEvent.click(cancelButton as HTMLElement); } return cancelButton; }; @@ -120,14 +116,12 @@ export const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { const confirmButton = await waitFor( () => { - const btn = document.querySelector( - POPCONFIRM_SELECTORS.confirmButton, - ) as HTMLElement | null; + const btn = document.querySelector(POPCONFIRM_SELECTORS.confirmButton); if (!btn) throw new Error('Confirm button not found yet'); return btn; }, { timeout: 1000 }, ); - await userEvent.click(confirmButton); + await userEvent.click(confirmButton as HTMLElement); }; From a6220c7306ed1982f0f344bbff23586fee04e025 Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 22:16:17 -0500 Subject: [PATCH 09/20] added additional code coverage for sonarqube and addressed sourcery comments --- .../listing-information.container.tsx | 3 + .../components/reservation-actions.tsx | 60 +++---- .../stories/reservation-actions.stories.tsx | 157 ++++++++---------- .../popconfirm-test-utils.stories.tsx | 2 +- .../reservation-request.resolvers.feature | 7 +- .../reservation-request.resolvers.test.ts | 56 +++++++ 6 files changed, 169 insertions(+), 116 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx index 566ef0ae3..ed746514d 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.tsx @@ -123,6 +123,9 @@ export const ListingInformationContainer: React.FC< }; const handleCancelClick = async () => { + if (cancelLoading) { + return; + } if (!userReservationRequest?.id) { message.error('No reservation request to cancel'); return; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx index 15e58b5a0..cf26467f5 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx @@ -3,6 +3,28 @@ import { Space, Popconfirm } from 'antd'; import { ReservationActionButton } from './reservation-action-button.tsx'; import type { ReservationActionStatus } from '../utils/reservation-status.utils.ts'; +interface CancelWithPopconfirmProps { + onConfirm?: () => void; + loading?: boolean; +} + +const CancelWithPopconfirm: React.FC = ({ + onConfirm, + loading, +}) => ( + + + + + +); + interface ReservationActionsProps { status: ReservationActionStatus; onCancel?: () => void; @@ -24,22 +46,11 @@ export const ReservationActions: React.FC = ({ switch (status) { case 'REQUESTED': return [ - - - - - , + loading={cancelLoading} + />, = ({ case 'REJECTED': return [ - - - - - , + loading={cancelLoading} + />, ]; default: // No actions for cancelled or closed reservations diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index c56c96a27..40221c7fa 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -16,6 +16,50 @@ const expectNoButtons = (canvas: Canvas) => { expect(buttons.length).toBe(0); }; +const openCancelPopconfirm = async (canvas: Canvas) => { + const cancelButton = getFirstButton(canvas); + expect(cancelButton).toBeTruthy(); + + if (!cancelButton) return null; + + await userEvent.click(cancelButton); + await waitForPopconfirm(); + + return getPopconfirmElements(); +}; + +const confirmPopconfirmAction = async ( + elements: ReturnType, +) => { + const { confirmButton } = elements; + if (confirmButton) { + await userEvent.click(confirmButton); + } +}; + +const cancelPopconfirmAction = async ( + elements: ReturnType, +) => { + const { cancelButton } = elements; + if (cancelButton) { + await userEvent.click(cancelButton); + } +}; + +const playExpectNoButtons: Story['play'] = ({ canvasElement }) => { + const canvas = within(canvasElement); + expectNoButtons(canvas); +}; + +const playLoadingState: Story['play'] = ({ canvasElement }) => { + const canvas = within(canvasElement); + const buttons = getButtons(canvas); + expect(buttons.length).toBeGreaterThan(0); + + const primaryButton = getFirstButton(canvas); + expect(primaryButton).toBeTruthy(); +}; + const meta: Meta = { title: 'Molecules/ReservationActions', component: ReservationActions, @@ -90,17 +134,15 @@ export const ButtonInteraction: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - // Get all buttons + // Guard against assumptions about button ordering by asserting count + // and selecting the message button via its accessible name. const buttons = getButtons(canvas); - expect(buttons.length).toBeGreaterThan(0); + expect(buttons.length).toBeGreaterThan(1); - // Click the message button (second button in REQUESTED state - first is cancel with Popconfirm) - const messageButton = buttons[1]; - if (messageButton) { - await userEvent.click(messageButton); - // Verify the message callback was called (message button doesn't have Popconfirm) - expect(args.onMessage).toHaveBeenCalled(); - } + const messageButton = canvas.getByRole('button', { name: /message/i }); + await userEvent.click(messageButton); + // Verify the message callback was called (message button doesn't have Popconfirm) + expect(args.onMessage).toHaveBeenCalled(); }, }; @@ -152,23 +194,15 @@ export const RequestedWithPopconfirm: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const cancelButton = getFirstButton(canvas); - expect(cancelButton).toBeTruthy(); - - if (cancelButton) { - await userEvent.click(cancelButton); - await waitForPopconfirm(); - - const { title, description, confirmButton } = getPopconfirmElements(); + const elements = await openCancelPopconfirm(canvas); + if (!elements) return; - expect(title?.textContent).toContain('Cancel Reservation Request'); - expect(description?.textContent).toContain('Are you sure'); + const { title, description } = elements; + expect(title?.textContent).toContain('Cancel Reservation Request'); + expect(description?.textContent).toContain('Are you sure'); - if (confirmButton) { - await userEvent.click(confirmButton); - expect(args.onCancel).toHaveBeenCalled(); - } - } + await confirmPopconfirmAction(elements); + expect(args.onCancel).toHaveBeenCalled(); }, }; @@ -181,20 +215,11 @@ export const PopconfirmCancelAction: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const cancelButton = getFirstButton(canvas); - expect(cancelButton).toBeTruthy(); - - if (cancelButton) { - await userEvent.click(cancelButton); - await waitForPopconfirm(); - - const { cancelButton: cancelPopconfirmButton } = getPopconfirmElements(); + const elements = await openCancelPopconfirm(canvas); + if (!elements) return; - if (cancelPopconfirmButton) { - await userEvent.click(cancelPopconfirmButton); - expect(args.onCancel).not.toHaveBeenCalled(); - } - } + await cancelPopconfirmAction(elements); + expect(args.onCancel).not.toHaveBeenCalled(); }, }; @@ -212,23 +237,15 @@ export const RejectedWithCancel: Story = { const buttons = getButtons(canvas); expect(buttons.length).toBe(1); - const cancelButton = getFirstButton(canvas); - expect(cancelButton).toBeTruthy(); - - if (cancelButton) { - await userEvent.click(cancelButton); - await waitForPopconfirm(); - - const { title, description, confirmButton } = getPopconfirmElements(); + const elements = await openCancelPopconfirm(canvas); + if (!elements) return; - expect(title?.textContent).toContain('Cancel Reservation Request'); - expect(description?.textContent).toContain('Are you sure'); + const { title, description } = elements; + expect(title?.textContent).toContain('Cancel Reservation Request'); + expect(description?.textContent).toContain('Are you sure'); - if (confirmButton) { - await userEvent.click(confirmButton); - expect(args.onCancel).toHaveBeenCalled(); - } - } + await confirmPopconfirmAction(elements); + expect(args.onCancel).toHaveBeenCalled(); }, }; @@ -239,11 +256,7 @@ export const CancelledNoActions: Story = { onClose: fn(), onMessage: fn(), }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - - expectNoButtons(canvas); - }, + play: playExpectNoButtons, }; export const ClosedNoActions: Story = { @@ -253,11 +266,7 @@ export const ClosedNoActions: Story = { onClose: fn(), onMessage: fn(), }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - - expectNoButtons(canvas); - }, + play: playExpectNoButtons, }; export const AcceptedActions: Story = { @@ -285,17 +294,7 @@ export const CancelLoadingState: Story = { onMessage: fn(), cancelLoading: true, }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify loading state renders buttons - const buttons = getButtons(canvas); - expect(buttons.length).toBeGreaterThan(0); - - // Verify cancel button is present with loading state - const cancelButton = getFirstButton(canvas); - expect(cancelButton).toBeTruthy(); - }, + play: playLoadingState, }; export const CloseLoadingState: Story = { @@ -305,15 +304,5 @@ export const CloseLoadingState: Story = { onMessage: fn(), closeLoading: true, }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify loading state renders buttons - const buttons = getButtons(canvas); - expect(buttons.length).toBeGreaterThan(0); - - // Verify close button is present with loading state - const closeButton = getFirstButton(canvas); - expect(closeButton).toBeTruthy(); - }, + play: playLoadingState, }; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx index ae9e888fa..d2bcdf73e 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx @@ -170,7 +170,7 @@ export const CancelPopconfirmSuccess: Story = { await waitForPopconfirm(); await cancelPopconfirm(); - // onConfirm should NOT be called when cancelling + expect(args.onCancel).toHaveBeenCalled(); expect(args.onConfirm).not.toHaveBeenCalled(); }, }; diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature b/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature index 970a29f84..27bbe24a4 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature @@ -151,4 +151,9 @@ So that I can view my reservations and make new ones through the GraphQL API Scenario: Cancel reservation without authentication Given an unauthenticated user When cancelReservation mutation is called - Then an authentication error should be thrown \ No newline at end of file + Then an authentication error should be thrown + + Scenario: Cancel reservation when user not found + Given an authenticated user whose email does not exist in the database + When cancelReservation mutation is called + Then a 'User not found' error should be thrown \ No newline at end of file diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts index bd6b7436a..6c24a4875 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts @@ -1241,4 +1241,60 @@ test.for(feature, ({ Scenario }) => { }); }, ); + + Scenario( + 'Cancel reservation when user not found', + ({ Given, When, Then }) => { + Given( + 'an authenticated user whose email does not exist in the database', + () => { + context = { + applicationServices: { + verifiedUser: { + verifiedJwt: { + sub: 'user-123', + email: 'nonexistent@example.com', + }, + }, + User: { + PersonalUser: { + queryByEmail: vi.fn().mockResolvedValue(null), + }, + AdminUser: { + queryByEmail: vi.fn().mockResolvedValue(null), + }, + }, + ReservationRequest: { + ReservationRequest: { + cancel: vi.fn(), + }, + }, + }, + } as never; + }, + ); + + When('cancelReservation mutation is called', async () => { + const resolver = reservationRequestResolvers.Mutation + ?.cancelReservation as TestResolver<{ + input: { id: string }; + }>; + try { + await resolver( + {}, + { input: { id: 'res-123' } }, + context, + {} as never, + ); + } catch (err) { + error = err as Error; + } + }); + + Then("a 'User not found' error should be thrown", () => { + expect(error).toBeDefined(); + expect((error as Error).message).toBe('User not found'); + }); + }, + ); }); From 2da502abf34a515cdc8ce2e6d346a84e323dc5e8 Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 18 Dec 2025 23:34:37 -0500 Subject: [PATCH 10/20] remove test-utils stories file --- .../stories/reservation-actions.stories.tsx | 159 ++++------- .../popconfirm-test-utils.stories.tsx | 259 ------------------ 2 files changed, 53 insertions(+), 365 deletions(-) delete mode 100644 apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index 40221c7fa..d56b17d26 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -3,8 +3,7 @@ import { ReservationActions } from '../components/reservation-actions.js'; import { expect, fn, userEvent, within } from 'storybook/test'; import { canvasUtils, - waitForPopconfirm, - getPopconfirmElements, + triggerPopconfirmAnd, } from '../../../../../test-utils/popconfirm-test-utils.ts'; type Canvas = ReturnType; @@ -16,36 +15,6 @@ const expectNoButtons = (canvas: Canvas) => { expect(buttons.length).toBe(0); }; -const openCancelPopconfirm = async (canvas: Canvas) => { - const cancelButton = getFirstButton(canvas); - expect(cancelButton).toBeTruthy(); - - if (!cancelButton) return null; - - await userEvent.click(cancelButton); - await waitForPopconfirm(); - - return getPopconfirmElements(); -}; - -const confirmPopconfirmAction = async ( - elements: ReturnType, -) => { - const { confirmButton } = elements; - if (confirmButton) { - await userEvent.click(confirmButton); - } -}; - -const cancelPopconfirmAction = async ( - elements: ReturnType, -) => { - const { cancelButton } = elements; - if (cancelButton) { - await userEvent.click(cancelButton); - } -}; - const playExpectNoButtons: Story['play'] = ({ canvasElement }) => { const canvas = within(canvasElement); expectNoButtons(canvas); @@ -56,10 +25,43 @@ const playLoadingState: Story['play'] = ({ canvasElement }) => { const buttons = getButtons(canvas); expect(buttons.length).toBeGreaterThan(0); + // Verify buttons exist (loading state should still render buttons) const primaryButton = getFirstButton(canvas); expect(primaryButton).toBeTruthy(); + + // Ant Design loading buttons have aria-busy attribute or loading class + const loadingIndicators = canvasElement.querySelectorAll( + '.ant-btn-loading, [aria-busy="true"]', + ); + expect(loadingIndicators.length).toBeGreaterThan(0); }; +// Factory function for no-actions stories +const createNoActionsStory = (status: string): Story => ({ + args: { + status, + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + }, + play: playExpectNoButtons, +}); + +// Factory function for loading state stories +const createLoadingStory = ( + status: string, + loadingProp: 'cancelLoading' | 'closeLoading', +): Story => ({ + args: { + status, + onCancel: fn(), + onClose: fn(), + onMessage: fn(), + [loadingProp]: true, + }, + play: playLoadingState, +}); + const meta: Meta = { title: 'Molecules/ReservationActions', component: ReservationActions, @@ -164,27 +166,6 @@ export const Cancelled: Story = { }, }; -export const LoadingStates: Story = { - args: { - status: 'REQUESTED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - cancelLoading: true, - }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify loading state is rendered - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - - // Check if any button shows loading state (might be disabled) - const disabledButtons = buttons.filter((b) => b.hasAttribute('disabled')); - expect(disabledButtons.length).toBeGreaterThanOrEqual(0); - }, -}; - export const RequestedWithPopconfirm: Story = { args: { status: 'REQUESTED', @@ -194,14 +175,11 @@ export const RequestedWithPopconfirm: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const elements = await openCancelPopconfirm(canvas); - if (!elements) return; - - const { title, description } = elements; - expect(title?.textContent).toContain('Cancel Reservation Request'); - expect(description?.textContent).toContain('Are you sure'); + await triggerPopconfirmAnd(canvas, 'confirm', { + expectedTitle: 'Cancel Reservation Request', + expectedDescription: 'Are you sure', + }); - await confirmPopconfirmAction(elements); expect(args.onCancel).toHaveBeenCalled(); }, }; @@ -215,10 +193,8 @@ export const PopconfirmCancelAction: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const elements = await openCancelPopconfirm(canvas); - if (!elements) return; + await triggerPopconfirmAnd(canvas, 'cancel'); - await cancelPopconfirmAction(elements); expect(args.onCancel).not.toHaveBeenCalled(); }, }; @@ -237,37 +213,18 @@ export const RejectedWithCancel: Story = { const buttons = getButtons(canvas); expect(buttons.length).toBe(1); - const elements = await openCancelPopconfirm(canvas); - if (!elements) return; + await triggerPopconfirmAnd(canvas, 'confirm', { + expectedTitle: 'Cancel Reservation Request', + expectedDescription: 'Are you sure', + }); - const { title, description } = elements; - expect(title?.textContent).toContain('Cancel Reservation Request'); - expect(description?.textContent).toContain('Are you sure'); - - await confirmPopconfirmAction(elements); expect(args.onCancel).toHaveBeenCalled(); }, }; -export const CancelledNoActions: Story = { - args: { - status: 'CANCELLED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, - play: playExpectNoButtons, -}; +export const CancelledNoActions: Story = createNoActionsStory('CANCELLED'); -export const ClosedNoActions: Story = { - args: { - status: 'CLOSED', - onCancel: fn(), - onClose: fn(), - onMessage: fn(), - }, - play: playExpectNoButtons, -}; +export const ClosedNoActions: Story = createNoActionsStory('CLOSED'); export const AcceptedActions: Story = { args: { @@ -287,22 +244,12 @@ export const AcceptedActions: Story = { }, }; -export const CancelLoadingState: Story = { - args: { - status: 'REQUESTED', - onCancel: fn(), - onMessage: fn(), - cancelLoading: true, - }, - play: playLoadingState, -}; +export const CancelLoadingState: Story = createLoadingStory( + 'REQUESTED', + 'cancelLoading', +); -export const CloseLoadingState: Story = { - args: { - status: 'ACCEPTED', - onClose: fn(), - onMessage: fn(), - closeLoading: true, - }, - play: playLoadingState, -}; +export const CloseLoadingState: Story = createLoadingStory( + 'ACCEPTED', + 'closeLoading', +); diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx deleted file mode 100644 index d2bcdf73e..000000000 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, within } from 'storybook/test'; -import { Button, Popconfirm, Space } from 'antd'; -import type React from 'react'; -import { - canvasUtils, - waitForPopconfirm, - getPopconfirmElements, - confirmPopconfirm, - cancelPopconfirm, - triggerPopconfirmAnd, - POPCONFIRM_SELECTORS, -} from './popconfirm-test-utils.ts'; - -interface PopconfirmTestProps { - onConfirm?: () => void; - onCancel?: () => void; - showMultipleButtons?: boolean; -} - -const PopconfirmTestComponent: React.FC = ({ - onConfirm, - onCancel, - showMultipleButtons = false, -}) => ( - - - - - {showMultipleButtons && ( - <> - - - - )} - -); - -const meta: Meta = { - title: 'Test Utilities/PopconfirmTestUtils', - component: PopconfirmTestComponent, - parameters: { - layout: 'centered', - }, -}; - -export default meta; -type Story = StoryObj; - -export const CanvasUtilsGetButtons: Story = { - args: { - showMultipleButtons: true, - }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - const buttons = canvasUtils.getButtons(canvas); - expect(buttons.length).toBe(3); - }, -}; - -export const CanvasUtilsQueryButtons: Story = { - args: { - showMultipleButtons: false, - }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - const buttons = canvasUtils.queryButtons(canvas); - expect(buttons.length).toBe(1); - }, -}; - -export const CanvasUtilsGetFirstButton: Story = { - args: { - showMultipleButtons: true, - }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - const firstButton = canvasUtils.getFirstButton(canvas); - expect(firstButton).toBeTruthy(); - expect(firstButton.textContent).toBe('Trigger Popconfirm'); - }, -}; - -export const CanvasUtilsAssertButtonCount: Story = { - args: { - showMultipleButtons: true, - }, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - canvasUtils.assertButtonCount(canvas, 3); - }, -}; - -export const CanvasUtilsAssertHasButtons: Story = { - args: {}, - play: ({ canvasElement }) => { - const canvas = within(canvasElement); - canvasUtils.assertHasButtons(canvas); - }, -}; - -export const WaitForPopconfirmSuccess: Story = { - args: { - onConfirm: fn(), - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const triggerButton = canvasUtils.getFirstButton(canvas); - await userEvent.click(triggerButton); - - const title = await waitForPopconfirm(); - expect(title).toBeTruthy(); - expect(title.textContent).toContain('Test Confirmation'); - }, -}; - -export const GetPopconfirmElementsSuccess: Story = { - args: { - onConfirm: fn(), - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const triggerButton = canvasUtils.getFirstButton(canvas); - await userEvent.click(triggerButton); - await waitForPopconfirm(); - - const elements = getPopconfirmElements(); - expect(elements.title?.textContent).toContain('Test Confirmation'); - expect(elements.description?.textContent).toContain('Are you sure'); - expect(elements.confirmButton).toBeTruthy(); - expect(elements.cancelButton).toBeTruthy(); - }, -}; - -export const ConfirmPopconfirmSuccess: Story = { - args: { - onConfirm: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const triggerButton = canvasUtils.getFirstButton(canvas); - await userEvent.click(triggerButton); - await waitForPopconfirm(); - - await confirmPopconfirm(); - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; - -export const CancelPopconfirmSuccess: Story = { - args: { - onCancel: fn(), - onConfirm: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const triggerButton = canvasUtils.getFirstButton(canvas); - await userEvent.click(triggerButton); - await waitForPopconfirm(); - - await cancelPopconfirm(); - expect(args.onCancel).toHaveBeenCalled(); - expect(args.onConfirm).not.toHaveBeenCalled(); - }, -}; - -export const TriggerPopconfirmAndConfirm: Story = { - args: { - onConfirm: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'confirm', { - expectedTitle: 'Test Confirmation', - expectedDescription: 'Are you sure', - }); - - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; - -export const TriggerPopconfirmAndCancel: Story = { - args: { - onConfirm: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'cancel', { - expectedTitle: 'Test Confirmation', - }); - - expect(args.onConfirm).not.toHaveBeenCalled(); - }, -}; - -export const PopconfirmSelectorsExist: Story = { - args: {}, - play: () => { - // Verify all selectors are defined - expect(POPCONFIRM_SELECTORS.title).toBe('.ant-popconfirm-title'); - expect(POPCONFIRM_SELECTORS.description).toBe( - '.ant-popconfirm-description', - ); - expect(POPCONFIRM_SELECTORS.confirmButton).toBe( - '.ant-popconfirm-buttons .ant-btn-primary', - ); - expect(POPCONFIRM_SELECTORS.cancelButton).toBe( - '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', - ); - }, -}; - -export const ConfirmPopconfirmWhenNoButton: Story = { - args: {}, - play: async () => { - // Call confirmPopconfirm when no popconfirm is open - const result = await confirmPopconfirm(); - expect(result).toBeNull(); - }, -}; - -export const CancelPopconfirmWhenNoButton: Story = { - args: {}, - play: async () => { - // Call cancelPopconfirm when no popconfirm is open - const result = await cancelPopconfirm(); - expect(result).toBeNull(); - }, -}; - -export const TriggerPopconfirmWithButtonIndex: Story = { - args: { - onConfirm: fn(), - showMultipleButtons: false, - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - // Trigger the first button (index 0) which has the Popconfirm - await triggerPopconfirmAnd(canvas, 'confirm', { - triggerButtonIndex: 0, - }); - - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; From 8cde1caf9e64184412bad6bb6d9c288c2ad2ddb2 Mon Sep 17 00:00:00 2001 From: Lian Date: Fri, 19 Dec 2025 09:15:15 -0500 Subject: [PATCH 11/20] addressed sourcery comments for duplication in stories/test files --- .../listing-information.stories.tsx | 81 +++--- .../components/reservation-actions.tsx | 36 ++- .../stories/reservation-actions.stories.tsx | 115 ++++---- .../src/test-utils/popconfirm-test-utils.ts | 15 +- .../reservation-request/cancel.test.ts | 271 ++++++++---------- 5 files changed, 269 insertions(+), 249 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx index 2bb1cccfb..22b4c2bff 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx @@ -2,12 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, within, userEvent, fn } from 'storybook/test'; import { ListingInformation } from './listing-information.tsx'; import { withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; -import { - POPCONFIRM_SELECTORS, - waitForPopconfirm, - confirmPopconfirm, - cancelPopconfirm, -} from '../../../../../../test-utils/popconfirm-test-utils.ts'; +import { triggerPopconfirmAnd } from '../../../../../../test-utils/popconfirm-test-utils.ts'; const baseReservationRequest = { __typename: 'ReservationRequest' as const, @@ -16,23 +11,39 @@ const baseReservationRequest = { reservationPeriodEnd: '1739145600000', }; -const createUserReservationRequest = (state: 'Requested' | 'Accepted') => ({ +type ReservationState = 'Requested' | 'Accepted'; + +const createUserReservationRequest = ( + state: ReservationState, + overrides: Partial = {}, +) => ({ ...baseReservationRequest, state, + ...overrides, }); -const openCancelRequestPopconfirm = async ( - canvas: ReturnType, +type CancelFlowOptions = { + confirm: boolean; + expectCallback: boolean; +}; + +const runCancelFlow = async ( + canvasElement: HTMLElement, + args: Record, + { confirm, expectCallback }: CancelFlowOptions, ) => { - const cancelButton = canvas.queryByRole('button', { - name: /Cancel Request/i, + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + await triggerPopconfirmAnd(canvas, confirm ? 'confirm' : 'cancel', { + triggerButtonLabel: /Cancel Request/i, }); - expect(cancelButton).toBeTruthy(); - if (cancelButton) { - await userEvent.click(cancelButton); - await waitForPopconfirm(); + + if (expectCallback) { + expect(args['onCancelClick']).toHaveBeenCalled(); + } else { + expect(args['onCancelClick']).not.toHaveBeenCalled(); } - return cancelButton; }; const mockListing = { @@ -222,12 +233,14 @@ export const ClickCancelButton: Story = { userReservationRequest: createUserReservationRequest('Requested'), }, play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - await expect(canvasElement).toBeTruthy(); - - await openCancelRequestPopconfirm(canvas); - await confirmPopconfirm(); - expect(args.onCancelClick).toHaveBeenCalled(); + await runCancelFlow( + canvasElement, + args as unknown as Record, + { + confirm: true, + expectCallback: true, + }, + ); }, }; @@ -349,12 +362,11 @@ export const CancelButtonWithPopconfirm: Story = { const canvas = within(canvasElement); await expect(canvasElement).toBeTruthy(); - await openCancelRequestPopconfirm(canvas); - - const title = document.querySelector(POPCONFIRM_SELECTORS.title); - expect(title?.textContent).toContain('Cancel Reservation Request'); + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel Request/i, + expectedTitle: 'Cancel Reservation Request', + }); - await confirmPopconfirm(); expect(args.onCancelClick).toHaveBeenCalled(); }, }; @@ -396,12 +408,13 @@ export const PopconfirmCancelButton: Story = { onCancelClick: fn(), }, play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - await expect(canvasElement).toBeTruthy(); - - await openCancelRequestPopconfirm(canvas); - await cancelPopconfirm(); - - expect(args.onCancelClick).not.toHaveBeenCalled(); + await runCancelFlow( + canvasElement, + args as unknown as Record, + { + confirm: false, + expectCallback: false, + }, + ); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx index cf26467f5..ee1e634e4 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx @@ -11,19 +11,29 @@ interface CancelWithPopconfirmProps { const CancelWithPopconfirm: React.FC = ({ onConfirm, loading, -}) => ( - - - - - -); +}) => { + const handleConfirm = () => { + if (loading) { + return; + } + onConfirm?.(); + }; + + return ( + + + + + + ); +}; interface ReservationActionsProps { status: ReservationActionStatus; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index d56b17d26..11365ecc1 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -1,33 +1,63 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ReservationActions } from '../components/reservation-actions.js'; -import { expect, fn, userEvent, within } from 'storybook/test'; +import { expect, fn, within } from 'storybook/test'; import { canvasUtils, triggerPopconfirmAnd, } from '../../../../../test-utils/popconfirm-test-utils.ts'; +import type { ReservationActionStatus } from '../utils/reservation-status.utils.ts'; type Canvas = ReturnType; -const { getButtons, queryButtons, getFirstButton } = canvasUtils; +const { getButtons, assertNoButtons, assertHasButtons, assertButtonCount } = + canvasUtils; -const expectNoButtons = (canvas: Canvas) => { - const buttons = queryButtons(canvas); - expect(buttons.length).toBe(0); +// Shared helper for button visibility assertions +const expectButtonsVisible = (canvas: Canvas, expectedCount?: number) => { + const buttons = getButtons(canvas); + expect(buttons.length).toBeGreaterThan(0); + if (expectedCount !== undefined) { + expect(buttons.length).toBe(expectedCount); + } + for (const button of buttons) { + expect(button).toBeVisible(); + } +}; + +// Shared helper for popconfirm cancel flow +type PopconfirmExpectation = { + kind: 'confirm' | 'cancel'; + expectedTitle?: string; + expectedDescription?: string; + assertCalled: (args: Record) => void; +}; + +const runCancelPopconfirmFlow = async ( + canvas: Canvas, + args: Record, + { + kind, + expectedTitle, + expectedDescription, + assertCalled, + }: PopconfirmExpectation, +) => { + await triggerPopconfirmAnd(canvas, kind, { + triggerButtonLabel: /cancel/i, + expectedTitle, + expectedDescription, + }); + assertCalled(args); }; const playExpectNoButtons: Story['play'] = ({ canvasElement }) => { const canvas = within(canvasElement); - expectNoButtons(canvas); + assertNoButtons(canvas); }; const playLoadingState: Story['play'] = ({ canvasElement }) => { const canvas = within(canvasElement); - const buttons = getButtons(canvas); - expect(buttons.length).toBeGreaterThan(0); - - // Verify buttons exist (loading state should still render buttons) - const primaryButton = getFirstButton(canvas); - expect(primaryButton).toBeTruthy(); + assertHasButtons(canvas); // Ant Design loading buttons have aria-busy attribute or loading class const loadingIndicators = canvasElement.querySelectorAll( @@ -37,7 +67,7 @@ const playLoadingState: Story['play'] = ({ canvasElement }) => { }; // Factory function for no-actions stories -const createNoActionsStory = (status: string): Story => ({ +const createNoActionsStory = (status: ReservationActionStatus): Story => ({ args: { status, onCancel: fn(), @@ -49,7 +79,7 @@ const createNoActionsStory = (status: string): Story => ({ // Factory function for loading state stories const createLoadingStory = ( - status: string, + status: ReservationActionStatus, loadingProp: 'cancelLoading' | 'closeLoading', ): Story => ({ args: { @@ -98,15 +128,7 @@ export const Requested: Story = { }, play: ({ canvasElement }) => { const canvas = within(canvasElement); - - // Verify action buttons are present - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); - - // Verify buttons are visible - for (const button of buttons) { - expect(button).toBeVisible(); - } + expectButtonsVisible(canvas); }, }; @@ -119,10 +141,7 @@ export const Accepted: Story = { }, play: ({ canvasElement }) => { const canvas = within(canvasElement); - - // Verify buttons are rendered for accepted state - const buttons = canvas.getAllByRole('button'); - expect(buttons.length).toBeGreaterThan(0); + expectButtonsVisible(canvas); }, }; @@ -135,15 +154,11 @@ export const ButtonInteraction: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - - // Guard against assumptions about button ordering by asserting count - // and selecting the message button via its accessible name. - const buttons = getButtons(canvas); - expect(buttons.length).toBeGreaterThan(1); + assertButtonCount(canvas, 2); const messageButton = canvas.getByRole('button', { name: /message/i }); + const { userEvent } = await import('storybook/test'); await userEvent.click(messageButton); - // Verify the message callback was called (message button doesn't have Popconfirm) expect(args.onMessage).toHaveBeenCalled(); }, }; @@ -174,13 +189,12 @@ export const RequestedWithPopconfirm: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'confirm', { + await runCancelPopconfirmFlow(canvas, args, { + kind: 'confirm', expectedTitle: 'Cancel Reservation Request', expectedDescription: 'Are you sure', + assertCalled: (a) => expect(a['onCancel']).toHaveBeenCalled(), }); - - expect(args.onCancel).toHaveBeenCalled(); }, }; @@ -192,10 +206,10 @@ export const PopconfirmCancelAction: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'cancel'); - - expect(args.onCancel).not.toHaveBeenCalled(); + await runCancelPopconfirmFlow(canvas, args, { + kind: 'cancel', + assertCalled: (a) => expect(a['onCancel']).not.toHaveBeenCalled(), + }); }, }; @@ -208,17 +222,14 @@ export const RejectedWithCancel: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); + assertButtonCount(canvas, 1); - // REJECTED status should have Cancel button (domain allows cancellation from Rejected state) - const buttons = getButtons(canvas); - expect(buttons.length).toBe(1); - - await triggerPopconfirmAnd(canvas, 'confirm', { + await runCancelPopconfirmFlow(canvas, args, { + kind: 'confirm', expectedTitle: 'Cancel Reservation Request', expectedDescription: 'Are you sure', + assertCalled: (a) => expect(a['onCancel']).toHaveBeenCalled(), }); - - expect(args.onCancel).toHaveBeenCalled(); }, }; @@ -234,13 +245,7 @@ export const AcceptedActions: Story = { }, play: ({ canvasElement }) => { const canvas = within(canvasElement); - - // Verify actions are present for accepted status - const buttons = getButtons(canvas); - expect(buttons.length).toBeGreaterThan(0); - - // Should have Close and Message buttons - expect(buttons.length).toBe(2); + expectButtonsVisible(canvas, 2); }, }; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts index 23e5dc592..fa5e2f01f 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts @@ -63,19 +63,30 @@ export const triggerPopconfirmAnd = async ( canvas: Canvas, action: PopconfirmAction, options?: { + triggerButtonLabel?: string | RegExp; triggerButtonIndex?: number; expectedTitle?: string; expectedDescription?: string; }, ) => { const { + triggerButtonLabel, triggerButtonIndex = 0, expectedTitle, expectedDescription, } = options ?? {}; - const buttons = canvas.getAllByRole('button'); - const triggerButton = buttons[triggerButtonIndex]; + let triggerButton: HTMLElement | undefined; + + if (triggerButtonLabel) { + triggerButton = (await canvas.findByRole('button', { + name: triggerButtonLabel, + })) as HTMLElement; + } else { + const buttons = canvas.getAllByRole('button'); + triggerButton = buttons[triggerButtonIndex] as HTMLElement | undefined; + } + expect(triggerButton).toBeTruthy(); if (!triggerButton) return; diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts index 0d57e00fa..0ff4e0d5a 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -11,6 +11,59 @@ const feature = await loadFeature( path.resolve(__dirname, 'features/cancel.feature'), ); +function buildReservation({ + id, + state, + reserverId, +}: { + id: string; + state: 'Requested' | 'Rejected' | 'Accepted'; + reserverId: string; +}) { + return { + id, + state, + loadReserver: vi.fn().mockResolvedValue({ id: reserverId }), + }; +} + +function mockTransaction({ + dataSources, + getByIdReturn, + saveReturn, +}: { + dataSources: DataSources; + getByIdReturn?: unknown; + saveReturn?: unknown; +}) { + ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock access + dataSources.domainDataSource as any + ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + async (callback: any) => { + const mockRepo = { + getById: vi.fn().mockResolvedValue(getByIdReturn), + save: vi.fn().mockResolvedValue(saveReturn), + }; + await callback(mockRepo); + }, + ); +} + +async function runCancel( + dataSources: DataSources, + command: ReservationRequestCancelCommand, +) { + const cancelFn = cancel(dataSources); + try { + const result = await cancelFn(command); + return { result, error: undefined }; + } catch (err) { + return { result: undefined, error: err }; + } +} + test.for(feature, ({ Scenario, BeforeEachScenario }) => { let mockDataSources: DataSources; let command: ReservationRequestCancelCommand; @@ -52,37 +105,23 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }); And('the reservation request exists and is in requested state', () => { - const mockReservationRequest = { - id: 'reservation-123', + const mockReservationRequest = buildReservation({ + id: command.id, state: 'Requested', - loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), - }; - - ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access - mockDataSources.domainDataSource as any - ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { - const mockRepo = { - getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi.fn().mockResolvedValue({ - ...mockReservationRequest, - state: 'Cancelled', - }), - }; - await callback(mockRepo); - }, - ); + reserverId: command.callerId, + }); + + mockTransaction({ + dataSources: mockDataSources, + getByIdReturn: mockReservationRequest, + saveReturn: { ...mockReservationRequest, state: 'Cancelled' }, + }); }); When('the cancel command is executed', async () => { - const cancelFn = cancel(mockDataSources); - try { - result = await cancelFn(command); - } catch (err) { - error = err; - } + const outcome = await runCancel(mockDataSources, command); + result = outcome.result; + error = outcome.error; }); Then('the reservation request should be cancelled', () => { @@ -101,37 +140,23 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }); And('the reservation request exists and is in rejected state', () => { - const mockReservationRequest = { - id: 'reservation-rejected', + const mockReservationRequest = buildReservation({ + id: command.id, state: 'Rejected', - loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), - }; - - ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access - mockDataSources.domainDataSource as any - ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { - const mockRepo = { - getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi.fn().mockResolvedValue({ - ...mockReservationRequest, - state: 'Cancelled', - }), - }; - await callback(mockRepo); - }, - ); + reserverId: command.callerId, + }); + + mockTransaction({ + dataSources: mockDataSources, + getByIdReturn: mockReservationRequest, + saveReturn: { ...mockReservationRequest, state: 'Cancelled' }, + }); }); When('the cancel command is executed', async () => { - const cancelFn = cancel(mockDataSources); - try { - result = await cancelFn(command); - } catch (err) { - error = err; - } + const outcome = await runCancel(mockDataSources, command); + result = outcome.result; + error = outcome.error; }); Then('the reservation request should be cancelled', () => { @@ -150,28 +175,17 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }); And('the reservation request does not exist', () => { - ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access - mockDataSources.domainDataSource as any - ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { - const mockRepo = { - getById: vi.fn().mockResolvedValue(undefined), - save: vi.fn(), - }; - await callback(mockRepo); - }, - ); + mockTransaction({ + dataSources: mockDataSources, + getByIdReturn: undefined, + saveReturn: undefined, + }); }); When('the cancel command is executed', async () => { - const cancelFn = cancel(mockDataSources); - try { - result = await cancelFn(command); - } catch (err) { - error = err; - } + const outcome = await runCancel(mockDataSources, command); + result = outcome.result; + error = outcome.error; }); Then('an error "Reservation request not found" should be thrown', () => { @@ -193,34 +207,23 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }); And('save returns undefined', () => { - const mockReservationRequest = { - id: 'reservation-456', + const mockReservationRequest = buildReservation({ + id: command.id, state: 'Requested', - loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), - }; - - ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access - mockDataSources.domainDataSource as any - ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { - const mockRepo = { - getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi.fn().mockResolvedValue(undefined), - }; - await callback(mockRepo); - }, - ); + reserverId: command.callerId, + }); + + mockTransaction({ + dataSources: mockDataSources, + getByIdReturn: mockReservationRequest, + saveReturn: undefined, + }); }); When('the cancel command is executed', async () => { - const cancelFn = cancel(mockDataSources); - try { - result = await cancelFn(command); - } catch (err) { - error = err; - } + const outcome = await runCancel(mockDataSources, command); + result = outcome.result; + error = outcome.error; }); Then( @@ -241,34 +244,23 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }); And('the reservation request is in Accepted state', () => { - const mockReservationRequest = { - id: 'reservation-accepted', + const mockReservationRequest = buildReservation({ + id: command.id, state: 'Accepted', - loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), - }; - - ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access - mockDataSources.domainDataSource as any - ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { - const mockRepo = { - getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi.fn(), - }; - await callback(mockRepo); - }, - ); + reserverId: command.callerId, + }); + + mockTransaction({ + dataSources: mockDataSources, + getByIdReturn: mockReservationRequest, + saveReturn: undefined, + }); }); When('the cancel command is executed', async () => { - const cancelFn = cancel(mockDataSources); - try { - result = await cancelFn(command); - } catch (err) { - error = err; - } + const outcome = await runCancel(mockDataSources, command); + result = outcome.result; + error = outcome.error; }); Then( @@ -291,34 +283,23 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }); And('the reservation request belongs to a different user', () => { - const mockReservationRequest = { - id: 'reservation-789', + const mockReservationRequest = buildReservation({ + id: command.id, state: 'Requested', - loadReserver: vi.fn().mockResolvedValue({ id: 'user-123' }), - }; - - ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access - mockDataSources.domainDataSource as any - ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { - const mockRepo = { - getById: vi.fn().mockResolvedValue(mockReservationRequest), - save: vi.fn(), - }; - await callback(mockRepo); - }, - ); + reserverId: 'user-123', // Different from command.callerId + }); + + mockTransaction({ + dataSources: mockDataSources, + getByIdReturn: mockReservationRequest, + saveReturn: undefined, + }); }); When('the cancel command is executed', async () => { - const cancelFn = cancel(mockDataSources); - try { - result = await cancelFn(command); - } catch (err) { - error = err; - } + const outcome = await runCancel(mockDataSources, command); + result = outcome.result; + error = outcome.error; }); Then( From 0ab6e76cabb437892a3895a2b0bc4bf2c54f699d Mon Sep 17 00:00:00 2001 From: Lian Date: Fri, 19 Dec 2025 11:23:50 -0500 Subject: [PATCH 12/20] removed unused exports error from knip --- .../src/test-utils/popconfirm-test-utils.ts | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts index fa5e2f01f..81f921c1c 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts @@ -1,6 +1,6 @@ import { expect, userEvent, waitFor, within } from 'storybook/test'; -export const POPCONFIRM_SELECTORS = { +const POPCONFIRM_SELECTORS = { title: '.ant-popconfirm-title', description: '.ant-popconfirm-description', confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', @@ -22,7 +22,7 @@ export const canvasUtils = { expect(canvas.getAllByRole('button').length).toBeGreaterThan(0), }; -export const waitForPopconfirm = async () => +const waitForPopconfirm = async () => waitFor( () => { const title = document.querySelector(POPCONFIRM_SELECTORS.title); @@ -32,33 +32,13 @@ export const waitForPopconfirm = async () => { timeout: 1000 }, ); -export const getPopconfirmElements = () => ({ +const getPopconfirmElements = () => ({ title: document.querySelector(POPCONFIRM_SELECTORS.title), description: document.querySelector(POPCONFIRM_SELECTORS.description), confirmButton: document.querySelector(POPCONFIRM_SELECTORS.confirmButton), cancelButton: document.querySelector(POPCONFIRM_SELECTORS.cancelButton), }); -export const confirmPopconfirm = async () => { - const confirmButton = document.querySelector( - POPCONFIRM_SELECTORS.confirmButton, - ); - if (confirmButton) { - await userEvent.click(confirmButton as HTMLElement); - } - return confirmButton; -}; - -export const cancelPopconfirm = async () => { - const cancelButton = document.querySelector( - POPCONFIRM_SELECTORS.cancelButton, - ); - if (cancelButton) { - await userEvent.click(cancelButton as HTMLElement); - } - return cancelButton; -}; - export const triggerPopconfirmAnd = async ( canvas: Canvas, action: PopconfirmAction, From 77b087a1de9b9f443703805227728559f381d694 Mon Sep 17 00:00:00 2001 From: Lian Date: Fri, 19 Dec 2025 13:33:01 -0500 Subject: [PATCH 13/20] addressed sourcery feedback --- .../listing-information.container.stories.tsx | 234 +++++++++++++++++- .../listing-information.stories.tsx | 92 +++---- .../listing-information.tsx | 1 + .../stories/reservation-actions.stories.tsx | 234 +++++++++--------- .../src/test-utils/popconfirm-test-utils.ts | 12 - 5 files changed, 390 insertions(+), 183 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx index e6bb120b5..9cda8913d 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect } from 'storybook/test'; +import { expect, within, userEvent, waitFor } from 'storybook/test'; import { ListingInformationContainer } from './listing-information.container.tsx'; import { withMockApolloClient, @@ -316,3 +316,235 @@ export const CancelReservationLoading: Story = { await expect(canvasElement).toBeTruthy(); }, }; + +// Test handleReserveClick success path (covers lines 99-116) +export const CreateReservationSuccess: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: [ + ...buildBaseListingMocks(), + { + request: { + query: HomeListingInformationCreateReservationRequestDocument, + variables: { + input: { + listingId: '1', + reservationPeriodStart: expect.any(String), + reservationPeriodEnd: expect.any(String), + }, + }, + }, + variableMatcher: () => true, + result: { + data: { + createReservationRequest: { + __typename: 'ReservationRequest', + id: 'new-res-1', + }, + }, + }, + }, + { + request: { + query: ViewListingActiveReservationRequestForListingDocument, + }, + result: { + data: { + myActiveReservationForListing: { + __typename: 'ReservationRequest', + id: 'new-res-1', + state: 'Requested', + reservationPeriodStart: String( + new Date('2025-03-01').getTime(), + ), + reservationPeriodEnd: String(new Date('2025-03-10').getTime()), + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for component to load + await waitFor(() => { + const dateInputs = canvas.queryAllByPlaceholderText(/date/i); + expect(dateInputs.length).toBeGreaterThan(0); + }); + + // Find and click the first date input to open picker + const dateInputs = canvas.getAllByPlaceholderText(/date/i); + if (dateInputs[0]) { + await userEvent.click(dateInputs[0]); + } + }, +}; + +// Test handleReserveClick error path (covers lines 113-115 - error logging) +export const CreateReservationError: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: [ + ...buildBaseListingMocks(), + { + request: { + query: HomeListingInformationCreateReservationRequestDocument, + variables: { + input: { + listingId: '1', + reservationPeriodStart: expect.any(String), + reservationPeriodEnd: expect.any(String), + }, + }, + }, + variableMatcher: () => true, + error: new Error('Failed to create reservation request'), + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for component to load + await waitFor(() => { + const dateInputs = canvas.queryAllByPlaceholderText(/date/i); + expect(dateInputs.length).toBeGreaterThan(0); + }); + }, +}; + +// Test handleCancelClick with no reservation id (covers lines 126-129) +export const CancelReservationNoId: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: { + __typename: 'ReservationRequest' as const, + id: '', // Empty id to trigger the early return + state: 'Requested' as const, + reservationPeriodStart: '2025-02-01', + reservationPeriodEnd: '2025-02-10', + }, + }, + parameters: { + apolloClient: { + mocks: [...buildBaseListingMocks()], + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + // Cancel button should trigger error message when clicked with no id + await clickCancelThenConfirm(canvasElement); + }, +}; + +// Test onCompleted callback for create mutation (covers lines 80-84) +export const CreateReservationOnCompleted: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: [ + ...buildBaseListingMocks(), + { + request: { + query: HomeListingInformationCreateReservationRequestDocument, + variables: { + input: { + listingId: '1', + reservationPeriodStart: expect.any(String), + reservationPeriodEnd: expect.any(String), + }, + }, + }, + variableMatcher: () => true, + result: { + data: { + createReservationRequest: { + __typename: 'ReservationRequest', + id: 'new-res-completed', + }, + }, + }, + }, + { + request: { + query: ViewListingActiveReservationRequestForListingDocument, + }, + result: { + data: { + myActiveReservationForListing: { + __typename: 'ReservationRequest', + id: 'new-res-completed', + state: 'Requested', + reservationPeriodStart: String( + new Date('2025-03-01').getTime(), + ), + reservationPeriodEnd: String(new Date('2025-03-10').getTime()), + }, + }, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + }, +}; + +// Test onError callback for create mutation (covers lines 86-87) +export const CreateReservationOnError: Story = { + args: { + listing: mockListing, + userIsSharer: false, + isAuthenticated: true, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: [ + ...buildBaseListingMocks(), + { + request: { + query: HomeListingInformationCreateReservationRequestDocument, + variables: { + input: { + listingId: '1', + reservationPeriodStart: expect.any(String), + reservationPeriodEnd: expect.any(String), + }, + }, + }, + variableMatcher: () => true, + error: new Error('Reservation period overlaps with existing booking'), + }, + ], + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx index 22b4c2bff..2e2c580fa 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx @@ -11,41 +11,6 @@ const baseReservationRequest = { reservationPeriodEnd: '1739145600000', }; -type ReservationState = 'Requested' | 'Accepted'; - -const createUserReservationRequest = ( - state: ReservationState, - overrides: Partial = {}, -) => ({ - ...baseReservationRequest, - state, - ...overrides, -}); - -type CancelFlowOptions = { - confirm: boolean; - expectCallback: boolean; -}; - -const runCancelFlow = async ( - canvasElement: HTMLElement, - args: Record, - { confirm, expectCallback }: CancelFlowOptions, -) => { - const canvas = within(canvasElement); - await expect(canvasElement).toBeTruthy(); - - await triggerPopconfirmAnd(canvas, confirm ? 'confirm' : 'cancel', { - triggerButtonLabel: /Cancel Request/i, - }); - - if (expectCallback) { - expect(args['onCancelClick']).toHaveBeenCalled(); - } else { - expect(args['onCancelClick']).not.toHaveBeenCalled(); - } -}; - const mockListing = { __typename: 'ItemListing' as const, listingType: 'item-listing' as const, @@ -230,17 +195,20 @@ export const ClickReserveButton: Story = { export const ClickCancelButton: Story = { args: { onCancelClick: fn(), - userReservationRequest: createUserReservationRequest('Requested'), + userReservationRequest: { + ...baseReservationRequest, + state: 'Requested' as const, + }, }, play: async ({ canvasElement, args }) => { - await runCancelFlow( - canvasElement, - args as unknown as Record, - { - confirm: true, - expectCallback: true, - }, - ); + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel Request/i, + }); + + expect(args.onCancelClick).toHaveBeenCalled(); }, }; @@ -354,7 +322,10 @@ export const ClearDateSelection: Story = { export const CancelButtonWithPopconfirm: Story = { args: { - userReservationRequest: createUserReservationRequest('Requested'), + userReservationRequest: { + ...baseReservationRequest, + state: 'Requested' as const, + }, onCancelClick: fn(), cancelLoading: false, }, @@ -373,7 +344,10 @@ export const CancelButtonWithPopconfirm: Story = { export const CancelButtonLoading: Story = { args: { - userReservationRequest: createUserReservationRequest('Requested'), + userReservationRequest: { + ...baseReservationRequest, + state: 'Requested' as const, + }, cancelLoading: true, }, play: async ({ canvasElement }) => { @@ -390,7 +364,10 @@ export const CancelButtonLoading: Story = { export const NoCancelButtonForAcceptedReservation: Story = { args: { - userReservationRequest: createUserReservationRequest('Accepted'), + userReservationRequest: { + ...baseReservationRequest, + state: 'Accepted' as const, + }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -404,17 +381,20 @@ export const NoCancelButtonForAcceptedReservation: Story = { export const PopconfirmCancelButton: Story = { args: { - userReservationRequest: createUserReservationRequest('Requested'), + userReservationRequest: { + ...baseReservationRequest, + state: 'Requested' as const, + }, onCancelClick: fn(), }, play: async ({ canvasElement, args }) => { - await runCancelFlow( - canvasElement, - args as unknown as Record, - { - confirm: false, - expectCallback: false, - }, - ); + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + await triggerPopconfirmAnd(canvas, 'cancel', { + triggerButtonLabel: /Cancel Request/i, + }); + + expect(args.onCancelClick).not.toHaveBeenCalled(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx index 15e4ba8b6..5feb1ec16 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.tsx @@ -319,6 +319,7 @@ export const ListingInformation: React.FC = ({ onConfirm={onCancelClick} okText="Yes" cancelText="No" + okButtonProps={{ loading: cancelLoading }} > - + ); } return ( diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx index ee1e634e4..901eb0dea 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/components/reservation-actions.tsx @@ -1,40 +1,9 @@ import type React from 'react'; -import { Space, Popconfirm } from 'antd'; +import { Space } from 'antd'; +import { CancelReservationPopconfirm } from '@sthrift/ui-components'; import { ReservationActionButton } from './reservation-action-button.tsx'; import type { ReservationActionStatus } from '../utils/reservation-status.utils.ts'; -interface CancelWithPopconfirmProps { - onConfirm?: () => void; - loading?: boolean; -} - -const CancelWithPopconfirm: React.FC = ({ - onConfirm, - loading, -}) => { - const handleConfirm = () => { - if (loading) { - return; - } - onConfirm?.(); - }; - - return ( - - - - - - ); -}; - interface ReservationActionsProps { status: ReservationActionStatus; onCancel?: () => void; @@ -56,11 +25,18 @@ export const ReservationActions: React.FC = ({ switch (status) { case 'REQUESTED': return [ - , + > + + + + , = ({ case 'REJECTED': return [ - , + > + + + + , ]; default: // No actions for cancelled or closed reservations diff --git a/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.tsx b/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.tsx new file mode 100644 index 000000000..c6bdc7e41 --- /dev/null +++ b/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.tsx @@ -0,0 +1,45 @@ +import type React from 'react'; +import { Popconfirm } from 'antd'; + +interface CancelReservationPopconfirmProps { + /** + * Callback when user confirms cancellation + */ + onConfirm?: () => void; + /** + * Whether the cancel operation is in progress + */ + loading?: boolean; + /** + * The trigger element to wrap with the popconfirm + */ + children: React.ReactNode; +} + +/** + * Shared Popconfirm component for cancelling reservation requests. + * Provides consistent UX and behavior across the application. + */ +export const CancelReservationPopconfirm: React.FC< + CancelReservationPopconfirmProps +> = ({ onConfirm, loading, children }) => { + const handleConfirm = () => { + if (loading) { + return; + } + onConfirm?.(); + }; + + return ( + + {children} + + ); +}; diff --git a/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/index.ts b/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/index.ts new file mode 100644 index 000000000..0c6de69ac --- /dev/null +++ b/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/index.ts @@ -0,0 +1 @@ +export { CancelReservationPopconfirm } from './cancel-reservation-popconfirm.tsx'; diff --git a/packages/sthrift/ui-components/src/index.ts b/packages/sthrift/ui-components/src/index.ts index 46b4ff7e1..055ad3c1a 100644 --- a/packages/sthrift/ui-components/src/index.ts +++ b/packages/sthrift/ui-components/src/index.ts @@ -10,3 +10,4 @@ export { ListingsGrid } from './organisms/listings-grid/index.js'; export { ComponentQueryLoader } from './molecules/component-query-loader/index.js'; export { Dashboard } from './organisms/dashboard/index.tsx'; export { ReservationStatusTag } from './atoms/reservation-status-tag/index.js'; +export { CancelReservationPopconfirm } from './components/cancel-reservation-popconfirm/index.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3c14ec49..81211186d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -639,8 +639,8 @@ importers: packages/cellix/vitest-config: dependencies: '@storybook/addon-vitest': - specifier: ^9.1.10 - version: 9.1.16(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) + specifier: ^9.1.17 + version: 9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4) vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) @@ -4420,6 +4420,24 @@ packages: vitest: optional: true + '@storybook/addon-vitest@9.1.17': + resolution: {integrity: sha512-2EIvZPz0N+mnIUnUHW3+GIgwJRIqjZrK5BFyHsi82NhOQ1LCh/1GqbcB+kNoaiXioRcAgOsHUDWbQZrvyx3GhQ==} + peerDependencies: + '@vitest/browser': ^3.0.0 || ^4.0.0 + '@vitest/browser-playwright': ^4.0.0 + '@vitest/runner': ^3.0.0 || ^4.0.0 + storybook: ^9.1.17 + vitest: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/runner': + optional: true + vitest: + optional: true + '@storybook/builder-vite@9.1.16': resolution: {integrity: sha512-CyvYA5w1BKeSVaRavKi+euWxLffshq0v9Rz/5E9MKCitbYtjwkDH6UMIYmcbTs906mEBuYqrbz3nygDP0ppodw==} peerDependencies: @@ -16021,6 +16039,21 @@ snapshots: - react - react-dom + '@storybook/addon-vitest@9.1.17(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vitest@3.2.4)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + prompts: 2.4.2 + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + ts-dedent: 2.2.0 + optionalDependencies: + '@vitest/browser': 3.2.4(playwright@1.56.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/runner': 3.2.4 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.2)(@vitest/browser@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - react + - react-dom + '@storybook/builder-vite@9.1.16(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@storybook/csf-plugin': 9.1.16(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) From 756a721fe6c338a16eb483332e920186032d9d34 Mon Sep 17 00:00:00 2001 From: Lian Date: Fri, 19 Dec 2025 15:36:08 -0500 Subject: [PATCH 15/20] more sourcery comments --- .../listing-information.container.stories.tsx | 396 ++++++++++++++++++ .../stories/reservation-actions.stories.tsx | 17 +- .../popconfirm-test-utils.stories.tsx | 238 +++++++++++ .../src/test-utils/popconfirm-test-utils.ts | 15 +- apps/ui-sharethrift/vitest.config.ts | 9 +- .../cancel-reservation-popconfirm.stories.tsx | 177 ++++++++ .../src/test-utils/popconfirm-test-utils.ts | 80 ++++ 7 files changed, 916 insertions(+), 16 deletions(-) create mode 100644 apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx create mode 100644 packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.stories.tsx create mode 100644 packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx index 69e189ebc..58ac5ce73 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx @@ -515,3 +515,399 @@ export const CreateReservationOnError: Story = { await expect(canvasElement).toBeTruthy(); }, }; + +// Scenario-focused helpers for cleaner story declarations +const buildCancelSuccessMocks = (id: string) => + buildCancelReservationMocks({ + id, + result: { id, state: 'Cancelled' }, + includeActiveReservationRefetch: true, + activeReservationResult: null, + }); + +const buildCancelErrorMocks = (id: string, message: string) => + buildCancelReservationMocks({ + id, + error: new Error(message), + }); + +const buildCreateSuccessMocks = (listingId: string, reservationId: string) => + buildCreateReservationMocks({ + listingId, + result: { id: reservationId }, + activeReservation: { + id: reservationId, + state: 'Requested', + reservationPeriodStart: String(new Date('2025-03-01').getTime()), + reservationPeriodEnd: String(new Date('2025-03-10').getTime()), + }, + }); + +const buildCreateErrorMocks = (listingId: string, message: string) => + buildCreateReservationMocks({ + listingId, + error: new Error(message), + }); + +// Reservation presets for common states +const requestedReservation = (id = 'res-1') => + makeUserReservationRequest({ id, state: 'Requested' }); + +/** + * Exercise handleReserveClick with dates selected and successful mutation. + * This covers lines 104-123 (the full handleReserveClick flow). + */ +export const ReserveWithDatesSuccess: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: buildCreateSuccessMocks('1', 'new-res-with-dates'), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for the date picker to be available + await waitFor(() => { + const dateInputs = canvas.queryAllByPlaceholderText(/date/i); + expect(dateInputs.length).toBeGreaterThan(0); + }); + + // Click on date picker to open it + const dateInputs = canvas.getAllByPlaceholderText(/date/i); + const startDateInput = dateInputs[0]; + if (startDateInput) { + await userEvent.click(startDateInput); + } + + // Wait for calendar to open + await waitFor(() => { + const calendarCells = document.querySelectorAll('.ant-picker-cell-inner'); + expect(calendarCells.length).toBeGreaterThan(0); + }); + + // Select a future date (find cells that are not disabled) + const availableCells = document.querySelectorAll( + '.ant-picker-cell:not(.ant-picker-cell-disabled) .ant-picker-cell-inner', + ); + + if (availableCells.length >= 2) { + // Click start date + const startCell = availableCells[10]; + const endCell = availableCells[15]; + if (startCell && endCell) { + await userEvent.click(startCell as HTMLElement); + // Click end date + await userEvent.click(endCell as HTMLElement); + } + } + + // Wait for Reserve button to be enabled + await waitFor( + () => { + const reserveButton = canvas.queryByRole('button', { + name: /reserve/i, + }); + if (reserveButton && !reserveButton.hasAttribute('disabled')) { + return reserveButton; + } + throw new Error('Reserve button not enabled yet'); + }, + { timeout: 3000 }, + ); + + // Click Reserve button + const reserveButton = canvas.getByRole('button', { name: /reserve/i }); + await userEvent.click(reserveButton); + }, +}; + +/** + * Exercise handleReserveClick error path with dates selected. + * This covers the onError callback (lines 86-87) for create mutation. + */ +export const ReserveWithDatesError: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: buildCreateErrorMocks('1', 'Failed to create reservation request'), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for the date picker to be available + await waitFor(() => { + const dateInputs = canvas.queryAllByPlaceholderText(/date/i); + expect(dateInputs.length).toBeGreaterThan(0); + }); + + // Click on date picker to open it + const dateInputs = canvas.getAllByPlaceholderText(/date/i); + const startInput = dateInputs[0]; + if (startInput) { + await userEvent.click(startInput); + } + + // Wait for calendar to open + await waitFor(() => { + const calendarCells = document.querySelectorAll('.ant-picker-cell-inner'); + expect(calendarCells.length).toBeGreaterThan(0); + }); + + // Select a future date + const availableCells = document.querySelectorAll( + '.ant-picker-cell:not(.ant-picker-cell-disabled) .ant-picker-cell-inner', + ); + + if (availableCells.length >= 2) { + const startCell = availableCells[10]; + const endCell = availableCells[15]; + if (startCell && endCell) { + await userEvent.click(startCell as HTMLElement); + await userEvent.click(endCell as HTMLElement); + } + } + + // Wait for Reserve button to be enabled and click + await waitFor( + () => { + const reserveButton = canvas.queryByRole('button', { + name: /reserve/i, + }); + if (reserveButton && !reserveButton.hasAttribute('disabled')) { + return reserveButton; + } + throw new Error('Reserve button not enabled yet'); + }, + { timeout: 3000 }, + ); + + const reserveButton = canvas.getByRole('button', { name: /reserve/i }); + await userEvent.click(reserveButton); + }, +}; + +/** + * Exercise cancelLoading early return path (lines 126-128). + * Tests that handleCancelClick returns early when cancel is in progress. + */ +export const CancelLoadingEarlyReturn: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: requestedReservation('res-loading-test'), + }, + parameters: { + apolloClient: { + mocks: buildCancelReservationMocks({ + id: 'res-loading-test', + result: { id: 'res-loading-test', state: 'Cancelled' }, + delay: 5000, // Long delay to keep loading state active + }), + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + + // First click to start the cancellation + await clickCancelThenConfirm(canvasElement); + + // Try to click again while loading - this tests the early return + // The second click should be ignored due to cancelLoading check + const canvas = within(canvasElement); + const cancelButton = canvas.queryByRole('button', { + name: /cancel request/i, + }); + if (cancelButton) { + await userEvent.click(cancelButton); + } + }, +}; + +/** + * Exercise onCompleted callback for cancel mutation (lines 92-96). + * Tests that success message is shown after cancellation. + */ +export const CancelOnCompletedCallback: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: requestedReservation('res-completed-test'), + }, + parameters: { + apolloClient: { + mocks: buildCancelSuccessMocks('res-completed-test'), + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + await clickCancelThenConfirm(canvasElement); + }, +}; + +/** + * Exercise onError callback for cancel mutation (lines 97-99). + * Tests that error message is shown when cancellation fails. + */ +export const CancelOnErrorCallback: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: requestedReservation('res-error-test'), + }, + parameters: { + apolloClient: { + mocks: buildCancelErrorMocks('res-error-test', 'Network error occurred'), + }, + }, + play: async ({ canvasElement }) => { + await expect(canvasElement).toBeTruthy(); + await clickCancelThenConfirm(canvasElement); + }, +}; + +/** + * Exercise onCompleted callback for create mutation (lines 80-84). + * Tests that refetchQueries is called and dates are reset after success. + */ +export const CreateOnCompletedCallback: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: buildCreateSuccessMocks('1', 'new-res-complete-test'), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for the date picker + await waitFor(() => { + const dateInputs = canvas.queryAllByPlaceholderText(/date/i); + expect(dateInputs.length).toBeGreaterThan(0); + }); + + // Click on date picker + const dateInputs = canvas.getAllByPlaceholderText(/date/i); + const startDateInput = dateInputs[0]; + if (startDateInput) { + await userEvent.click(startDateInput); + } + + // Wait for calendar + await waitFor(() => { + const calendarCells = document.querySelectorAll('.ant-picker-cell-inner'); + expect(calendarCells.length).toBeGreaterThan(0); + }); + + // Select dates + const availableCells = document.querySelectorAll( + '.ant-picker-cell:not(.ant-picker-cell-disabled) .ant-picker-cell-inner', + ); + + if (availableCells.length >= 2) { + const startCell = availableCells[10]; + const endCell = availableCells[15]; + if (startCell && endCell) { + await userEvent.click(startCell as HTMLElement); + await userEvent.click(endCell as HTMLElement); + } + } + + // Wait and click Reserve + await waitFor( + () => { + const reserveButton = canvas.queryByRole('button', { + name: /reserve/i, + }); + if (reserveButton && !reserveButton.hasAttribute('disabled')) { + return reserveButton; + } + throw new Error('Reserve button not enabled yet'); + }, + { timeout: 3000 }, + ); + + const reserveButton = canvas.getByRole('button', { name: /reserve/i }); + await userEvent.click(reserveButton); + }, +}; + +/** + * Exercise onError callback for create mutation (lines 86-87). + * Tests that error is logged when creation fails. + */ +export const CreateOnErrorCallback: Story = { + args: { + ...baseAuthedBorrowerArgs, + userReservationRequest: null, + }, + parameters: { + apolloClient: { + mocks: buildCreateErrorMocks('1', 'Database connection failed'), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvasElement).toBeTruthy(); + + // Wait for the date picker + await waitFor(() => { + const dateInputs = canvas.queryAllByPlaceholderText(/date/i); + expect(dateInputs.length).toBeGreaterThan(0); + }); + + // Click on date picker + const dateInputs = canvas.getAllByPlaceholderText(/date/i); + const startDateInput = dateInputs[0]; + if (startDateInput) { + await userEvent.click(startDateInput); + } + + // Wait for calendar + await waitFor(() => { + const calendarCells = document.querySelectorAll('.ant-picker-cell-inner'); + expect(calendarCells.length).toBeGreaterThan(0); + }); + + // Select dates + const availableCells = document.querySelectorAll( + '.ant-picker-cell:not(.ant-picker-cell-disabled) .ant-picker-cell-inner', + ); + + if (availableCells.length >= 2) { + const startCell = availableCells[10]; + const endCell = availableCells[15]; + if (startCell && endCell) { + await userEvent.click(startCell as HTMLElement); + await userEvent.click(endCell as HTMLElement); + } + } + + // Wait and click Reserve + await waitFor( + () => { + const reserveButton = canvas.queryByRole('button', { + name: /reserve/i, + }); + if (reserveButton && !reserveButton.hasAttribute('disabled')) { + return reserveButton; + } + throw new Error('Reserve button not enabled yet'); + }, + { timeout: 3000 }, + ); + + const reserveButton = canvas.getByRole('button', { name: /reserve/i }); + await userEvent.click(reserveButton); + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index c6aa5c74e..bbc43d8aa 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -1,7 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ReservationActions } from '../components/reservation-actions.js'; import { expect, fn, within } from 'storybook/test'; -import { triggerPopconfirmAnd } from '../../../../../test-utils/popconfirm-test-utils.ts'; +import { + triggerPopconfirmAnd, + getLoadingIndicators, +} from '../../../../../test-utils/popconfirm-test-utils.ts'; const meta: Meta = { title: 'Molecules/ReservationActions', @@ -215,9 +218,9 @@ export const CancelLoadingState: Story = { const buttons = canvas.getAllByRole('button'); expect(buttons.length).toBeGreaterThan(0); - // Ant Design loading buttons have aria-busy attribute or loading class - const loadingIndicators = canvasElement.querySelectorAll( - '.ant-btn-loading, [aria-busy="true"]', + // Use centralized helper to find loading indicators + const loadingIndicators = getLoadingIndicators( + canvasElement as HTMLElement, ); expect(loadingIndicators.length).toBeGreaterThan(0); }, @@ -236,9 +239,9 @@ export const CloseLoadingState: Story = { const buttons = canvas.getAllByRole('button'); expect(buttons.length).toBeGreaterThan(0); - // Ant Design loading buttons have aria-busy attribute or loading class - const loadingIndicators = canvasElement.querySelectorAll( - '.ant-btn-loading, [aria-busy="true"]', + // Use centralized helper to find loading indicators + const loadingIndicators = getLoadingIndicators( + canvasElement as HTMLElement, ); expect(loadingIndicators.length).toBeGreaterThan(0); }, diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx new file mode 100644 index 000000000..2998ef0f3 --- /dev/null +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx @@ -0,0 +1,238 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button, Popconfirm } from 'antd'; +import { expect, fn, within } from 'storybook/test'; +import { + triggerPopconfirmAnd, + clickCancelThenConfirm, + getLoadingIndicators, +} from './popconfirm-test-utils.ts'; + +/** + * Demo component that wraps a button with a Popconfirm for testing purposes. + * This component is used to test the popconfirm-test-utils helper functions. + */ +const PopconfirmDemo = ({ + onConfirm, + onCancel, + buttonLabel = 'Cancel', + title = 'Confirm Action', + description = 'Are you sure you want to proceed?', + loading = false, +}: { + onConfirm?: () => void; + onCancel?: () => void; + buttonLabel?: string; + title?: string; + description?: string; + loading?: boolean; +}) => ( + + + +); + +/** + * Multi-button demo to test triggerButtonIndex option + */ +const MultiButtonPopconfirmDemo = ({ + onConfirm, + onCancel, +}: { + onConfirm?: () => void; + onCancel?: () => void; +}) => ( +
+ + + + + +
+); + +const meta: Meta = { + title: 'Test Utils/PopconfirmTestUtils', + component: PopconfirmDemo, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +/** + * Tests triggerPopconfirmAnd with 'confirm' action using triggerButtonLabel option. + * This covers the triggerButtonLabel path and expectedTitle/expectedDescription assertions. + */ +export const TriggerPopconfirmAndConfirm: Story = { + args: { + onConfirm: fn(), + onCancel: fn(), + buttonLabel: 'Cancel', + title: 'Confirm Action', + description: 'Are you sure you want to proceed?', + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel/i, + expectedTitle: 'Confirm Action', + expectedDescription: 'Are you sure', + }); + + expect(args.onConfirm).toHaveBeenCalled(); + expect(args.onCancel).not.toHaveBeenCalled(); + }, +}; + +/** + * Tests triggerPopconfirmAnd with 'cancel' action. + * This covers the cancel action path where onConfirm should NOT be called. + */ +export const TriggerPopconfirmAndCancel: Story = { + args: { + onConfirm: fn(), + onCancel: fn(), + buttonLabel: 'Cancel', + title: 'Confirm Action', + description: 'Are you sure you want to proceed?', + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'cancel', { + triggerButtonLabel: /Cancel/i, + }); + + expect(args.onConfirm).not.toHaveBeenCalled(); + }, +}; + +/** + * Tests triggerPopconfirmAnd using triggerButtonIndex option (default behavior). + * This covers the path where no triggerButtonLabel is provided. + */ +export const TriggerPopconfirmByIndex: StoryObj< + typeof MultiButtonPopconfirmDemo +> = { + render: (args) => , + args: { + onConfirm: fn(), + onCancel: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + // Use index 1 to click the second button (the one with Popconfirm) + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonIndex: 1, + }); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; + +/** + * Tests clickCancelThenConfirm helper function. + * This covers the entire clickCancelThenConfirm flow. + */ +export const ClickCancelThenConfirmFlow: Story = { + args: { + onConfirm: fn(), + onCancel: fn(), + buttonLabel: 'Cancel', + title: 'Confirm Action', + description: 'Are you sure you want to proceed?', + }, + play: async ({ canvasElement, args }) => { + await clickCancelThenConfirm(canvasElement); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; + +/** + * Tests getLoadingIndicators helper function. + * This covers the loading indicator detection utility. + */ +export const LoadingIndicatorDetection: Story = { + args: { + onConfirm: fn(), + buttonLabel: 'Cancel', + loading: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Click button to open popconfirm + const button = canvas.getByRole('button', { name: /Cancel/i }); + const { userEvent } = await import('storybook/test'); + await userEvent.click(button); + + // Use getLoadingIndicators helper to find loading buttons + const loadingIndicators = getLoadingIndicators(document.body); + expect(loadingIndicators.length).toBeGreaterThan(0); + }, +}; + +/** + * Tests triggerPopconfirmAnd without optional parameters. + * This covers the default options path and ensures basic functionality works. + */ +export const TriggerPopconfirmDefaultOptions: Story = { + args: { + onConfirm: fn(), + onCancel: fn(), + buttonLabel: 'Cancel', + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + // Call with no options to test default behavior + await triggerPopconfirmAnd(canvas, 'confirm'); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; + +/** + * Tests getPopconfirmElements indirectly through the confirm flow. + * This validates that all popconfirm elements are found correctly. + */ +export const PopconfirmElementsValidation: Story = { + args: { + onConfirm: fn(), + buttonLabel: 'Cancel', + title: 'Test Title', + description: 'Test Description', + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel/i, + expectedTitle: 'Test Title', + expectedDescription: 'Test Description', + }); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts index 73d396642..9b93c80a1 100644 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts +++ b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts @@ -10,14 +10,21 @@ const POPCONFIRM_SELECTORS = { type Canvas = ReturnType; type PopconfirmAction = 'confirm' | 'cancel'; -const waitForPopconfirm = async () => +/** + * Centralized helper to find loading indicators on Ant Design buttons. + * This abstracts Ant Design implementation details so they can be updated in one place. + */ +export const getLoadingIndicators = (root: HTMLElement) => + root.querySelectorAll('.ant-btn-loading, [aria-busy="true"]'); + +const waitForPopconfirm = async (timeoutMs = 3000) => waitFor( () => { const title = document.querySelector(POPCONFIRM_SELECTORS.title); if (!title) throw new Error('Popconfirm not found'); return title; }, - { timeout: 1000 }, + { timeout: timeoutMs }, ); const getPopconfirmElements = () => ({ @@ -88,7 +95,7 @@ export const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { if (!btn) throw new Error('Cancel button not found yet'); return btn; }, - { timeout: 1000 }, + { timeout: 3000 }, ); await userEvent.click(cancelButton); @@ -99,7 +106,7 @@ export const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { if (!btn) throw new Error('Confirm button not found yet'); return btn; }, - { timeout: 1000 }, + { timeout: 3000 }, ); await userEvent.click(confirmButton as HTMLElement); diff --git a/apps/ui-sharethrift/vitest.config.ts b/apps/ui-sharethrift/vitest.config.ts index f0c4b5905..9cb3dddaa 100644 --- a/apps/ui-sharethrift/vitest.config.ts +++ b/apps/ui-sharethrift/vitest.config.ts @@ -14,14 +14,13 @@ export default defineConfig( additionalCoverageExclude: [ '**/index.ts', '**/index.tsx', - '**/Index.tsx', + '**/Index.tsx', 'src/main.tsx', - 'src/test-utils/**', - 'src/config/**', - 'src/test/**', + 'src/config/**', + 'src/test/**', '**/*.d.ts', 'src/generated/**', - 'eslint.config.js' + 'eslint.config.js', ], }), ); diff --git a/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.stories.tsx b/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.stories.tsx new file mode 100644 index 000000000..7929361c4 --- /dev/null +++ b/packages/sthrift/ui-components/src/components/cancel-reservation-popconfirm/cancel-reservation-popconfirm.stories.tsx @@ -0,0 +1,177 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +import { expect, fn, within } from 'storybook/test'; +import { CancelReservationPopconfirm } from './cancel-reservation-popconfirm.tsx'; +import { triggerPopconfirmAnd } from '../../test-utils/popconfirm-test-utils.ts'; + +const meta: Meta = { + title: 'Components/CancelReservationPopconfirm', + component: CancelReservationPopconfirm, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + onConfirm: { action: 'confirmed' }, + loading: { control: 'boolean' }, + }, +}; + +export default meta; +type Story = StoryObj; + +/** + * Default state - shows the popconfirm with a trigger button. + * Covers lines: component declaration, interface, basic rendering. + */ +export const Default: Story = { + args: { + onConfirm: fn(), + loading: false, + children: , + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByRole('button', { name: /Cancel Reservation/i }); + expect(button).toBeVisible(); + }, +}; + +/** + * Tests clicking the popconfirm trigger and confirming. + * Covers lines: handleConfirm function, onConfirm callback invocation. + */ +export const ConfirmCancellation: Story = { + args: { + onConfirm: fn(), + loading: false, + children: , + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel Reservation/i, + expectedTitle: 'Cancel Reservation Request', + expectedDescription: 'Are you sure you want to cancel this request?', + }); + + expect(args.onConfirm).toHaveBeenCalled(); + }, +}; + +/** + * Tests clicking the popconfirm trigger and then clicking 'No' to cancel. + * Covers lines: Popconfirm rendering with okText/cancelText props. + */ +export const CancelPopconfirm: Story = { + args: { + onConfirm: fn(), + loading: false, + children: , + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'cancel', { + triggerButtonLabel: /Cancel Reservation/i, + }); + + expect(args.onConfirm).not.toHaveBeenCalled(); + }, +}; + +/** + * Tests the loading state which should prevent handleConfirm from executing. + * Covers lines: loading prop, early return in handleConfirm when loading is true. + */ +export const LoadingState: Story = { + args: { + onConfirm: fn(), + loading: true, + children: , + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel Reservation/i, + }); + + // onConfirm should NOT be called because loading is true + expect(args.onConfirm).not.toHaveBeenCalled(); + }, +}; + +/** + * Tests without onConfirm callback (optional prop). + * Covers lines: optional chaining onConfirm?.(). + */ +export const NoConfirmCallback: Story = { + args: { + loading: false, + children: , + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Should not throw even without onConfirm handler + await triggerPopconfirmAnd(canvas, 'confirm', { + triggerButtonLabel: /Cancel Reservation/i, + }); + + // Expect no errors - the component handles missing callback gracefully + expect(canvas.getByRole('button')).toBeVisible(); + }, +}; + +/** + * Tests with different children (text instead of button). + * Covers lines: children prop rendering. + */ +export const WithTextChild: Story = { + args: { + onConfirm: fn(), + loading: false, + children: ( + Click to cancel + ), + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Click to cancel/i)).toBeVisible(); + }, +}; + +/** + * Tests that loading prop is passed to okButtonProps. + * Covers lines: okButtonProps={{ loading }}. + */ +export const LoadingButtonState: Story = { + args: { + onConfirm: fn(), + loading: true, + children: , + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const { userEvent, waitFor } = await import('storybook/test'); + + // Click to open popconfirm + const button = canvas.getByRole('button', { name: /Cancel Reservation/i }); + await userEvent.click(button); + + // Wait for popconfirm to appear and check for loading state on OK button + await waitFor(() => { + const okButton = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-primary', + ); + expect(okButton).toBeTruthy(); + // The OK button should have loading state + const loadingIndicator = document.querySelector( + '.ant-popconfirm-buttons .ant-btn-loading', + ); + expect(loadingIndicator).toBeTruthy(); + }); + }, +}; diff --git a/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts b/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts new file mode 100644 index 000000000..510c7fbfd --- /dev/null +++ b/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts @@ -0,0 +1,80 @@ +import { expect, userEvent, waitFor, within } from 'storybook/test'; + +const POPCONFIRM_SELECTORS = { + title: '.ant-popconfirm-title', + description: '.ant-popconfirm-description', + confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', + cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', +} as const; + +type Canvas = ReturnType; +type PopconfirmAction = 'confirm' | 'cancel'; + +const waitForPopconfirm = async (timeoutMs = 3000) => + waitFor( + () => { + const title = document.querySelector(POPCONFIRM_SELECTORS.title); + if (!title) throw new Error('Popconfirm not found'); + return title; + }, + { timeout: timeoutMs }, + ); + +const getPopconfirmElements = () => ({ + title: document.querySelector(POPCONFIRM_SELECTORS.title), + description: document.querySelector(POPCONFIRM_SELECTORS.description), + confirmButton: document.querySelector(POPCONFIRM_SELECTORS.confirmButton), + cancelButton: document.querySelector(POPCONFIRM_SELECTORS.cancelButton), +}); + +export const triggerPopconfirmAnd = async ( + canvas: Canvas, + action: PopconfirmAction, + options?: { + triggerButtonLabel?: string | RegExp; + triggerButtonIndex?: number; + expectedTitle?: string; + expectedDescription?: string; + }, +) => { + const { + triggerButtonLabel, + triggerButtonIndex = 0, + expectedTitle, + expectedDescription, + } = options ?? {}; + + let triggerButton: HTMLElement | undefined; + + if (triggerButtonLabel) { + triggerButton = (await canvas.findByRole('button', { + name: triggerButtonLabel, + })) as HTMLElement; + } else { + const buttons = canvas.getAllByRole('button'); + triggerButton = buttons[triggerButtonIndex] as HTMLElement | undefined; + } + + expect(triggerButton).toBeTruthy(); + + if (!triggerButton) return; + + await userEvent.click(triggerButton); + await waitForPopconfirm(); + + const { title, description, confirmButton, cancelButton } = + getPopconfirmElements(); + + if (expectedTitle) { + expect(title?.textContent).toContain(expectedTitle); + } + if (expectedDescription) { + expect(description?.textContent).toContain(expectedDescription); + } + + const target = action === 'confirm' ? confirmButton : cancelButton; + + if (target) { + await userEvent.click(target); + } +}; From 1637b6a2f0857901b904f53df716374174e60996 Mon Sep 17 00:00:00 2001 From: Lian Date: Mon, 22 Dec 2025 09:45:07 -0500 Subject: [PATCH 16/20] remove duplicated popconfirm util logic --- .../listing-information.container.stories.tsx | 2 +- .../listing-information.stories.tsx | 2 +- .../stories/reservation-actions.stories.tsx | 2 +- .../popconfirm-test-utils.stories.tsx | 238 ------------------ .../src/test-utils/popconfirm-test-utils.ts | 113 --------- .../reservation-request/cancel.test.ts | 14 +- .../reservation-request/cancel.ts | 12 +- packages/sthrift/ui-components/src/index.ts | 5 + .../src/test-utils/popconfirm-test-utils.ts | 33 +++ 9 files changed, 56 insertions(+), 365 deletions(-) delete mode 100644 apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx delete mode 100644 apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx index 58ac5ce73..46b8d36fb 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.container.stories.tsx @@ -12,7 +12,7 @@ import { HomeListingInformationCancelReservationRequestDocument, ViewListingActiveReservationRequestForListingDocument, } from '../../../../../../generated.tsx'; -import { clickCancelThenConfirm } from '../../../../../../test-utils/popconfirm-test-utils.ts'; +import { clickCancelThenConfirm } from '@sthrift/ui-components'; const mockListing = { __typename: 'ItemListing' as const, diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx index 2e2c580fa..40935bfa4 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/listing-information/listing-information.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, within, userEvent, fn } from 'storybook/test'; import { ListingInformation } from './listing-information.tsx'; import { withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; -import { triggerPopconfirmAnd } from '../../../../../../test-utils/popconfirm-test-utils.ts'; +import { triggerPopconfirmAnd } from '@sthrift/ui-components'; const baseReservationRequest = { __typename: 'ReservationRequest' as const, diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx index bbc43d8aa..cba53aaa7 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-reservations/stories/reservation-actions.stories.tsx @@ -4,7 +4,7 @@ import { expect, fn, within } from 'storybook/test'; import { triggerPopconfirmAnd, getLoadingIndicators, -} from '../../../../../test-utils/popconfirm-test-utils.ts'; +} from '@sthrift/ui-components'; const meta: Meta = { title: 'Molecules/ReservationActions', diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx deleted file mode 100644 index 2998ef0f3..000000000 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.stories.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { Button, Popconfirm } from 'antd'; -import { expect, fn, within } from 'storybook/test'; -import { - triggerPopconfirmAnd, - clickCancelThenConfirm, - getLoadingIndicators, -} from './popconfirm-test-utils.ts'; - -/** - * Demo component that wraps a button with a Popconfirm for testing purposes. - * This component is used to test the popconfirm-test-utils helper functions. - */ -const PopconfirmDemo = ({ - onConfirm, - onCancel, - buttonLabel = 'Cancel', - title = 'Confirm Action', - description = 'Are you sure you want to proceed?', - loading = false, -}: { - onConfirm?: () => void; - onCancel?: () => void; - buttonLabel?: string; - title?: string; - description?: string; - loading?: boolean; -}) => ( - - - -); - -/** - * Multi-button demo to test triggerButtonIndex option - */ -const MultiButtonPopconfirmDemo = ({ - onConfirm, - onCancel, -}: { - onConfirm?: () => void; - onCancel?: () => void; -}) => ( -
- - - - - -
-); - -const meta: Meta = { - title: 'Test Utils/PopconfirmTestUtils', - component: PopconfirmDemo, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], -}; - -export default meta; -type Story = StoryObj; - -/** - * Tests triggerPopconfirmAnd with 'confirm' action using triggerButtonLabel option. - * This covers the triggerButtonLabel path and expectedTitle/expectedDescription assertions. - */ -export const TriggerPopconfirmAndConfirm: Story = { - args: { - onConfirm: fn(), - onCancel: fn(), - buttonLabel: 'Cancel', - title: 'Confirm Action', - description: 'Are you sure you want to proceed?', - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'confirm', { - triggerButtonLabel: /Cancel/i, - expectedTitle: 'Confirm Action', - expectedDescription: 'Are you sure', - }); - - expect(args.onConfirm).toHaveBeenCalled(); - expect(args.onCancel).not.toHaveBeenCalled(); - }, -}; - -/** - * Tests triggerPopconfirmAnd with 'cancel' action. - * This covers the cancel action path where onConfirm should NOT be called. - */ -export const TriggerPopconfirmAndCancel: Story = { - args: { - onConfirm: fn(), - onCancel: fn(), - buttonLabel: 'Cancel', - title: 'Confirm Action', - description: 'Are you sure you want to proceed?', - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'cancel', { - triggerButtonLabel: /Cancel/i, - }); - - expect(args.onConfirm).not.toHaveBeenCalled(); - }, -}; - -/** - * Tests triggerPopconfirmAnd using triggerButtonIndex option (default behavior). - * This covers the path where no triggerButtonLabel is provided. - */ -export const TriggerPopconfirmByIndex: StoryObj< - typeof MultiButtonPopconfirmDemo -> = { - render: (args) => , - args: { - onConfirm: fn(), - onCancel: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - // Use index 1 to click the second button (the one with Popconfirm) - await triggerPopconfirmAnd(canvas, 'confirm', { - triggerButtonIndex: 1, - }); - - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; - -/** - * Tests clickCancelThenConfirm helper function. - * This covers the entire clickCancelThenConfirm flow. - */ -export const ClickCancelThenConfirmFlow: Story = { - args: { - onConfirm: fn(), - onCancel: fn(), - buttonLabel: 'Cancel', - title: 'Confirm Action', - description: 'Are you sure you want to proceed?', - }, - play: async ({ canvasElement, args }) => { - await clickCancelThenConfirm(canvasElement); - - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; - -/** - * Tests getLoadingIndicators helper function. - * This covers the loading indicator detection utility. - */ -export const LoadingIndicatorDetection: Story = { - args: { - onConfirm: fn(), - buttonLabel: 'Cancel', - loading: true, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Click button to open popconfirm - const button = canvas.getByRole('button', { name: /Cancel/i }); - const { userEvent } = await import('storybook/test'); - await userEvent.click(button); - - // Use getLoadingIndicators helper to find loading buttons - const loadingIndicators = getLoadingIndicators(document.body); - expect(loadingIndicators.length).toBeGreaterThan(0); - }, -}; - -/** - * Tests triggerPopconfirmAnd without optional parameters. - * This covers the default options path and ensures basic functionality works. - */ -export const TriggerPopconfirmDefaultOptions: Story = { - args: { - onConfirm: fn(), - onCancel: fn(), - buttonLabel: 'Cancel', - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - // Call with no options to test default behavior - await triggerPopconfirmAnd(canvas, 'confirm'); - - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; - -/** - * Tests getPopconfirmElements indirectly through the confirm flow. - * This validates that all popconfirm elements are found correctly. - */ -export const PopconfirmElementsValidation: Story = { - args: { - onConfirm: fn(), - buttonLabel: 'Cancel', - title: 'Test Title', - description: 'Test Description', - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - await triggerPopconfirmAnd(canvas, 'confirm', { - triggerButtonLabel: /Cancel/i, - expectedTitle: 'Test Title', - expectedDescription: 'Test Description', - }); - - expect(args.onConfirm).toHaveBeenCalled(); - }, -}; diff --git a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts b/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts deleted file mode 100644 index 9b93c80a1..000000000 --- a/apps/ui-sharethrift/src/test-utils/popconfirm-test-utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { expect, userEvent, waitFor, within } from 'storybook/test'; - -const POPCONFIRM_SELECTORS = { - title: '.ant-popconfirm-title', - description: '.ant-popconfirm-description', - confirmButton: '.ant-popconfirm-buttons .ant-btn-primary', - cancelButton: '.ant-popconfirm-buttons .ant-btn:not(.ant-btn-primary)', -} as const; - -type Canvas = ReturnType; -type PopconfirmAction = 'confirm' | 'cancel'; - -/** - * Centralized helper to find loading indicators on Ant Design buttons. - * This abstracts Ant Design implementation details so they can be updated in one place. - */ -export const getLoadingIndicators = (root: HTMLElement) => - root.querySelectorAll('.ant-btn-loading, [aria-busy="true"]'); - -const waitForPopconfirm = async (timeoutMs = 3000) => - waitFor( - () => { - const title = document.querySelector(POPCONFIRM_SELECTORS.title); - if (!title) throw new Error('Popconfirm not found'); - return title; - }, - { timeout: timeoutMs }, - ); - -const getPopconfirmElements = () => ({ - title: document.querySelector(POPCONFIRM_SELECTORS.title), - description: document.querySelector(POPCONFIRM_SELECTORS.description), - confirmButton: document.querySelector(POPCONFIRM_SELECTORS.confirmButton), - cancelButton: document.querySelector(POPCONFIRM_SELECTORS.cancelButton), -}); - -export const triggerPopconfirmAnd = async ( - canvas: Canvas, - action: PopconfirmAction, - options?: { - triggerButtonLabel?: string | RegExp; - triggerButtonIndex?: number; - expectedTitle?: string; - expectedDescription?: string; - }, -) => { - const { - triggerButtonLabel, - triggerButtonIndex = 0, - expectedTitle, - expectedDescription, - } = options ?? {}; - - let triggerButton: HTMLElement | undefined; - - if (triggerButtonLabel) { - triggerButton = (await canvas.findByRole('button', { - name: triggerButtonLabel, - })) as HTMLElement; - } else { - const buttons = canvas.getAllByRole('button'); - triggerButton = buttons[triggerButtonIndex] as HTMLElement | undefined; - } - - expect(triggerButton).toBeTruthy(); - - if (!triggerButton) return; - - await userEvent.click(triggerButton); - await waitForPopconfirm(); - - const { title, description, confirmButton, cancelButton } = - getPopconfirmElements(); - - if (expectedTitle) { - expect(title?.textContent).toContain(expectedTitle); - } - if (expectedDescription) { - expect(description?.textContent).toContain(expectedDescription); - } - - const target = action === 'confirm' ? confirmButton : cancelButton; - - if (target) { - await userEvent.click(target); - } -}; - -export const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { - const canvas = within(canvasElement); - - const cancelButton = await waitFor( - () => { - const btn = canvas.queryByRole('button', { name: /Cancel/i }); - if (!btn) throw new Error('Cancel button not found yet'); - return btn; - }, - { timeout: 3000 }, - ); - - await userEvent.click(cancelButton); - - const confirmButton = await waitFor( - () => { - const btn = document.querySelector(POPCONFIRM_SELECTORS.confirmButton); - if (!btn) throw new Error('Confirm button not found yet'); - return btn; - }, - { timeout: 3000 }, - ); - - await userEvent.click(confirmButton as HTMLElement); -}; diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts index 0ff4e0d5a..6a2662585 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -20,9 +20,21 @@ function buildReservation({ state: 'Requested' | 'Rejected' | 'Accepted'; reserverId: string; }) { + let currentState = state; return { id, - state, + get state() { + return currentState; + }, + set state(value: string) { + // Simulate domain entity state validation + if (value === 'Cancelled') { + if (currentState !== 'Requested' && currentState !== 'Rejected') { + throw new Error('Cannot cancel reservation in current state'); + } + } + currentState = value; + }, loadReserver: vi.fn().mockResolvedValue({ id: reserverId }), }; } diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts index c7b246878..cab45e96f 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts @@ -6,8 +6,6 @@ export interface ReservationRequestCancelCommand { callerId: string; } -const CANCELLABLE_STATES = ['Requested', 'Rejected'] as const; - export const cancel = (dataSources: DataSources) => { return async ( command: ReservationRequestCancelCommand, @@ -29,14 +27,8 @@ export const cancel = (dataSources: DataSources) => { ); } - if ( - !CANCELLABLE_STATES.includes( - reservationRequest.state as (typeof CANCELLABLE_STATES)[number], - ) - ) { - throw new Error('Cannot cancel reservation in current state'); - } - + // State setter delegates to domain entity's private cancel() method + // which handles state validation and permission checks reservationRequest.state = 'Cancelled'; reservationRequestToReturn = await repo.save(reservationRequest); }, diff --git a/packages/sthrift/ui-components/src/index.ts b/packages/sthrift/ui-components/src/index.ts index 055ad3c1a..48251aace 100644 --- a/packages/sthrift/ui-components/src/index.ts +++ b/packages/sthrift/ui-components/src/index.ts @@ -1,5 +1,10 @@ export type { UIItemListing } from './organisms/listings-grid/index.tsx'; // Barrel file for all reusable UI components +export { + getLoadingIndicators, + triggerPopconfirmAnd, + clickCancelThenConfirm, +} from './test-utils/popconfirm-test-utils.js'; export { Footer } from './molecules/footer/index.js'; export { Header } from './molecules/header/index.js'; export { Navigation } from './molecules/navigation/index.js'; diff --git a/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts b/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts index 510c7fbfd..9b93c80a1 100644 --- a/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts +++ b/packages/sthrift/ui-components/src/test-utils/popconfirm-test-utils.ts @@ -10,6 +10,13 @@ const POPCONFIRM_SELECTORS = { type Canvas = ReturnType; type PopconfirmAction = 'confirm' | 'cancel'; +/** + * Centralized helper to find loading indicators on Ant Design buttons. + * This abstracts Ant Design implementation details so they can be updated in one place. + */ +export const getLoadingIndicators = (root: HTMLElement) => + root.querySelectorAll('.ant-btn-loading, [aria-busy="true"]'); + const waitForPopconfirm = async (timeoutMs = 3000) => waitFor( () => { @@ -78,3 +85,29 @@ export const triggerPopconfirmAnd = async ( await userEvent.click(target); } }; + +export const clickCancelThenConfirm = async (canvasElement: HTMLElement) => { + const canvas = within(canvasElement); + + const cancelButton = await waitFor( + () => { + const btn = canvas.queryByRole('button', { name: /Cancel/i }); + if (!btn) throw new Error('Cancel button not found yet'); + return btn; + }, + { timeout: 3000 }, + ); + + await userEvent.click(cancelButton); + + const confirmButton = await waitFor( + () => { + const btn = document.querySelector(POPCONFIRM_SELECTORS.confirmButton); + if (!btn) throw new Error('Confirm button not found yet'); + return btn; + }, + { timeout: 3000 }, + ); + + await userEvent.click(confirmButton as HTMLElement); +}; From 72316e51abc8aa53ba23601edf664c4984c878dc Mon Sep 17 00:00:00 2001 From: Lian Date: Mon, 5 Jan 2026 10:07:13 -0500 Subject: [PATCH 17/20] Fix snyk vulnerabilities: upgrade express to 4.22.0 and override qs to 6.14.1 --- .../cellix/mock-payment-server/package.json | 2 +- .../mock-messaging-server/package.json | 3 +- pnpm-lock.yaml | 34 ++++++++----------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/cellix/mock-payment-server/package.json b/packages/cellix/mock-payment-server/package.json index bc8c4efa1..b34f69497 100644 --- a/packages/cellix/mock-payment-server/package.json +++ b/packages/cellix/mock-payment-server/package.json @@ -7,7 +7,7 @@ "license": "MIT", "dependencies": { "@cellix/payment-service": "workspace:*", - "express": "^4.18.2", + "express": "^4.22.0", "jose": "^5.10.0", "jsonwebtoken": "^9.0.3" }, diff --git a/packages/sthrift/mock-messaging-server/package.json b/packages/sthrift/mock-messaging-server/package.json index cd47e3636..f1875f446 100644 --- a/packages/sthrift/mock-messaging-server/package.json +++ b/packages/sthrift/mock-messaging-server/package.json @@ -5,7 +5,6 @@ "type": "module", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", - "license": "MIT", "scripts": { "prebuild": "biome lint", @@ -18,7 +17,7 @@ }, "dependencies": { "dotenv": "^16.6.1", - "express": "^4.18.2", + "express": "^4.22.0", "mongodb": "catalog:" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index deb2abe97..722f6ece1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -495,8 +495,8 @@ importers: specifier: workspace:* version: link:../payment-service express: - specifier: ^4.18.2 - version: 4.21.2 + specifier: ^4.22.0 + version: 4.22.0 jose: specifier: ^5.10.0 version: 5.10.0 @@ -921,8 +921,8 @@ importers: specifier: ^16.6.1 version: 16.6.1 express: - specifier: ^4.18.2 - version: 4.21.2 + specifier: ^4.22.0 + version: 4.22.0 mongodb: specifier: 'catalog:' version: 6.18.0 @@ -5887,10 +5887,6 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -6616,8 +6612,8 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + express@4.22.0: + resolution: {integrity: sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==} engines: {node: '>= 0.10.0'} express@4.22.1: @@ -17247,7 +17243,7 @@ snapshots: args: 5.0.3 axios: 0.27.2 etag: 1.8.1 - express: 4.21.2 + express: 4.22.1 fs-extra: 11.3.2 glob-to-regexp: 0.4.1 jsonwebtoken: 9.0.2 @@ -17375,7 +17371,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3(supports-color@8.1.1) - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.14.0 @@ -17839,8 +17835,6 @@ snapshots: cookie-signature@1.0.6: {} - cookie@0.7.1: {} - cookie@0.7.2: {} cookie@1.0.2: {} @@ -18707,14 +18701,14 @@ snapshots: expect-type@1.2.2: {} - express@4.21.2: + express@4.22.0: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.1 + cookie: 0.7.2 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 @@ -18723,20 +18717,20 @@ snapshots: etag: 1.8.1 finalhandler: 1.3.1 fresh: 0.5.2 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 serve-static: 1.16.2 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -18905,7 +18899,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color From d2d8d029993d5c3fdb8593fc63bb29588f253300 Mon Sep 17 00:00:00 2001 From: Lian Date: Mon, 5 Jan 2026 10:15:54 -0500 Subject: [PATCH 18/20] Revert security fixes - will address separately --- .../cellix/mock-payment-server/package.json | 2 +- .../mock-messaging-server/package.json | 3 +- pnpm-lock.yaml | 34 +++++++++++-------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/cellix/mock-payment-server/package.json b/packages/cellix/mock-payment-server/package.json index b34f69497..bc8c4efa1 100644 --- a/packages/cellix/mock-payment-server/package.json +++ b/packages/cellix/mock-payment-server/package.json @@ -7,7 +7,7 @@ "license": "MIT", "dependencies": { "@cellix/payment-service": "workspace:*", - "express": "^4.22.0", + "express": "^4.18.2", "jose": "^5.10.0", "jsonwebtoken": "^9.0.3" }, diff --git a/packages/sthrift/mock-messaging-server/package.json b/packages/sthrift/mock-messaging-server/package.json index f1875f446..cd47e3636 100644 --- a/packages/sthrift/mock-messaging-server/package.json +++ b/packages/sthrift/mock-messaging-server/package.json @@ -5,6 +5,7 @@ "type": "module", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", + "license": "MIT", "scripts": { "prebuild": "biome lint", @@ -17,7 +18,7 @@ }, "dependencies": { "dotenv": "^16.6.1", - "express": "^4.22.0", + "express": "^4.18.2", "mongodb": "catalog:" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 722f6ece1..deb2abe97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -495,8 +495,8 @@ importers: specifier: workspace:* version: link:../payment-service express: - specifier: ^4.22.0 - version: 4.22.0 + specifier: ^4.18.2 + version: 4.21.2 jose: specifier: ^5.10.0 version: 5.10.0 @@ -921,8 +921,8 @@ importers: specifier: ^16.6.1 version: 16.6.1 express: - specifier: ^4.22.0 - version: 4.22.0 + specifier: ^4.18.2 + version: 4.21.2 mongodb: specifier: 'catalog:' version: 6.18.0 @@ -5887,6 +5887,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -6612,8 +6616,8 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express@4.22.0: - resolution: {integrity: sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} express@4.22.1: @@ -17243,7 +17247,7 @@ snapshots: args: 5.0.3 axios: 0.27.2 etag: 1.8.1 - express: 4.22.1 + express: 4.21.2 fs-extra: 11.3.2 glob-to-regexp: 0.4.1 jsonwebtoken: 9.0.2 @@ -17371,7 +17375,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3(supports-color@8.1.1) - http-errors: 2.0.1 + http-errors: 2.0.0 iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.14.0 @@ -17835,6 +17839,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie@0.7.1: {} + cookie@0.7.2: {} cookie@1.0.2: {} @@ -18701,14 +18707,14 @@ snapshots: expect-type@1.2.2: {} - express@4.22.0: + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.2 + cookie: 0.7.1 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 @@ -18717,20 +18723,20 @@ snapshots: etag: 1.8.1 finalhandler: 1.3.1 fresh: 0.5.2 - http-errors: 2.0.1 + http-errors: 2.0.0 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.13.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 serve-static: 1.16.2 setprototypeof: 1.2.0 - statuses: 2.0.2 + statuses: 2.0.1 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -18899,7 +18905,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.2 + statuses: 2.0.1 transitivePeerDependencies: - supports-color From a04257e59bb0b2a43d4d62fe54cdea69aea09b82 Mon Sep 17 00:00:00 2001 From: Lian Date: Tue, 6 Jan 2026 13:57:26 -0500 Subject: [PATCH 19/20] setting state in cancel reservation request handles permission checks via visa --- .../reservation-request/cancel.test.ts | 25 +++++++++++++++---- .../reservation-request/cancel.ts | 9 +------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts index 6a2662585..86d1f7b8a 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.test.ts @@ -15,12 +15,14 @@ function buildReservation({ id, state, reserverId, + callerId, }: { id: string; state: 'Requested' | 'Rejected' | 'Accepted'; reserverId: string; + callerId?: string; }) { - let currentState = state; + let currentState: string = state; return { id, get state() { @@ -29,6 +31,12 @@ function buildReservation({ set state(value: string) { // Simulate domain entity state validation if (value === 'Cancelled') { + // Simulate visa permission check: only reserver can cancel + if (callerId && callerId !== reserverId) { + throw new Error( + 'You do not have permission to cancel this reservation request', + ); + } if (currentState !== 'Requested' && currentState !== 'Rejected') { throw new Error('Cannot cancel reservation in current state'); } @@ -49,11 +57,13 @@ function mockTransaction({ saveReturn?: unknown; }) { ( - // biome-ignore lint/suspicious/noExplicitAny: Test mock access + // biome-ignore lint/suspicious/noExplicitAny: Test mock type assertion dataSources.domainDataSource as any ).ReservationRequest.ReservationRequest.ReservationRequestUnitOfWork.withScopedTransaction.mockImplementation( - // biome-ignore lint/suspicious/noExplicitAny: Test mock callback - async (callback: any) => { + async ( + // biome-ignore lint/suspicious/noExplicitAny: Test mock callback + callback: any, + ) => { const mockRepo = { getById: vi.fn().mockResolvedValue(getByIdReturn), save: vi.fn().mockResolvedValue(saveReturn), @@ -121,6 +131,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { id: command.id, state: 'Requested', reserverId: command.callerId, + callerId: command.callerId, }); mockTransaction({ @@ -156,6 +167,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { id: command.id, state: 'Rejected', reserverId: command.callerId, + callerId: command.callerId, }); mockTransaction({ @@ -223,6 +235,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { id: command.id, state: 'Requested', reserverId: command.callerId, + callerId: command.callerId, }); mockTransaction({ @@ -260,6 +273,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { id: command.id, state: 'Accepted', reserverId: command.callerId, + callerId: command.callerId, }); mockTransaction({ @@ -299,6 +313,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { id: command.id, state: 'Requested', reserverId: 'user-123', // Different from command.callerId + callerId: command.callerId, }); mockTransaction({ @@ -319,7 +334,7 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { () => { expect(error).toBeDefined(); expect(error.message).toBe( - 'Only the reserver can cancel their reservation request', + 'You do not have permission to cancel this reservation request', ); }, ); diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts index cab45e96f..54957d796 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/cancel.ts @@ -20,15 +20,8 @@ export const cancel = (dataSources: DataSources) => { throw new Error('Reservation request not found'); } - const reserver = await reservationRequest.loadReserver(); - if (reserver.id !== command.callerId) { - throw new Error( - 'Only the reserver can cancel their reservation request', - ); - } - // State setter delegates to domain entity's private cancel() method - // which handles state validation and permission checks + // which handles state validation and permission checks via visa reservationRequest.state = 'Cancelled'; reservationRequestToReturn = await repo.save(reservationRequest); }, From 19ef66598e9d2af80c55315429baed33fa661763 Mon Sep 17 00:00:00 2001 From: Lian Date: Thu, 8 Jan 2026 09:48:25 -0500 Subject: [PATCH 20/20] changed .js to .tsx --- packages/sthrift/ui-components/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sthrift/ui-components/src/index.ts b/packages/sthrift/ui-components/src/index.ts index eb74fb520..c6ed1d6ae 100644 --- a/packages/sthrift/ui-components/src/index.ts +++ b/packages/sthrift/ui-components/src/index.ts @@ -15,4 +15,4 @@ export { ListingsGrid } from './organisms/listings-grid/index.tsx'; export { ComponentQueryLoader } from './molecules/component-query-loader/index.tsx'; export { Dashboard } from './organisms/dashboard/index.tsx'; export { ReservationStatusTag } from './atoms/reservation-status-tag/index.tsx'; -export { CancelReservationPopconfirm } from './components/cancel-reservation-popconfirm/index.js'; +export { CancelReservationPopconfirm } from './components/cancel-reservation-popconfirm/index.tsx';