@tangle-network/ui 11.51.6 → 11.51.8

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -0
  3. package/dist/chat/agent-timeline.d.ts +1 -1
  4. package/dist/chat/speaker.d.ts +2 -2
  5. package/dist/chat.d.ts +2 -2
  6. package/dist/index.d.ts +4 -4
  7. package/package.json +31 -1
  8. package/src/editor/source-markdown-editor.test.tsx +7 -7
  9. package/src/editor/document-editor-pane.test.tsx +0 -60
  10. package/src/editor/editor-lazy.test.tsx +0 -102
  11. package/src/editor/editor-peers.test.ts +0 -250
  12. package/src/editor/editor-provider.test.tsx +0 -226
  13. package/src/editor/markdown-document-editor.test.tsx +0 -126
  14. package/src/editor/tiptap-editor.test.tsx +0 -228
  15. package/src/files/file-artifact-pane.test.tsx +0 -301
  16. package/src/files/file-card.test.tsx +0 -62
  17. package/src/files/file-list.test.tsx +0 -81
  18. package/src/files/pdf-loader.test.ts +0 -130
  19. package/src/files/pdf-viewer.test.tsx +0 -245
  20. package/src/files/rich-file-tree.test.tsx +0 -90
  21. package/src/markdown/api-presentation.test.tsx +0 -63
  22. package/src/markdown/code-block.test.tsx +0 -201
  23. package/src/markdown/markdown-table.test.tsx +0 -76
  24. package/src/markdown/markdown.test.tsx +0 -166
  25. package/src/openui/schema.test.tsx +0 -158
  26. package/src/primitives/button.test.tsx +0 -230
  27. package/src/primitives/card.test.tsx +0 -71
  28. package/src/primitives/chip.test.tsx +0 -200
  29. package/src/primitives/date-picker.test.tsx +0 -146
  30. package/src/primitives/dialog.test.tsx +0 -75
  31. package/src/primitives/input.test.tsx +0 -161
  32. package/src/primitives/interaction-states.test.tsx +0 -287
  33. package/src/primitives/pagination-alert.test.tsx +0 -180
  34. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +0 -165
  35. package/src/primitives/radio-group.test.tsx +0 -26
  36. package/src/primitives/segmented-control.test.tsx +0 -451
  37. package/src/primitives/table.test.tsx +0 -72
  38. package/src/primitives/theme-toggle.test.tsx +0 -168
  39. package/src/primitives/toast.test.tsx +0 -130
  40. package/src/primitives/wizard.test.tsx +0 -106
  41. package/src/run/expandable-content.test.tsx +0 -52
@@ -1,287 +0,0 @@
1
- import * as React from "react";
2
- import { cleanup, render, screen } from "@testing-library/react";
3
- import { compile } from "tailwindcss";
4
- import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
5
- import { focusFieldWithin } from "../lib/focus";
6
- import {
7
- ixControl,
8
- ixControlAfter,
9
- ixControlSoft,
10
- ixField,
11
- ixRow,
12
- ixSelected,
13
- ixTabUnderline,
14
- } from "../lib/interaction";
15
- import { Button } from "./button";
16
- import { Card, CardDescription } from "./card";
17
- import {
18
- DropdownMenu,
19
- DropdownMenuCheckboxItem,
20
- DropdownMenuContent,
21
- DropdownMenuItem,
22
- DropdownMenuRadioGroup,
23
- DropdownMenuRadioItem,
24
- DropdownMenuSub,
25
- DropdownMenuSubTrigger,
26
- DropdownMenuTrigger,
27
- } from "./dropdown-menu";
28
- import { Input, Textarea } from "./input";
29
- import { SegmentedControl } from "./segmented-control";
30
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
31
- import { Tabs, TabsList, TabsTrigger } from "./tabs";
32
-
33
- /**
34
- * Every primitive draws its interaction states from one helper in
35
- * lib/interaction.ts (ROOT-SPEC section b). This is the cheap static guard:
36
- * each primitive's rendered className carries every class of its helper, so a
37
- * later edit or a tailwind-merge conflict cannot silently drop a state. The
38
- * second suite compiles each helper with Tailwind to prove the classes emit the
39
- * declarations and selectors the states need.
40
- */
41
- afterEach(cleanup);
42
- beforeAll(() => {
43
- // jsdom lacks the layout APIs Radix Select and Menu call while opening.
44
- Element.prototype.scrollIntoView ??= vi.fn();
45
- Element.prototype.hasPointerCapture ??= vi.fn(() => false);
46
- Element.prototype.releasePointerCapture ??= vi.fn();
47
- });
48
-
49
- const tokens = (helper: string) => helper.split(/\s+/).filter(Boolean);
50
- const classes = (el: Element) => new Set(el.className.split(/\s+/));
51
-
52
- /** Every class of every helper is on the element. */
53
- function expectHelpers(el: Element, ...helpers: string[]) {
54
- const have = classes(el);
55
- for (const helper of helpers) {
56
- for (const token of tokens(helper)) expect(have, `${el.tagName} is missing ${token}`).toContain(token);
57
- }
58
- }
59
-
60
- /** The glow only strong controls get. */
61
- const HOVER_GLOW = "hover:shadow-[var(--ix-hover)]";
62
- /** focusRing, which every control keeps exactly. */
63
- const FOCUS_RING = [
64
- "focus-visible:outline-1",
65
- "focus-visible:outline-[var(--focus-border)]",
66
- "focus-visible:ring-4",
67
- "focus-visible:ring-[var(--focus-halo)]",
68
- ];
69
- /** focusField's `:focus` part, which every field keeps exactly. */
70
- const FOCUS_FIELD = ["focus:outline-hidden", "focus:border-[var(--focus-border)]", "focus:ring-3", "focus:ring-[var(--focus-halo)]"];
71
-
72
- describe("each primitive renders its interaction helper", () => {
73
- it.each(["default", "destructive", "secondary", "outline", "sandbox"] as const)(
74
- "Button %s takes ixControl and keeps the focus ring",
75
- (variant) => {
76
- render(<Button variant={variant}>Go</Button>);
77
- const button = screen.getByRole("button");
78
- expectHelpers(button, ixControl);
79
- for (const token of FOCUS_RING) expect(classes(button)).toContain(token);
80
- },
81
- );
82
-
83
- it.each(["ghost", "link", "bare"] as const)("Button %s takes ixControlSoft and never the hover glow", (variant) => {
84
- render(<Button variant={variant}>Go</Button>);
85
- const button = screen.getByRole("button");
86
- expectHelpers(button, ixControlSoft);
87
- expect(classes(button)).not.toContain(HOVER_GLOW);
88
- expect(classes(button)).not.toContain("hover:bg-muted");
89
- for (const token of FOCUS_RING) expect(classes(button)).toContain(token);
90
- });
91
-
92
- it.each(["outline", "ghost", "bare"] as const)("a %s toggle Button (aria-pressed) adds ixControl and ixSelected", (variant) => {
93
- render(<Button variant={variant} aria-pressed>Bold</Button>);
94
- expectHelpers(screen.getByRole("button"), ixControl, ixSelected);
95
- });
96
-
97
- it("Input and Textarea take ixField with focusField's focus border and halo", () => {
98
- render(<><Input aria-label="Name" /><Textarea aria-label="Notes" /></>);
99
- for (const field of [screen.getByLabelText("Name"), screen.getByLabelText("Notes")]) {
100
- expectHelpers(field, ixField);
101
- for (const token of FOCUS_FIELD) expect(classes(field)).toContain(token);
102
- expect(classes(field)).not.toContain("border-border");
103
- expect(classes(field)).not.toContain("hover:border-[var(--border-strong)]");
104
- }
105
- });
106
-
107
- it("focusFieldWithin is a field: ixField plus the has-focus ring, without the neutral hover", () => {
108
- expect(tokens(focusFieldWithin)).toEqual(expect.arrayContaining(tokens(ixField)));
109
- expect(focusFieldWithin).toContain("has-[");
110
- expect(focusFieldWithin).toContain(":focus]:ring-[var(--focus-halo)]");
111
- expect(tokens(focusFieldWithin)).not.toContain("hover:border-[var(--border-strong)]");
112
- expect(tokens(focusFieldWithin)).not.toContain("border-border");
113
- });
114
-
115
- it("SelectTrigger is a field that keeps its well and an after-click ring while open", () => {
116
- render(<Select><SelectTrigger aria-label="Status"><SelectValue /></SelectTrigger></Select>);
117
- const trigger = screen.getByRole("combobox");
118
- expectHelpers(trigger, ixField, ixControlAfter);
119
- for (const token of FOCUS_FIELD) expect(classes(trigger)).toContain(token);
120
- expect(classes(trigger)).toContain("data-[state=open]:shadow-[var(--ix-after)]");
121
- expect(classes(trigger)).toContain("data-[state=open]:border-[var(--ix-hover-border)]");
122
- // The open state must not repaint the well.
123
- expect([...classes(trigger)].filter((c) => c.startsWith("data-[state=open]:bg-"))).toEqual([]);
124
- });
125
-
126
- it("SelectContent paints the popover level and SelectItem takes ixRow", async () => {
127
- render(
128
- <Select open value="a">
129
- <SelectTrigger aria-label="Status"><SelectValue /></SelectTrigger>
130
- <SelectContent><SelectItem value="a">Active</SelectItem><SelectItem value="b">Paused</SelectItem></SelectContent>
131
- </Select>,
132
- );
133
- const listbox = await screen.findByRole("listbox");
134
- const content = listbox.closest("[data-radix-select-viewport]")?.parentElement ?? listbox;
135
- expect(classes(content)).toContain("bg-popover");
136
- expect(classes(content)).toContain("shadow-[var(--edge-highlight),var(--shadow-card)]");
137
- expect(classes(content)).not.toContain("bg-card");
138
- for (const option of screen.getAllByRole("option")) {
139
- expectHelpers(option, ixRow);
140
- expect(classes(option)).not.toContain("focus:bg-[var(--accent-surface-strong)]");
141
- }
142
- });
143
-
144
- it("DropdownMenu content paints the popover level and every item kind takes ixRow", async () => {
145
- render(
146
- <DropdownMenu open>
147
- <DropdownMenuTrigger>Menu</DropdownMenuTrigger>
148
- <DropdownMenuContent>
149
- <DropdownMenuItem>Rename</DropdownMenuItem>
150
- <DropdownMenuCheckboxItem checked>Pinned</DropdownMenuCheckboxItem>
151
- <DropdownMenuRadioGroup value="a"><DropdownMenuRadioItem value="a">Newest</DropdownMenuRadioItem></DropdownMenuRadioGroup>
152
- <DropdownMenuSub><DropdownMenuSubTrigger>Move to</DropdownMenuSubTrigger></DropdownMenuSub>
153
- </DropdownMenuContent>
154
- </DropdownMenu>,
155
- );
156
- const menu = await screen.findByRole("menu");
157
- expect(classes(menu)).toContain("bg-popover");
158
- expect(classes(menu)).toContain("shadow-[var(--edge-highlight),var(--shadow-card)]");
159
- expect(classes(menu)).not.toContain("bg-card");
160
- const items = [
161
- screen.getByRole("menuitem", { name: "Rename" }),
162
- screen.getByRole("menuitemcheckbox", { name: "Pinned" }),
163
- screen.getByRole("menuitemradio", { name: "Newest" }),
164
- screen.getByRole("menuitem", { name: "Move to" }),
165
- ];
166
- for (const item of items) {
167
- expectHelpers(item, ixRow);
168
- expect(classes(item)).not.toContain("focus:bg-muted/50");
169
- }
170
- });
171
-
172
- it("Card is a ladder level with a shadow; hover and interactive cards take ixControl", () => {
173
- render(
174
- <>
175
- <Card data-testid="plain" />
176
- <Card data-testid="hover" hover />
177
- <Card data-testid="interactive" interactive />
178
- <CardDescription>Guests, staff and vendors message your business on WhatsApp.</CardDescription>
179
- </>,
180
- );
181
- const plain = screen.getByTestId("plain");
182
- expect(plain).toHaveAttribute("data-slot", "card");
183
- expect(classes(plain)).toContain("shadow-[var(--edge-highlight),var(--shadow-card)]");
184
- expect(classes(plain)).not.toContain(HOVER_GLOW);
185
- for (const id of ["hover", "interactive"]) {
186
- const card = screen.getByTestId(id);
187
- expectHelpers(card, ixControl);
188
- expect(card).not.toHaveAttribute("interactive");
189
- expect(classes(card)).not.toContain("hover:border-[var(--border-strong)]");
190
- }
191
- expect(classes(screen.getByText(/Guests, staff/))).toContain("line-clamp-2");
192
- });
193
-
194
- it("SegmentedControl row: a quiet well track, soft items, a raised thumb on aria-checked and no ring", () => {
195
- const options = [{ value: "a", label: "A–Z" }, { value: "b", label: "Recent" }];
196
- render(<SegmentedControl aria-label="Sort" value="a" onValueChange={() => {}} options={options} />);
197
- const track = screen.getByRole("radiogroup");
198
- expect(classes(track)).toContain("bg-[var(--well)]");
199
- expect(classes(track)).toContain("border-border");
200
- expect(classes(track)).not.toContain("bg-card");
201
- for (const radio of screen.getAllByRole("radio")) {
202
- expectHelpers(radio, ixControlSoft);
203
- expect(classes(radio)).toContain("aria-checked:bg-card");
204
- expect([...classes(radio)].some((token) => token.includes("--ix-sel-ring"))).toBe(false);
205
- for (const token of FOCUS_RING) expect(classes(radio)).toContain(token);
206
- expect(classes(radio)).not.toContain("bg-[var(--accent-surface)]");
207
- }
208
- });
209
-
210
- it("SegmentedControl tabs: the underline helper and a transparent border slot", () => {
211
- const options = [{ value: "a", label: "Team" }, { value: "b", label: "Roles" }];
212
- render(<SegmentedControl aria-label="Section" variant="tabs" value="a" onValueChange={() => {}} options={options} />);
213
- for (const radio of screen.getAllByRole("radio")) {
214
- expectHelpers(radio, ixTabUnderline);
215
- expect(classes(radio)).toContain("border-transparent");
216
- expect(classes(radio)).not.toContain("border-[var(--accent-text)]");
217
- }
218
- });
219
-
220
- it.each([
221
- ["default", [ixControlSoft, ixSelected]],
222
- ["pills", [ixControlSoft, ixSelected]],
223
- ["underline", [ixTabUnderline]],
224
- ] as const)("TabsTrigger %s takes its helper", (variant, helpers) => {
225
- render(
226
- <Tabs defaultValue="a">
227
- <TabsList variant={variant}>
228
- <TabsTrigger variant={variant} value="a">Overview</TabsTrigger>
229
- <TabsTrigger variant={variant} value="b">Activity</TabsTrigger>
230
- </TabsList>
231
- </Tabs>,
232
- );
233
- for (const tab of screen.getAllByRole("tab")) {
234
- expectHelpers(tab, ...helpers);
235
- for (const token of FOCUS_RING) expect(classes(tab)).toContain(token);
236
- expect([...classes(tab)]).not.toEqual(expect.arrayContaining(["data-[state=active]:bg-background"]));
237
- expect(classes(tab)).not.toContain("data-[state=active]:border-primary");
238
- }
239
- });
240
- });
241
-
242
- describe("Tailwind compiles every interaction class to the state it names", () => {
243
- /** A fresh compiler per class, so each class's CSS is read on its own. */
244
- async function cssFor(candidate: string) {
245
- const compiler = await compile("@tailwind utilities;");
246
- return compiler.build([candidate]);
247
- }
248
-
249
- const all = [...new Set([ixControl, ixControlAfter, ixControlSoft, ixField, ixSelected, ixTabUnderline, ixRow].flatMap(tokens))];
250
-
251
- it.each(all)("%s emits a rule", async (candidate) => {
252
- expect(await cssFor(candidate)).toMatch(/\{[^}]*:[^}]*\}/);
253
- });
254
-
255
- it("composes the hover and press glows with the focus ring instead of replacing it", async () => {
256
- for (const [candidate, value] of [
257
- ["hover:shadow-[var(--ix-hover)]", "var(--ix-hover)"],
258
- ["active:shadow-[var(--ix-press)]", "var(--ix-press)"],
259
- ]) {
260
- const css = await cssFor(candidate);
261
- expect(css).toContain(`--tw-shadow: ${value}`);
262
- expect(css).toContain("var(--tw-ring-shadow), var(--tw-shadow)");
263
- }
264
- });
265
-
266
- it("keys the after-click ring on mouse focus only", async () => {
267
- const css = await cssFor("focus:not-focus-visible:shadow-[var(--ix-after)]");
268
- expect(css).toContain(":focus:not(:focus-visible)");
269
- expect(css).toContain("--tw-shadow: var(--ix-after)");
270
- });
271
-
272
- it("reads the field fill and border from the surface context", async () => {
273
- expect(await cssFor("bg-[var(--field-surface,var(--well))]")).toContain("background-color: var(--field-surface,var(--well))");
274
- expect(await cssFor("border-[var(--field-border)]")).toContain("border-color: var(--field-border)");
275
- expect(await cssFor("hover:border-[var(--ix-hover-border)]")).toContain("border-color: var(--ix-hover-border)");
276
- });
277
-
278
- it("draws selection on aria-pressed, aria-checked, aria-current, data-state on and active", async () => {
279
- const css = await cssFor("aria-[current=page]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)]");
280
- expect(css).toContain('[aria-current="page"]');
281
- expect(css).toContain("inset 0 0 0 1px");
282
- expect(await cssFor("aria-pressed:bg-[var(--ix-sel-bg)]")).toContain('[aria-pressed="true"]');
283
- expect(await cssFor("aria-checked:bg-[var(--ix-sel-bg)]")).toContain('[aria-checked="true"]');
284
- expect(await cssFor("data-[state=active]:bg-[var(--ix-sel-bg)]")).toContain('[data-state="active"]');
285
- expect(await cssFor("data-[highlighted]:bg-[var(--ix-sel-bg)]")).toContain("[data-highlighted]");
286
- });
287
- });
@@ -1,180 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import type * as React from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { Alert, AlertDescription, AlertTitle } from "./alert";
6
- import { Pagination, pageSlots } from "./pagination";
7
-
8
- const PALETTE = /\b(?:bg|text|border)-(?:slate|zinc|gray|neutral|blue|green|red|amber|emerald|black|white)(?:-\d|\b)/;
9
-
10
- describe("pageSlots", () => {
11
- it("lists every page when they fit", () => {
12
- expect(pageSlots(0, 1)).toEqual([0]);
13
- expect(pageSlots(3, 7)).toEqual([0, 1, 2, 3, 4, 5, 6]);
14
- });
15
-
16
- it("keeps the same number of slots as the current page moves", () => {
17
- expect(pageSlots(0, 10)).toEqual([0, 1, 2, 3, 4, "gap-end", 9]);
18
- expect(pageSlots(5, 10)).toEqual([0, "gap-start", 4, 5, 6, "gap-end", 9]);
19
- expect(pageSlots(9, 10)).toEqual([0, "gap-start", 5, 6, 7, 8, 9]);
20
- for (let page = 0; page < 40; page++) expect(pageSlots(page, 40)).toHaveLength(7);
21
- });
22
-
23
- it("clamps a page outside the range", () => {
24
- expect(pageSlots(-3, 10)).toEqual(pageSlots(0, 10));
25
- expect(pageSlots(99, 10)).toEqual(pageSlots(9, 10));
26
- });
27
- });
28
-
29
- describe("Pagination", () => {
30
- it("renders nothing for a single page", () => {
31
- const { container } = render(<Pagination page={0} pageCount={1} onPageChange={vi.fn()} />);
32
- expect(container).toBeEmptyDOMElement();
33
- });
34
-
35
- it("marks the current page and moves with previous, next and a number", async () => {
36
- const user = userEvent.setup();
37
- const onPageChange = vi.fn();
38
- render(<Pagination page={4} pageCount={12} onPageChange={onPageChange} />);
39
-
40
- expect(screen.getByRole("navigation", { name: "Pagination" })).toBeInTheDocument();
41
- const current = screen.getByRole("button", { name: "Page 5" });
42
- expect(current).toHaveAttribute("aria-current", "page");
43
- expect(current.className).not.toMatch(PALETTE);
44
-
45
- await user.click(screen.getByRole("button", { name: "Previous page" }));
46
- await user.click(screen.getByRole("button", { name: "Next page" }));
47
- await user.click(screen.getByRole("button", { name: "Page 12" }));
48
- expect(onPageChange.mock.calls).toEqual([[3], [5], [11]]);
49
- });
50
-
51
- it("disables previous on the first page and next on the last", () => {
52
- const { rerender } = render(<Pagination page={0} pageCount={3} onPageChange={vi.fn()} />);
53
- expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
54
- rerender(<Pagination page={2} pageCount={3} onPageChange={vi.fn()} />);
55
- expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
56
- });
57
-
58
- it("does not report the page that is already current", async () => {
59
- const user = userEvent.setup();
60
- const onPageChange = vi.fn();
61
- render(<Pagination page={1} pageCount={3} onPageChange={onPageChange} />);
62
- await user.click(screen.getByRole("button", { name: "Page 2" }));
63
- expect(onPageChange).not.toHaveBeenCalled();
64
- });
65
- });
66
-
67
- describe("Pagination link mode", () => {
68
- const hrefFor = (page: number) => `?page=${page + 1}`;
69
-
70
- it("renders links to one-based URLs from zero-based pages", () => {
71
- render(<Pagination page={4} pageCount={12} hrefFor={hrefFor} />);
72
-
73
- expect(screen.queryByRole("button")).not.toBeInTheDocument();
74
- expect(screen.getByRole("link", { name: "Previous page" })).toHaveAttribute("href", "?page=4");
75
- expect(screen.getByRole("link", { name: "Next page" })).toHaveAttribute("href", "?page=6");
76
- expect(screen.getByRole("link", { name: "Page 1" })).toHaveAttribute("href", "?page=1");
77
- expect(screen.getByRole("link", { name: "Page 12" })).toHaveAttribute("href", "?page=12");
78
-
79
- const current = screen.getByRole("link", { name: "Page 5" });
80
- expect(current.tagName).toBe("A");
81
- expect(current).toHaveAttribute("href", "?page=5");
82
- expect(current).toHaveAttribute("aria-current", "page");
83
- expect(screen.getByRole("link", { name: "Page 4" })).not.toHaveAttribute("aria-current");
84
- });
85
-
86
- it("renders a disabled previous or next as a span without an href", () => {
87
- const { rerender } = render(<Pagination page={0} pageCount={3} hrefFor={hrefFor} />);
88
- const previous = screen.getByRole("link", { name: "Previous page" });
89
- expect(previous.tagName).toBe("SPAN");
90
- expect(previous).toHaveAttribute("aria-disabled", "true");
91
- expect(previous).not.toHaveAttribute("href");
92
- expect(previous.className).toContain("aria-disabled:opacity-50");
93
- expect(screen.getByRole("link", { name: "Next page" })).toHaveAttribute("href", "?page=2");
94
-
95
- rerender(<Pagination page={2} pageCount={3} hrefFor={hrefFor} />);
96
- const next = screen.getByRole("link", { name: "Next page" });
97
- expect(next.tagName).toBe("SPAN");
98
- expect(next).toHaveAttribute("aria-disabled", "true");
99
- expect(next).not.toHaveAttribute("href");
100
- });
101
-
102
- it("renders each enabled control with a custom link component", () => {
103
- const received: string[] = [];
104
- const RouterLink = ({ href, ...props }: React.ComponentProps<"a">) => {
105
- received.push(href ?? "");
106
- return <a data-router-link="" href={href} {...props} />;
107
- };
108
- render(<Pagination page={1} pageCount={3} hrefFor={hrefFor} linkComponent={RouterLink} />);
109
-
110
- expect(received).toEqual(["?page=1", "?page=1", "?page=2", "?page=3", "?page=3"]);
111
- const current = screen.getByRole("link", { name: "Page 2" });
112
- expect(current).toHaveAttribute("data-router-link");
113
- expect(current).toHaveAttribute("aria-current", "page");
114
- });
115
-
116
- it("reports a plain click on another page and ignores the current page and new-tab clicks", async () => {
117
- const user = userEvent.setup();
118
- const onPageChange = vi.fn();
119
- render(
120
- <Pagination
121
- page={1}
122
- pageCount={3}
123
- hrefFor={hrefFor}
124
- linkComponent={({ onClick, ...props }: React.ComponentProps<"a">) => (
125
- <a
126
- {...props}
127
- onClick={(event) => {
128
- onClick?.(event);
129
- event.preventDefault();
130
- }}
131
- />
132
- )}
133
- onPageChange={onPageChange}
134
- />,
135
- );
136
-
137
- await user.click(screen.getByRole("link", { name: "Next page" }));
138
- await user.click(screen.getByRole("link", { name: "Page 2" }));
139
- await user.keyboard("{Meta>}");
140
- await user.click(screen.getByRole("link", { name: "Page 1" }));
141
- await user.keyboard("{/Meta}");
142
- expect(onPageChange.mock.calls).toEqual([[2]]);
143
- });
144
-
145
- it("requires onPageChange or hrefFor, and a link component only with hrefFor", () => {
146
- const rejected = [
147
- // @ts-expect-error neither onPageChange nor hrefFor
148
- () => <Pagination page={0} pageCount={2} />,
149
- // @ts-expect-error linkComponent without hrefFor
150
- () => <Pagination page={0} pageCount={2} onPageChange={vi.fn()} linkComponent="a" />,
151
- ];
152
- expect(rejected).toHaveLength(2);
153
- });
154
- });
155
-
156
- describe("Alert", () => {
157
- it("paints a tone from the matched Brand status triple", () => {
158
- render(
159
- <Alert tone="warning">
160
- <AlertTitle>Policy is changing</AlertTitle>
161
- <AlertDescription>Seats above the limit are removed next cycle.</AlertDescription>
162
- </Alert>,
163
- );
164
- const alert = screen.getByRole("alert");
165
- expect(alert.className).toContain("bg-[var(--surface-warning-bg)]");
166
- expect(alert.className).toContain("text-[var(--surface-warning-text)]");
167
- expect(alert.className).not.toMatch(PALETTE);
168
- });
169
-
170
- it("uses role status for quiet tones and keeps an explicit role", () => {
171
- const { rerender } = render(<Alert tone="info">Saved</Alert>);
172
- expect(screen.getByRole("status")).toHaveTextContent("Saved");
173
- rerender(
174
- <Alert tone="neutral" role="note">
175
- Note
176
- </Alert>,
177
- );
178
- expect(screen.getByRole("note")).toHaveAttribute("data-tone", "neutral");
179
- });
180
- });
@@ -1,165 +0,0 @@
1
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { beforeAll, describe, expect, it, vi } from "vitest";
4
- import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogTitle,
11
- AlertDialogTrigger,
12
- } from "./alert-dialog";
13
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
14
- import {
15
- ContextMenu,
16
- ContextMenuContent,
17
- ContextMenuItem,
18
- ContextMenuSub,
19
- ContextMenuSubContent,
20
- ContextMenuSubTrigger,
21
- ContextMenuTrigger,
22
- } from "./context-menu";
23
- import { Popover, PopoverContent, PopoverTrigger } from "./popover";
24
- import { Slider } from "./slider";
25
-
26
- const PALETTE = /\b(?:bg|text|border)-(?:slate|zinc|gray|neutral|blue|green|red|amber|emerald|black|white)(?:-\d|\b)/;
27
-
28
- describe("Popover", () => {
29
- it("opens from its trigger on a token surface and closes on Escape", async () => {
30
- const user = userEvent.setup();
31
- render(
32
- <Popover>
33
- <PopoverTrigger>Filters</PopoverTrigger>
34
- <PopoverContent>Only failed runs</PopoverContent>
35
- </Popover>,
36
- );
37
- await user.click(screen.getByRole("button", { name: "Filters" }));
38
- const content = await screen.findByRole("dialog");
39
- expect(content).toHaveTextContent("Only failed runs");
40
- expect(content.className).toContain("bg-card");
41
- expect(content.className).not.toMatch(PALETTE);
42
- await user.keyboard("{Escape}");
43
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
44
- });
45
- });
46
-
47
- describe("Collapsible", () => {
48
- it("reports its expanded state on the trigger", async () => {
49
- const user = userEvent.setup();
50
- render(
51
- <Collapsible>
52
- <CollapsibleTrigger>Details</CollapsibleTrigger>
53
- <CollapsibleContent>Hidden body</CollapsibleContent>
54
- </Collapsible>,
55
- );
56
- const trigger = screen.getByRole("button", { name: "Details" });
57
- expect(trigger).toHaveAttribute("aria-expanded", "false");
58
- expect(screen.queryByText("Hidden body")).toBeNull();
59
- await user.click(trigger);
60
- expect(trigger).toHaveAttribute("aria-expanded", "true");
61
- expect(screen.getByText("Hidden body")).toBeVisible();
62
- });
63
- });
64
-
65
- describe("AlertDialog", () => {
66
- it("focuses Cancel on open so a destructive action is never under Enter", async () => {
67
- const user = userEvent.setup();
68
- const onDelete = vi.fn();
69
- render(
70
- <AlertDialog>
71
- <AlertDialogTrigger>Delete project</AlertDialogTrigger>
72
- <AlertDialogContent>
73
- <AlertDialogTitle>Delete this project?</AlertDialogTitle>
74
- <AlertDialogDescription>Its sandboxes stop.</AlertDialogDescription>
75
- <AlertDialogCancel>Cancel</AlertDialogCancel>
76
- <AlertDialogAction tone="destructive" onClick={onDelete}>
77
- Delete
78
- </AlertDialogAction>
79
- </AlertDialogContent>
80
- </AlertDialog>,
81
- );
82
- await user.click(screen.getByRole("button", { name: "Delete project" }));
83
- const dialog = await screen.findByRole("alertdialog", { name: "Delete this project?" });
84
- expect(dialog).toHaveAccessibleDescription("Its sandboxes stop.");
85
- expect(screen.getByRole("button", { name: "Cancel" })).toHaveFocus();
86
- const action = screen.getByRole("button", { name: "Delete" });
87
- expect(action.className).toContain("bg-destructive");
88
- await user.click(action);
89
- expect(onDelete).toHaveBeenCalledOnce();
90
- await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull());
91
- });
92
- });
93
-
94
- describe("ContextMenu", () => {
95
- it("opens on contextmenu and runs the chosen item", async () => {
96
- const onRename = vi.fn();
97
- render(
98
- <ContextMenu>
99
- <ContextMenuTrigger>File row</ContextMenuTrigger>
100
- <ContextMenuContent>
101
- <ContextMenuItem onSelect={onRename}>Rename</ContextMenuItem>
102
- <ContextMenuItem tone="destructive">Delete</ContextMenuItem>
103
- </ContextMenuContent>
104
- </ContextMenu>,
105
- );
106
- fireEvent.contextMenu(screen.getByText("File row"));
107
- const menu = await screen.findByRole("menu");
108
- expect(menu.className).toContain("bg-card");
109
- expect(screen.getByRole("menuitem", { name: "Delete" }).className).toContain("text-destructive");
110
- fireEvent.click(screen.getByRole("menuitem", { name: "Rename" }));
111
- expect(onRename).toHaveBeenCalledOnce();
112
- });
113
-
114
- it("portals submenu content out of the clipped parent menu", async () => {
115
- const user = userEvent.setup();
116
- render(
117
- <ContextMenu>
118
- <ContextMenuTrigger>File row</ContextMenuTrigger>
119
- <ContextMenuContent>
120
- <ContextMenuSub>
121
- <ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>
122
- <ContextMenuSubContent>
123
- <ContextMenuItem>Archive</ContextMenuItem>
124
- </ContextMenuSubContent>
125
- </ContextMenuSub>
126
- </ContextMenuContent>
127
- </ContextMenu>,
128
- );
129
- fireEvent.contextMenu(screen.getByText("File row"));
130
- const parent = await screen.findByRole("menu");
131
- await user.hover(screen.getByRole("menuitem", { name: "Move to" }));
132
- await user.keyboard("{ArrowRight}");
133
- const archive = await screen.findByRole("menuitem", { name: "Archive" });
134
- expect(parent.contains(archive)).toBe(false);
135
- });
136
- });
137
-
138
- describe("Slider", () => {
139
- beforeAll(() => {
140
- // Radix measures the thumb with ResizeObserver, which jsdom lacks.
141
- globalThis.ResizeObserver ??= class {
142
- observe() {}
143
- unobserve() {}
144
- disconnect() {}
145
- };
146
- });
147
-
148
- it("renders one labelled thumb per value", () => {
149
- render(<Slider defaultValue={[20, 80]} thumbLabels={["Minimum", "Maximum"]} />);
150
- const thumbs = screen.getAllByRole("slider");
151
- expect(thumbs).toHaveLength(2);
152
- expect(thumbs[0]).toHaveAccessibleName("Minimum");
153
- expect(thumbs[1]).toHaveAttribute("aria-valuenow", "80");
154
- });
155
-
156
- it("moves with the keyboard and reports the value", async () => {
157
- const user = userEvent.setup();
158
- const onValueChange = vi.fn();
159
- render(<Slider aria-label="Budget" defaultValue={[50]} step={10} onValueChange={onValueChange} />);
160
- expect(screen.getByRole("slider")).toHaveAccessibleName("Budget");
161
- screen.getByRole("slider").focus();
162
- await user.keyboard("{ArrowRight}");
163
- expect(onValueChange).toHaveBeenLastCalledWith([60]);
164
- });
165
- });
@@ -1,26 +0,0 @@
1
- import * as React from "react";
2
- import { cleanup, render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { Label } from "./label";
6
- import { RadioGroup, RadioGroupItem } from "./radio-group";
7
-
8
- afterEach(cleanup);
9
-
10
- describe("RadioGroup", () => {
11
- it("chooses one item by click or arrow key and reports its value", async () => {
12
- const onValueChange = vi.fn();
13
- render(
14
- <RadioGroup aria-label="Plan" defaultValue="basic" onValueChange={onValueChange}>
15
- <div><RadioGroupItem id="basic" value="basic" /><Label htmlFor="basic">Basic</Label></div>
16
- <div><RadioGroupItem id="pro" value="pro" /><Label htmlFor="pro">Pro</Label></div>
17
- </RadioGroup>,
18
- );
19
- expect(screen.getByRole("radiogroup", { name: "Plan" })).toBeTruthy();
20
- expect(screen.getByRole("radio", { name: "Basic" })).toHaveAttribute("aria-checked", "true");
21
- await userEvent.click(screen.getByText("Pro"));
22
- expect(onValueChange).toHaveBeenLastCalledWith("pro");
23
- expect(screen.getByRole("radio", { name: "Pro" })).toHaveAttribute("aria-checked", "true");
24
- expect(screen.getByRole("radio", { name: "Basic" })).toHaveAttribute("aria-checked", "false");
25
- });
26
- });