|
1 | 1 | import { DiscountValueTypeEnum } from "@dashboard/graphql"; |
2 | 2 | import type { Meta, StoryObj } from "@storybook/react-vite"; |
3 | | -import { fn } from "storybook/test"; |
| 3 | +import { type ComponentProps } from "react"; |
| 4 | +import { expect, fn, userEvent, within } from "storybook/test"; |
4 | 5 |
|
5 | 6 | import { OrderDiscountModal } from "./OrderDiscountModal"; |
6 | 7 | import { type OrderDiscountCommonInput } from "./types"; |
7 | 8 |
|
| 9 | +type Props = ComponentProps<typeof OrderDiscountModal>; |
| 10 | + |
8 | 11 | const defaultMaxPrice = { |
9 | 12 | __typename: "Money" as const, |
10 | 13 | currency: "USD", |
@@ -50,6 +53,15 @@ const meta: Meta<typeof OrderDiscountModal> = { |
50 | 53 | export default meta; |
51 | 54 | type Story = StoryObj<typeof OrderDiscountModal>; |
52 | 55 |
|
| 56 | +// DashboardModal renders in a Radix portal, so we find the dialog by role |
| 57 | +// within document.body and query inside it. |
| 58 | +const findDialog = async () => { |
| 59 | + const body = within(document.body); |
| 60 | + const dialog = await body.findByRole("dialog"); |
| 61 | + |
| 62 | + return within(dialog); |
| 63 | +}; |
| 64 | + |
53 | 65 | export const NewDiscount: Story = {}; |
54 | 66 |
|
55 | 67 | export const EditPercentageDiscount: Story = { |
@@ -77,3 +89,83 @@ export const RemoveLoading: Story = { |
77 | 89 | removeStatus: "loading", |
78 | 90 | }, |
79 | 91 | }; |
| 92 | + |
| 93 | +export const SubmitsNewDiscount: Story = { |
| 94 | + play: async ({ args }: { args: Props }) => { |
| 95 | + const dialog = await findDialog(); |
| 96 | + |
| 97 | + await userEvent.type(dialog.getByTestId("discount-value"), "15"); |
| 98 | + await userEvent.type(dialog.getByTestId("discount-reason"), "Referral promotion"); |
| 99 | + await userEvent.click(dialog.getByTestId("submit")); |
| 100 | + |
| 101 | + await expect(args.onConfirm).toHaveBeenCalledOnce(); |
| 102 | + await expect(args.onConfirm).toHaveBeenCalledWith({ |
| 103 | + calculationMode: DiscountValueTypeEnum.PERCENTAGE, |
| 104 | + reason: "Referral promotion", |
| 105 | + value: 15, |
| 106 | + }); |
| 107 | + }, |
| 108 | +}; |
| 109 | + |
| 110 | +export const ConvertsValueWhenSwitchingCalculationMode: Story = { |
| 111 | + args: { |
| 112 | + existingDiscount: existingPercentageDiscount, |
| 113 | + }, |
| 114 | + play: async ({ args }: { args: Props }) => { |
| 115 | + const dialog = await findDialog(); |
| 116 | + |
| 117 | + // Switching PERCENTAGE (10%) -> FIXED with maxPrice=250 should compute 25. |
| 118 | + await userEvent.click(dialog.getByTestId("FIXED")); |
| 119 | + await userEvent.click(dialog.getByTestId("submit")); |
| 120 | + |
| 121 | + await expect(args.onConfirm).toHaveBeenCalledOnce(); |
| 122 | + await expect(args.onConfirm).toHaveBeenCalledWith({ |
| 123 | + calculationMode: DiscountValueTypeEnum.FIXED, |
| 124 | + reason: "Loyal customer", |
| 125 | + value: 25, |
| 126 | + }); |
| 127 | + }, |
| 128 | +}; |
| 129 | + |
| 130 | +export const ShowsErrorWhenPercentageAbove100: Story = { |
| 131 | + play: async ({ args }: { args: Props }) => { |
| 132 | + const dialog = await findDialog(); |
| 133 | + |
| 134 | + await userEvent.type(dialog.getByTestId("discount-value"), "150"); |
| 135 | + |
| 136 | + await expect(await dialog.findByText("Cannot be higher than 100%")).toBeInTheDocument(); |
| 137 | + await expect(dialog.getByTestId("submit")).toBeDisabled(); |
| 138 | + |
| 139 | + // onConfirm must not be called while the form is invalid. |
| 140 | + await userEvent.click(dialog.getByTestId("submit")); |
| 141 | + await expect(args.onConfirm).not.toHaveBeenCalled(); |
| 142 | + }, |
| 143 | +}; |
| 144 | + |
| 145 | +export const RemovesExistingDiscount: Story = { |
| 146 | + args: { |
| 147 | + existingDiscount: existingPercentageDiscount, |
| 148 | + }, |
| 149 | + play: async ({ args }: { args: Props }) => { |
| 150 | + const dialog = await findDialog(); |
| 151 | + |
| 152 | + await userEvent.click(dialog.getByTestId("button-remove")); |
| 153 | + |
| 154 | + await expect(args.onRemove).toHaveBeenCalledOnce(); |
| 155 | + }, |
| 156 | +}; |
| 157 | + |
| 158 | +export const CloseButtonIsAccessible: Story = { |
| 159 | + play: async ({ args }: { args: Props }) => { |
| 160 | + const dialog = await findDialog(); |
| 161 | + const closeButton = dialog.getByTestId("close-button"); |
| 162 | + |
| 163 | + // The icon-only button must expose a name to assistive technologies |
| 164 | + // and a native tooltip via `title`. |
| 165 | + await expect(closeButton).toHaveAttribute("aria-label", "Close"); |
| 166 | + await expect(closeButton).toHaveAttribute("title", "Close"); |
| 167 | + |
| 168 | + await userEvent.click(closeButton); |
| 169 | + await expect(args.onClose).toHaveBeenCalled(); |
| 170 | + }, |
| 171 | +}; |
0 commit comments