@dowel-ui/react 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/package.json +7 -4
- package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
- package/src/blocks/admin-users/admin-users.test.tsx +0 -159
- package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
- package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
- package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
- package/src/blocks/dashboard/dashboard.test.tsx +0 -94
- package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
- package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
- package/src/blocks/login/login.stories.tsx +0 -46
- package/src/blocks/login/login.test.tsx +0 -100
- package/src/blocks/pricing/pricing.stories.tsx +0 -62
- package/src/blocks/pricing/pricing.test.tsx +0 -101
- package/src/blocks/settings/settings.stories.tsx +0 -94
- package/src/blocks/settings/settings.test.tsx +0 -167
- package/src/blocks/signup/signup.stories.tsx +0 -27
- package/src/blocks/signup/signup.test.tsx +0 -97
- package/src/components/accordion/accordion.stories.tsx +0 -90
- package/src/components/accordion/accordion.test.tsx +0 -156
- package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
- package/src/components/activity-feed/activity-feed.test.tsx +0 -125
- package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
- package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
- package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
- package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
- package/src/components/ai-message/ai-message.stories.tsx +0 -106
- package/src/components/ai-message/ai-message.test.tsx +0 -160
- package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
- package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
- package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
- package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
- package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
- package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
- package/src/components/ai-response/ai-response.stories.tsx +0 -75
- package/src/components/ai-response/ai-response.test.tsx +0 -96
- package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
- package/src/components/ai-sources/ai-sources.test.tsx +0 -102
- package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
- package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
- package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
- package/src/components/ai-tool/ai-tool.test.tsx +0 -137
- package/src/components/alert/alert.stories.tsx +0 -101
- package/src/components/alert/alert.test.tsx +0 -94
- package/src/components/avatar/avatar.stories.tsx +0 -58
- package/src/components/avatar/avatar.test.tsx +0 -64
- package/src/components/badge/badge.stories.tsx +0 -68
- package/src/components/badge/badge.test.tsx +0 -61
- package/src/components/button/button.stories.tsx +0 -128
- package/src/components/button/button.test.tsx +0 -224
- package/src/components/calendar/calendar.stories.tsx +0 -105
- package/src/components/calendar/calendar.test.tsx +0 -255
- package/src/components/card/card.stories.tsx +0 -61
- package/src/components/card/card.test.tsx +0 -66
- package/src/components/checkbox/checkbox.stories.tsx +0 -124
- package/src/components/checkbox/checkbox.test.tsx +0 -121
- package/src/components/code-block/code-block.stories.tsx +0 -62
- package/src/components/code-block/code-block.test.tsx +0 -139
- package/src/components/combobox/combobox.stories.tsx +0 -171
- package/src/components/combobox/combobox.test.tsx +0 -429
- package/src/components/command/command.stories.tsx +0 -152
- package/src/components/command/command.test.tsx +0 -380
- package/src/components/data-table/data-table.stories.tsx +0 -151
- package/src/components/data-table/data-table.test.tsx +0 -288
- package/src/components/date-picker/date-picker.stories.tsx +0 -103
- package/src/components/date-picker/date-picker.test.tsx +0 -205
- package/src/components/dialog/dialog.stories.tsx +0 -140
- package/src/components/dialog/dialog.test.tsx +0 -186
- package/src/components/drawer/drawer.stories.tsx +0 -111
- package/src/components/drawer/drawer.test.tsx +0 -187
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
- package/src/components/empty-state/empty-state.stories.tsx +0 -70
- package/src/components/empty-state/empty-state.test.tsx +0 -97
- package/src/components/form/form.stories.tsx +0 -144
- package/src/components/form/form.test.tsx +0 -203
- package/src/components/input/input.stories.tsx +0 -84
- package/src/components/input/input.test.tsx +0 -108
- package/src/components/label/label.stories.tsx +0 -51
- package/src/components/label/label.test.tsx +0 -55
- package/src/components/pagination/pagination.stories.tsx +0 -129
- package/src/components/pagination/pagination.test.tsx +0 -119
- package/src/components/popover/popover.stories.tsx +0 -84
- package/src/components/popover/popover.test.tsx +0 -163
- package/src/components/progress/progress.stories.tsx +0 -90
- package/src/components/progress/progress.test.tsx +0 -78
- package/src/components/radio-group/radio-group.stories.tsx +0 -102
- package/src/components/radio-group/radio-group.test.tsx +0 -152
- package/src/components/select/select.stories.tsx +0 -137
- package/src/components/select/select.test.tsx +0 -209
- package/src/components/separator/separator.stories.tsx +0 -42
- package/src/components/separator/separator.test.tsx +0 -46
- package/src/components/sheet/sheet.stories.tsx +0 -107
- package/src/components/sheet/sheet.test.tsx +0 -152
- package/src/components/skeleton/skeleton.stories.tsx +0 -46
- package/src/components/skeleton/skeleton.test.tsx +0 -57
- package/src/components/slider/slider.stories.tsx +0 -87
- package/src/components/slider/slider.test.tsx +0 -148
- package/src/components/spinner/spinner.stories.tsx +0 -47
- package/src/components/spinner/spinner.test.tsx +0 -41
- package/src/components/switch/switch.stories.tsx +0 -70
- package/src/components/switch/switch.test.tsx +0 -101
- package/src/components/table/table.stories.tsx +0 -131
- package/src/components/table/table.test.tsx +0 -153
- package/src/components/tabs/tabs.stories.tsx +0 -139
- package/src/components/tabs/tabs.test.tsx +0 -148
- package/src/components/toast/toast.stories.tsx +0 -167
- package/src/components/toast/toast.test.tsx +0 -306
- package/src/components/tooltip/tooltip.stories.tsx +0 -89
- package/src/components/tooltip/tooltip.test.tsx +0 -120
- package/src/lib/utils.test.ts +0 -32
- package/src/registry/meta.test.ts +0 -234
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { SettingsBlock, type SettingsNotification } from "./settings";
|
|
5
|
-
|
|
6
|
-
/** Named so its type is nameable in declaration output (TS2883). */
|
|
7
|
-
const withPageWidth: Decorator = (Story) => (
|
|
8
|
-
<div className="w-[48rem] max-w-full">
|
|
9
|
-
<Story />
|
|
10
|
-
</div>
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const PROFILE = {
|
|
14
|
-
name: "Ada Lovelace",
|
|
15
|
-
email: "ada@example.com",
|
|
16
|
-
bio: "Working on analytical engines.",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const NOTIFICATIONS: SettingsNotification[] = [
|
|
20
|
-
{
|
|
21
|
-
id: "deploys",
|
|
22
|
-
label: "Deployment updates",
|
|
23
|
-
description: "When a deploy starts or finishes.",
|
|
24
|
-
enabled: true,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: "failures",
|
|
28
|
-
label: "Build failures",
|
|
29
|
-
description: "Only when something breaks.",
|
|
30
|
-
enabled: true,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: "digest",
|
|
34
|
-
label: "Weekly digest",
|
|
35
|
-
description: "A summary every Monday morning.",
|
|
36
|
-
enabled: false,
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const meta = {
|
|
41
|
-
title: "Blocks/Settings",
|
|
42
|
-
component: SettingsBlock,
|
|
43
|
-
args: { profile: PROFILE, notifications: NOTIFICATIONS },
|
|
44
|
-
parameters: { controls: { disable: true } },
|
|
45
|
-
decorators: [withPageWidth],
|
|
46
|
-
} satisfies Meta<typeof SettingsBlock>;
|
|
47
|
-
|
|
48
|
-
export default meta;
|
|
49
|
-
type Story = StoryObj<typeof meta>;
|
|
50
|
-
|
|
51
|
-
export const Default: Story = {};
|
|
52
|
-
|
|
53
|
-
/** Switches apply immediately. There is no Save button, and that is the point. */
|
|
54
|
-
export const Interactive: Story = {
|
|
55
|
-
render: function Interactive() {
|
|
56
|
-
const [notifications, setNotifications] = useState(NOTIFICATIONS);
|
|
57
|
-
const [pending, setPending] = useState(false);
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<SettingsBlock
|
|
61
|
-
profile={PROFILE}
|
|
62
|
-
notifications={notifications}
|
|
63
|
-
pending={pending}
|
|
64
|
-
onToggleNotification={(id, enabled) => {
|
|
65
|
-
setNotifications((current) =>
|
|
66
|
-
current.map((item) => (item.id === id ? { ...item, enabled } : item)),
|
|
67
|
-
);
|
|
68
|
-
}}
|
|
69
|
-
onSaveProfile={() => {
|
|
70
|
-
setPending(true);
|
|
71
|
-
window.setTimeout(() => {
|
|
72
|
-
setPending(false);
|
|
73
|
-
}, 1000);
|
|
74
|
-
}}
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const WithExtraTab: Story = {
|
|
81
|
-
args: {
|
|
82
|
-
extraTabs: [
|
|
83
|
-
{
|
|
84
|
-
value: "billing",
|
|
85
|
-
label: "Billing",
|
|
86
|
-
content: (
|
|
87
|
-
<p className="p-6 text-sm text-muted-foreground">
|
|
88
|
-
You are on the Pro plan, billed yearly. Next invoice 1 October.
|
|
89
|
-
</p>
|
|
90
|
-
),
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
};
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { SettingsBlock, type SettingsNotification } from "./settings";
|
|
7
|
-
|
|
8
|
-
const PROFILE = { name: "Ada Lovelace", email: "ada@example.com", bio: "Building things." };
|
|
9
|
-
|
|
10
|
-
const NOTIFICATIONS: SettingsNotification[] = [
|
|
11
|
-
{
|
|
12
|
-
id: "deploys",
|
|
13
|
-
label: "Deployment updates",
|
|
14
|
-
description: "When a deploy finishes.",
|
|
15
|
-
enabled: true,
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: "digest",
|
|
19
|
-
label: "Weekly digest",
|
|
20
|
-
description: "A summary every Monday.",
|
|
21
|
-
enabled: false,
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
describe("SettingsBlock", () => {
|
|
26
|
-
it("opens on the profile tab", () => {
|
|
27
|
-
render(<SettingsBlock profile={PROFILE} notifications={NOTIFICATIONS} />);
|
|
28
|
-
expect(screen.getByLabelText("Name")).toHaveValue("Ada Lovelace");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("saves the profile as a staged change", async () => {
|
|
32
|
-
const onSaveProfile = vi.fn();
|
|
33
|
-
const user = userEvent.setup();
|
|
34
|
-
render(
|
|
35
|
-
<SettingsBlock
|
|
36
|
-
profile={PROFILE}
|
|
37
|
-
notifications={NOTIFICATIONS}
|
|
38
|
-
onSaveProfile={onSaveProfile}
|
|
39
|
-
/>,
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
await user.clear(screen.getByLabelText("Name"));
|
|
43
|
-
await user.type(screen.getByLabelText("Name"), "Ada L");
|
|
44
|
-
await user.click(screen.getByRole("button", { name: "Save changes" }));
|
|
45
|
-
|
|
46
|
-
expect(onSaveProfile).toHaveBeenCalledWith(expect.objectContaining({ name: "Ada L" }));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("refuses to save an invalid profile", async () => {
|
|
50
|
-
const onSaveProfile = vi.fn();
|
|
51
|
-
const user = userEvent.setup();
|
|
52
|
-
render(
|
|
53
|
-
<SettingsBlock
|
|
54
|
-
profile={PROFILE}
|
|
55
|
-
notifications={NOTIFICATIONS}
|
|
56
|
-
onSaveProfile={onSaveProfile}
|
|
57
|
-
/>,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
await user.clear(screen.getByLabelText("Email"));
|
|
61
|
-
await user.type(screen.getByLabelText("Email"), "nope");
|
|
62
|
-
await user.click(screen.getByRole("button", { name: "Save changes" }));
|
|
63
|
-
|
|
64
|
-
expect(onSaveProfile).not.toHaveBeenCalled();
|
|
65
|
-
expect(screen.getByLabelText("Email")).toHaveAttribute("aria-invalid", "true");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("notifications", () => {
|
|
69
|
-
it("apply immediately, with no Save button", async () => {
|
|
70
|
-
const onToggle = vi.fn();
|
|
71
|
-
const user = userEvent.setup();
|
|
72
|
-
render(
|
|
73
|
-
<SettingsBlock
|
|
74
|
-
profile={PROFILE}
|
|
75
|
-
notifications={NOTIFICATIONS}
|
|
76
|
-
onToggleNotification={onToggle}
|
|
77
|
-
/>,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
await user.click(screen.getByRole("tab", { name: "Notifications" }));
|
|
81
|
-
await user.click(await screen.findByRole("switch", { name: "Weekly digest" }));
|
|
82
|
-
|
|
83
|
-
expect(onToggle).toHaveBeenCalledWith("digest", true);
|
|
84
|
-
// A switch that needs saving is a broken promise.
|
|
85
|
-
expect(screen.queryByRole("button", { name: "Save changes" })).not.toBeInTheDocument();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("labels every switch", async () => {
|
|
89
|
-
const user = userEvent.setup();
|
|
90
|
-
render(<SettingsBlock profile={PROFILE} notifications={NOTIFICATIONS} />);
|
|
91
|
-
|
|
92
|
-
await user.click(screen.getByRole("tab", { name: "Notifications" }));
|
|
93
|
-
expect(await screen.findByRole("switch", { name: "Deployment updates" })).toBeChecked();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("the danger zone", () => {
|
|
98
|
-
async function openDialog() {
|
|
99
|
-
const user = userEvent.setup();
|
|
100
|
-
const onDeleteAccount = vi.fn();
|
|
101
|
-
render(
|
|
102
|
-
<SettingsBlock
|
|
103
|
-
profile={PROFILE}
|
|
104
|
-
notifications={NOTIFICATIONS}
|
|
105
|
-
onDeleteAccount={onDeleteAccount}
|
|
106
|
-
/>,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
await user.click(screen.getByRole("tab", { name: "Danger zone" }));
|
|
110
|
-
await user.click(await screen.findByRole("button", { name: "Delete account" }));
|
|
111
|
-
const dialog = await screen.findByRole("dialog");
|
|
112
|
-
return { user, onDeleteAccount, dialog };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
it("names what will be destroyed", async () => {
|
|
116
|
-
await openDialog();
|
|
117
|
-
// Not "Are you sure?" — that tells nobody anything they did not know.
|
|
118
|
-
expect(
|
|
119
|
-
screen.getByRole("dialog", { name: /Delete Ada Lovelace.s account\?/ }),
|
|
120
|
-
).toBeInTheDocument();
|
|
121
|
-
expect(screen.getByText(/permanently deletes the account/)).toBeInTheDocument();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("stays disabled until the email is typed exactly", async () => {
|
|
125
|
-
const { user, dialog } = await openDialog();
|
|
126
|
-
|
|
127
|
-
// Scoped to the dialog: while it is open the trigger behind it is inert,
|
|
128
|
-
// so a document-wide query finds only one button and the index is a trap.
|
|
129
|
-
const confirm = within(dialog).getByRole("button", { name: "Delete account" });
|
|
130
|
-
expect(confirm).toBeDisabled();
|
|
131
|
-
|
|
132
|
-
await user.type(screen.getByLabelText(/to confirm/), "ada@example.com");
|
|
133
|
-
await waitFor(() => {
|
|
134
|
-
expect(confirm).toBeEnabled();
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("deletes only after confirmation", async () => {
|
|
139
|
-
const { user, onDeleteAccount, dialog } = await openDialog();
|
|
140
|
-
|
|
141
|
-
await user.type(screen.getByLabelText(/to confirm/), "ada@example.com");
|
|
142
|
-
await user.click(within(dialog).getByRole("button", { name: "Delete account" }));
|
|
143
|
-
expect(onDeleteAccount).toHaveBeenCalledTimes(1);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("accepts extra tabs", async () => {
|
|
148
|
-
const user = userEvent.setup();
|
|
149
|
-
render(
|
|
150
|
-
<SettingsBlock
|
|
151
|
-
profile={PROFILE}
|
|
152
|
-
notifications={NOTIFICATIONS}
|
|
153
|
-
extraTabs={[{ value: "billing", label: "Billing", content: <p>Your plan is Pro.</p> }]}
|
|
154
|
-
/>,
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
await user.click(screen.getByRole("tab", { name: "Billing" }));
|
|
158
|
-
expect(await screen.findByText("Your plan is Pro.")).toBeInTheDocument();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("has no accessibility violations", async () => {
|
|
162
|
-
const { container } = render(
|
|
163
|
-
<SettingsBlock profile={PROFILE} notifications={NOTIFICATIONS} />,
|
|
164
|
-
);
|
|
165
|
-
await expectNoA11yViolations(container);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { SignupBlock } from "./signup";
|
|
4
|
-
|
|
5
|
-
const meta = {
|
|
6
|
-
title: "Blocks/Sign up",
|
|
7
|
-
component: SignupBlock,
|
|
8
|
-
parameters: { layout: "centered", controls: { disable: true } },
|
|
9
|
-
} satisfies Meta<typeof SignupBlock>;
|
|
10
|
-
|
|
11
|
-
export default meta;
|
|
12
|
-
type Story = StoryObj<typeof meta>;
|
|
13
|
-
|
|
14
|
-
/** Type a password to see the strength hint — it advises, it does not gate. */
|
|
15
|
-
export const Default: Story = {};
|
|
16
|
-
|
|
17
|
-
export const ShorterMinimum: Story = {
|
|
18
|
-
args: { minPasswordLength: 8 },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const WithServerError: Story = {
|
|
22
|
-
args: { error: "An account already exists for that address." },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const Submitting: Story = {
|
|
26
|
-
args: { pending: true },
|
|
27
|
-
};
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { SignupBlock, estimatePasswordStrength } from "./signup";
|
|
7
|
-
|
|
8
|
-
describe("estimatePasswordStrength", () => {
|
|
9
|
-
it("reports too short below the minimum", () => {
|
|
10
|
-
expect(estimatePasswordStrength("abc", 12).label).toBe("Too short");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("rises with length and variety", () => {
|
|
14
|
-
const short = estimatePasswordStrength("aaaaaaaaaaaa", 12);
|
|
15
|
-
const varied = estimatePasswordStrength("Correct-Horse-9-Battery", 12);
|
|
16
|
-
expect(varied.score).toBeGreaterThan(short.score);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("never exceeds 100", () => {
|
|
20
|
-
expect(estimatePasswordStrength("A".repeat(200) + "1!aA", 12).score).toBeLessThanOrEqual(
|
|
21
|
-
100,
|
|
22
|
-
);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("prompts when empty", () => {
|
|
26
|
-
expect(estimatePasswordStrength("", 12).label).toBe("Enter a password");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("SignupBlock", () => {
|
|
31
|
-
it("labels every field", () => {
|
|
32
|
-
render(<SignupBlock />);
|
|
33
|
-
expect(screen.getByLabelText("Name")).toBeInTheDocument();
|
|
34
|
-
expect(screen.getByLabelText("Email")).toBeInTheDocument();
|
|
35
|
-
expect(screen.getByLabelText("Password")).toBeInTheDocument();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("asks password managers for a new password, not an existing one", () => {
|
|
39
|
-
render(<SignupBlock />);
|
|
40
|
-
expect(screen.getByLabelText("Password")).toHaveAttribute("autocomplete", "new-password");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("states the length requirement before anyone gets it wrong", () => {
|
|
44
|
-
render(<SignupBlock minPasswordLength={12} />);
|
|
45
|
-
expect(screen.getByLabelText("Password")).toHaveAccessibleDescription(
|
|
46
|
-
"At least 12 characters.",
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("shows the strength as text, with the bar hidden", async () => {
|
|
51
|
-
const user = userEvent.setup();
|
|
52
|
-
const { container } = render(<SignupBlock />);
|
|
53
|
-
|
|
54
|
-
await user.type(screen.getByLabelText("Password"), "Correct-Horse-9-Battery");
|
|
55
|
-
|
|
56
|
-
expect(screen.getByText("Strong")).toBeInTheDocument();
|
|
57
|
-
// The label already says it; the bar must not be announced as well.
|
|
58
|
-
expect(container.querySelector("[data-slot='progress']")).toHaveAttribute(
|
|
59
|
-
"aria-hidden",
|
|
60
|
-
"true",
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("gates on length, not on the strength estimate", async () => {
|
|
65
|
-
const onSubmit = vi.fn();
|
|
66
|
-
const user = userEvent.setup();
|
|
67
|
-
render(<SignupBlock onSubmit={onSubmit} minPasswordLength={12} />);
|
|
68
|
-
|
|
69
|
-
await user.type(screen.getByLabelText("Email"), "ada@example.com");
|
|
70
|
-
// Weak by the estimate, but long enough — and length is the actual rule.
|
|
71
|
-
await user.type(screen.getByLabelText("Password"), "aaaaaaaaaaaaaa");
|
|
72
|
-
await user.click(screen.getByLabelText(/I agree to the terms/));
|
|
73
|
-
await user.click(screen.getByRole("button", { name: "Create account" }));
|
|
74
|
-
|
|
75
|
-
expect(onSubmit).toHaveBeenCalledWith(
|
|
76
|
-
expect.objectContaining({ email: "ada@example.com" }),
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("requires the terms to be accepted, and says so", async () => {
|
|
81
|
-
const onSubmit = vi.fn();
|
|
82
|
-
const user = userEvent.setup();
|
|
83
|
-
render(<SignupBlock onSubmit={onSubmit} />);
|
|
84
|
-
|
|
85
|
-
await user.type(screen.getByLabelText("Email"), "ada@example.com");
|
|
86
|
-
await user.type(screen.getByLabelText("Password"), "aaaaaaaaaaaaaa");
|
|
87
|
-
await user.click(screen.getByRole("button", { name: "Create account" }));
|
|
88
|
-
|
|
89
|
-
expect(onSubmit).not.toHaveBeenCalled();
|
|
90
|
-
expect(screen.getByText("Accept the terms to continue.")).toBeInTheDocument();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("has no accessibility violations", async () => {
|
|
94
|
-
const { container } = render(<SignupBlock />);
|
|
95
|
-
await expectNoA11yViolations(container);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Annotated rather than inferred with `satisfies`. These components are direct
|
|
7
|
-
* re-exports of Radix primitives, and inferring the meta type makes the emitted
|
|
8
|
-
* declaration reference Radix-internal prop types that are not nameable from
|
|
9
|
-
* this path (TS2883).
|
|
10
|
-
*/
|
|
11
|
-
const meta: Meta<typeof Accordion> = {
|
|
12
|
-
title: "Data/Accordion",
|
|
13
|
-
component: Accordion,
|
|
14
|
-
parameters: { controls: { disable: true } },
|
|
15
|
-
decorators: [
|
|
16
|
-
(Story) => (
|
|
17
|
-
<div className="w-96">
|
|
18
|
-
<Story />
|
|
19
|
-
</div>
|
|
20
|
-
),
|
|
21
|
-
],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export default meta;
|
|
25
|
-
|
|
26
|
-
/** Typed from the component: Accordion's props are a discriminated union on
|
|
27
|
-
* `type`, which collapses to `never` if inferred through the meta. */
|
|
28
|
-
type Story = StoryObj<typeof Accordion>;
|
|
29
|
-
|
|
30
|
-
export const Default: Story = {
|
|
31
|
-
render: () => (
|
|
32
|
-
<Accordion type="single" collapsible defaultValue="shipping">
|
|
33
|
-
<AccordionItem value="shipping">
|
|
34
|
-
<AccordionTrigger>How long does shipping take?</AccordionTrigger>
|
|
35
|
-
<AccordionContent>
|
|
36
|
-
Orders ship within two business days and arrive in three to five days after that.
|
|
37
|
-
</AccordionContent>
|
|
38
|
-
</AccordionItem>
|
|
39
|
-
<AccordionItem value="returns">
|
|
40
|
-
<AccordionTrigger>What is the return policy?</AccordionTrigger>
|
|
41
|
-
<AccordionContent>
|
|
42
|
-
Return anything unused within thirty days for a full refund.
|
|
43
|
-
</AccordionContent>
|
|
44
|
-
</AccordionItem>
|
|
45
|
-
<AccordionItem value="support">
|
|
46
|
-
<AccordionTrigger>How do I contact support?</AccordionTrigger>
|
|
47
|
-
<AccordionContent>
|
|
48
|
-
Email support@example.com and someone will reply within one business day.
|
|
49
|
-
</AccordionContent>
|
|
50
|
-
</AccordionItem>
|
|
51
|
-
</Accordion>
|
|
52
|
-
),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** Independent sections, so more than one can be open at a time. */
|
|
56
|
-
export const Multiple: Story = {
|
|
57
|
-
render: () => (
|
|
58
|
-
<Accordion type="multiple" defaultValue={["build"]}>
|
|
59
|
-
<AccordionItem value="build">
|
|
60
|
-
<AccordionTrigger>Build settings</AccordionTrigger>
|
|
61
|
-
<AccordionContent>
|
|
62
|
-
Framework preset, build command and output directory.
|
|
63
|
-
</AccordionContent>
|
|
64
|
-
</AccordionItem>
|
|
65
|
-
<AccordionItem value="env">
|
|
66
|
-
<AccordionTrigger>Environment variables</AccordionTrigger>
|
|
67
|
-
<AccordionContent>Values available at build and run time.</AccordionContent>
|
|
68
|
-
</AccordionItem>
|
|
69
|
-
<AccordionItem value="domains">
|
|
70
|
-
<AccordionTrigger>Domains</AccordionTrigger>
|
|
71
|
-
<AccordionContent>Custom domains and certificates.</AccordionContent>
|
|
72
|
-
</AccordionItem>
|
|
73
|
-
</Accordion>
|
|
74
|
-
),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const WithDisabledItem: Story = {
|
|
78
|
-
render: () => (
|
|
79
|
-
<Accordion type="single" collapsible>
|
|
80
|
-
<AccordionItem value="a">
|
|
81
|
-
<AccordionTrigger>Available section</AccordionTrigger>
|
|
82
|
-
<AccordionContent>Anyone can open this.</AccordionContent>
|
|
83
|
-
</AccordionItem>
|
|
84
|
-
<AccordionItem value="b">
|
|
85
|
-
<AccordionTrigger disabled>Enterprise only</AccordionTrigger>
|
|
86
|
-
<AccordionContent>Upgrade to view.</AccordionContent>
|
|
87
|
-
</AccordionItem>
|
|
88
|
-
</Accordion>
|
|
89
|
-
),
|
|
90
|
-
};
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
|
|
7
|
-
|
|
8
|
-
function Single({ onValueChange }: { onValueChange?: (value: string) => void } = {}) {
|
|
9
|
-
return (
|
|
10
|
-
<Accordion type="single" collapsible onValueChange={onValueChange}>
|
|
11
|
-
<AccordionItem value="shipping">
|
|
12
|
-
<AccordionTrigger>Shipping</AccordionTrigger>
|
|
13
|
-
<AccordionContent>Ships in two days.</AccordionContent>
|
|
14
|
-
</AccordionItem>
|
|
15
|
-
<AccordionItem value="returns">
|
|
16
|
-
<AccordionTrigger>Returns</AccordionTrigger>
|
|
17
|
-
<AccordionContent>Thirty day returns.</AccordionContent>
|
|
18
|
-
</AccordionItem>
|
|
19
|
-
</Accordion>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("Accordion", () => {
|
|
24
|
-
it("starts collapsed", () => {
|
|
25
|
-
render(<Single />);
|
|
26
|
-
expect(screen.queryByText("Ships in two days.")).not.toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("expands a section on click", async () => {
|
|
30
|
-
const user = userEvent.setup();
|
|
31
|
-
render(<Single />);
|
|
32
|
-
|
|
33
|
-
await user.click(screen.getByRole("button", { name: "Shipping" }));
|
|
34
|
-
expect(await screen.findByText("Ships in two days.")).toBeInTheDocument();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("exposes each trigger as a button inside a heading", () => {
|
|
38
|
-
render(<Single />);
|
|
39
|
-
|
|
40
|
-
const heading = screen.getByRole("heading", { name: "Shipping" });
|
|
41
|
-
expect(heading).toBeInTheDocument();
|
|
42
|
-
expect(heading).toContainElement(screen.getByRole("button", { name: "Shipping" }));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("reports expanded state on the trigger", async () => {
|
|
46
|
-
const user = userEvent.setup();
|
|
47
|
-
render(<Single />);
|
|
48
|
-
|
|
49
|
-
const trigger = screen.getByRole("button", { name: "Shipping" });
|
|
50
|
-
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
51
|
-
|
|
52
|
-
await user.click(trigger);
|
|
53
|
-
await waitFor(() => {
|
|
54
|
-
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("closes the previous section in single mode", async () => {
|
|
59
|
-
const user = userEvent.setup();
|
|
60
|
-
render(<Single />);
|
|
61
|
-
|
|
62
|
-
await user.click(screen.getByRole("button", { name: "Shipping" }));
|
|
63
|
-
await screen.findByText("Ships in two days.");
|
|
64
|
-
|
|
65
|
-
await user.click(screen.getByRole("button", { name: "Returns" }));
|
|
66
|
-
await screen.findByText("Thirty day returns.");
|
|
67
|
-
await waitFor(() => {
|
|
68
|
-
expect(screen.queryByText("Ships in two days.")).not.toBeInTheDocument();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("collapses an open section when collapsible", async () => {
|
|
73
|
-
const user = userEvent.setup();
|
|
74
|
-
render(<Single />);
|
|
75
|
-
|
|
76
|
-
const trigger = screen.getByRole("button", { name: "Shipping" });
|
|
77
|
-
await user.click(trigger);
|
|
78
|
-
await screen.findByText("Ships in two days.");
|
|
79
|
-
|
|
80
|
-
await user.click(trigger);
|
|
81
|
-
await waitFor(() => {
|
|
82
|
-
expect(screen.queryByText("Ships in two days.")).not.toBeInTheDocument();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("keeps sections independent in multiple mode", async () => {
|
|
87
|
-
const user = userEvent.setup();
|
|
88
|
-
render(
|
|
89
|
-
<Accordion type="multiple">
|
|
90
|
-
<AccordionItem value="a">
|
|
91
|
-
<AccordionTrigger>A</AccordionTrigger>
|
|
92
|
-
<AccordionContent>Panel A</AccordionContent>
|
|
93
|
-
</AccordionItem>
|
|
94
|
-
<AccordionItem value="b">
|
|
95
|
-
<AccordionTrigger>B</AccordionTrigger>
|
|
96
|
-
<AccordionContent>Panel B</AccordionContent>
|
|
97
|
-
</AccordionItem>
|
|
98
|
-
</Accordion>,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
await user.click(screen.getByRole("button", { name: "A" }));
|
|
102
|
-
await user.click(screen.getByRole("button", { name: "B" }));
|
|
103
|
-
|
|
104
|
-
expect(screen.getByText("Panel A")).toBeInTheDocument();
|
|
105
|
-
expect(screen.getByText("Panel B")).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("moves between triggers with the arrow keys", async () => {
|
|
109
|
-
const user = userEvent.setup();
|
|
110
|
-
render(<Single />);
|
|
111
|
-
|
|
112
|
-
await user.tab();
|
|
113
|
-
expect(screen.getByRole("button", { name: "Shipping" })).toHaveFocus();
|
|
114
|
-
|
|
115
|
-
await user.keyboard("{ArrowDown}");
|
|
116
|
-
expect(screen.getByRole("button", { name: "Returns" })).toHaveFocus();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("toggles from the keyboard", async () => {
|
|
120
|
-
const user = userEvent.setup();
|
|
121
|
-
render(<Single />);
|
|
122
|
-
|
|
123
|
-
await user.tab();
|
|
124
|
-
await user.keyboard("{Enter}");
|
|
125
|
-
expect(await screen.findByText("Ships in two days.")).toBeInTheDocument();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("reports value changes", async () => {
|
|
129
|
-
const onValueChange = vi.fn();
|
|
130
|
-
const user = userEvent.setup();
|
|
131
|
-
render(<Single onValueChange={onValueChange} />);
|
|
132
|
-
|
|
133
|
-
await user.click(screen.getByRole("button", { name: "Shipping" }));
|
|
134
|
-
expect(onValueChange).toHaveBeenCalledWith("shipping");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("lets a consumer className override a conflicting utility", () => {
|
|
138
|
-
render(
|
|
139
|
-
<Accordion type="single" collapsible>
|
|
140
|
-
<AccordionItem value="a" className="border-b-2" data-testid="item">
|
|
141
|
-
<AccordionTrigger>A</AccordionTrigger>
|
|
142
|
-
<AccordionContent>Panel</AccordionContent>
|
|
143
|
-
</AccordionItem>
|
|
144
|
-
</Accordion>,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const item = screen.getByTestId("item");
|
|
148
|
-
expect(item).toHaveClass("border-b-2");
|
|
149
|
-
expect(item).not.toHaveClass("border-b");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("has no accessibility violations", async () => {
|
|
153
|
-
const { container } = render(<Single />);
|
|
154
|
-
await expectNoA11yViolations(container);
|
|
155
|
-
});
|
|
156
|
-
});
|