Skip to content

Commit ddc6608

Browse files
committed
Extend storybook with interactions
1 parent 6c13c48 commit ddc6608

3 files changed

Lines changed: 164 additions & 3 deletions

File tree

src/orders/components/OrderDiscountModal/DiscountFormFields.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const DiscountFormFields = ({
6969
helperText={valueErrorMsg || ""}
7070
value={field.value}
7171
onChange={field.onChange}
72+
data-test-id="discount-value"
7273
/>
7374
)}
7475
/>

src/orders/components/OrderDiscountModal/OrderDiscountModal.stories.tsx

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { DiscountValueTypeEnum } from "@dashboard/graphql";
22
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";
45

56
import { OrderDiscountModal } from "./OrderDiscountModal";
67
import { type OrderDiscountCommonInput } from "./types";
78

9+
type Props = ComponentProps<typeof OrderDiscountModal>;
10+
811
const defaultMaxPrice = {
912
__typename: "Money" as const,
1013
currency: "USD",
@@ -50,6 +53,15 @@ const meta: Meta<typeof OrderDiscountModal> = {
5053
export default meta;
5154
type Story = StoryObj<typeof OrderDiscountModal>;
5255

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+
5365
export const NewDiscount: Story = {};
5466

5567
export const EditPercentageDiscount: Story = {
@@ -77,3 +89,83 @@ export const RemoveLoading: Story = {
7789
removeStatus: "loading",
7890
},
7991
};
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+
};

src/orders/components/OrderDiscountModal/OrderLineDiscountModal.stories.tsx

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { DiscountValueTypeEnum } from "@dashboard/graphql";
1+
import { DiscountValueTypeEnum, OrderDiscountType } from "@dashboard/graphql";
22
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";
45

56
import { OrderLineDiscountModal } from "./OrderLineDiscountModal";
67
import { type OrderDiscountCommonInput } from "./types";
78

9+
type Props = ComponentProps<typeof OrderLineDiscountModal>;
10+
811
const defaultMaxPrice = {
912
__typename: "Money" as const,
1013
currency: "USD",
@@ -55,6 +58,13 @@ const meta: Meta<typeof OrderLineDiscountModal> = {
5558
export default meta;
5659
type Story = StoryObj<typeof OrderLineDiscountModal>;
5760

61+
const findDialog = async () => {
62+
const body = within(document.body);
63+
const dialog = await body.findByRole("dialog");
64+
65+
return within(dialog);
66+
};
67+
5868
export const NewDiscount: Story = {};
5969

6070
export const WithExistingDiscount: Story = {
@@ -123,3 +133,61 @@ export const ConfirmLoading: Story = {
123133
confirmStatus: "loading",
124134
},
125135
};
136+
137+
export const WithAutomaticPromotion: Story = {
138+
args: {
139+
automaticDiscounts: [
140+
{
141+
type: OrderDiscountType.PROMOTION,
142+
name: "Summer Sale 2024",
143+
},
144+
],
145+
},
146+
play: async () => {
147+
const dialog = await findDialog();
148+
149+
// The promotion name is rendered inside its own bolded <span> to stand
150+
// out within the callout, while the rest of the sentence stays default.
151+
const promotionName = await dialog.findByText(/Summer Sale 2024/);
152+
153+
await expect(promotionName.tagName.toLowerCase()).toBe("span");
154+
await expect(dialog.getByText(/already discounted by/i)).toBeInTheDocument();
155+
await expect(
156+
dialog.getByText("A manual discount below will replace the existing one."),
157+
).toBeInTheDocument();
158+
},
159+
};
160+
161+
export const WithAutomaticVoucher: Story = {
162+
args: {
163+
automaticDiscounts: [
164+
{
165+
type: OrderDiscountType.VOUCHER,
166+
name: "WELCOME10",
167+
},
168+
],
169+
},
170+
play: async () => {
171+
const dialog = await findDialog();
172+
const voucherName = await dialog.findByText(/WELCOME10/);
173+
174+
await expect(voucherName.tagName.toLowerCase()).toBe("span");
175+
await expect(dialog.getByText(/voucher/i)).toBeInTheDocument();
176+
},
177+
};
178+
179+
export const SubmitsLineDiscount: Story = {
180+
play: async ({ args }: { args: Props }) => {
181+
const dialog = await findDialog();
182+
183+
await userEvent.type(dialog.getByTestId("discount-value"), "20");
184+
await userEvent.click(dialog.getByTestId("submit"));
185+
186+
await expect(args.onConfirm).toHaveBeenCalledOnce();
187+
await expect(args.onConfirm).toHaveBeenCalledWith({
188+
calculationMode: DiscountValueTypeEnum.PERCENTAGE,
189+
reason: "",
190+
value: 20,
191+
});
192+
},
193+
};

0 commit comments

Comments
 (0)