Skip to content

Commit 88ed935

Browse files
authored
test(storybook): add stories for FormControl, HelpText, and InputValidationMessage (#264)
1 parent 89a1c36 commit 88ed935

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react-vite';
3+
import { expect } from 'storybook/test';
4+
import { FormControl } from './FormControl';
5+
6+
const meta = {
7+
component: FormControl,
8+
tags: ['ai-generated'],
9+
} satisfies Meta<typeof FormControl>;
10+
11+
export default meta;
12+
type Story = StoryObj<typeof meta>;
13+
14+
export const Default: Story = {
15+
args: {
16+
id: 'first-name',
17+
label: 'First name',
18+
},
19+
render: (args) => (
20+
<FormControl {...args}>
21+
<input id={args.id} type="text" />
22+
</FormControl>
23+
),
24+
play: async ({ canvas }) => {
25+
// Proves the FormLabel htmlFor/input id wiring, not just that it mounted.
26+
await expect(canvas.getByLabelText('First name')).toBeVisible();
27+
},
28+
};
29+
30+
export const WithHelpText: Story = {
31+
args: {
32+
id: 'email',
33+
label: 'Email address',
34+
helpText: 'We will never share your email.',
35+
},
36+
render: (args) => (
37+
<FormControl {...args}>
38+
<input id={args.id} type="email" />
39+
</FormControl>
40+
),
41+
};
42+
43+
export const WithError: Story = {
44+
args: {
45+
id: 'password',
46+
label: 'Password',
47+
error: 'Password must be at least 8 characters.',
48+
},
49+
render: (args) => (
50+
<FormControl {...args}>
51+
<input id={args.id} type="password" />
52+
</FormControl>
53+
),
54+
play: async ({ canvas }) => {
55+
// The error prop renders as an InputValidationMessage below the input.
56+
await expect(
57+
canvas.getByText('Password must be at least 8 characters.')
58+
).toBeVisible();
59+
},
60+
};
61+
62+
export const Required: Story = {
63+
args: {
64+
id: 'username',
65+
label: 'Username',
66+
isRequired: true,
67+
},
68+
render: (args) => (
69+
<FormControl {...args}>
70+
<input id={args.id} type="text" required />
71+
</FormControl>
72+
),
73+
};
74+
75+
export const HiddenLabel: Story = {
76+
args: {
77+
id: 'search',
78+
label: 'Search',
79+
hideLabel: true,
80+
},
81+
render: (args) => (
82+
<FormControl {...args}>
83+
<input id={args.id} type="search" aria-label={args.label} />
84+
</FormControl>
85+
),
86+
play: async ({ canvas }) => {
87+
// hideLabel suppresses the visible FormLabel entirely.
88+
await expect(canvas.queryByText('Search')).not.toBeInTheDocument();
89+
// The input must still be accessible via its aria-label.
90+
await expect(canvas.getByLabelText('Search')).toBeVisible();
91+
},
92+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react-vite';
3+
import { HelpText } from './HelpText';
4+
import { Box } from '../Box/Box';
5+
6+
const meta = {
7+
component: HelpText,
8+
tags: ['ai-generated'],
9+
} satisfies Meta<typeof HelpText>;
10+
11+
export default meta;
12+
type Story = StoryObj<typeof meta>;
13+
14+
export const Default: Story = {
15+
args: {
16+
children: 'Additional clarifying text to help describe the input.',
17+
},
18+
};
19+
20+
export const BelowAnInput: Story = {
21+
render: () => (
22+
<Box gap="2xs">
23+
<label htmlFor="help-text-example">Email address</label>
24+
<input
25+
id="help-text-example"
26+
type="email"
27+
aria-describedby="help-text-example-hint"
28+
/>
29+
{/* HelpText doesn't forward an id, so associate via a wrapper. */}
30+
<div id="help-text-example-hint">
31+
<HelpText>We will only use this to send your receipt.</HelpText>
32+
</div>
33+
</Box>
34+
),
35+
};
36+
37+
export const WithRichContent: Story = {
38+
args: {
39+
children: (
40+
<>
41+
Must contain at least <strong>8 characters</strong>.
42+
</>
43+
),
44+
},
45+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react-vite';
3+
import { expect } from 'storybook/test';
4+
import { InputValidationMessage } from './InputValidationMessage';
5+
import { Box } from '../Box/Box';
6+
7+
const meta = {
8+
component: InputValidationMessage,
9+
tags: ['ai-generated'],
10+
} satisfies Meta<typeof InputValidationMessage>;
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {
16+
args: {
17+
children: 'This field is required.',
18+
},
19+
};
20+
21+
export const Sizes: Story = {
22+
args: {
23+
children: 'This field is required.',
24+
},
25+
render: () => (
26+
<Box gap="sm">
27+
<InputValidationMessage size="xs">
28+
Extra small validation message
29+
</InputValidationMessage>
30+
<InputValidationMessage size="sm">
31+
Small validation message
32+
</InputValidationMessage>
33+
<InputValidationMessage size="md">
34+
Medium validation message
35+
</InputValidationMessage>
36+
</Box>
37+
),
38+
};
39+
40+
export const CssCheck: Story = {
41+
args: {
42+
children: 'This field is required.',
43+
},
44+
play: async ({ canvas }) => {
45+
const message = canvas.getByText('This field is required.');
46+
// font-color-danger resolves to --color-font-danger (#dc2626 in the light
47+
// theme) — fails if the design-token CSS did not load in the preview.
48+
await expect(getComputedStyle(message).color).toBe('rgb(220, 38, 38)');
49+
},
50+
};

0 commit comments

Comments
 (0)