@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,71 +0,0 @@
1
- import { createRef } from "react";
2
- import { render, screen } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps } from "./card";
6
-
7
- describe("Card anatomy", () => {
8
- it("keeps the existing default title tag and heading ref", () => {
9
- const ref = createRef<HTMLHeadingElement>();
10
- render(<CardTitle ref={ref} id="card-title">Details</CardTitle>);
11
- expect(ref.current).toBe(screen.getByRole("heading", { level: 3 }));
12
- expect(ref.current).toHaveAttribute("id", "card-title");
13
- });
14
-
15
- it.each([1, 2, 3, 4, 5, 6] as const)("allows an h%i without changing its visual treatment", (level) => {
16
- const { rerender } = render(<CardTitle>Details</CardTitle>);
17
- const classes = screen.getByRole("heading").className;
18
- rerender(<CardTitle as={`h${level}` as CardTitleProps["as"]}>Details</CardTitle>);
19
- expect(screen.getByRole("heading", { level }).className).toBe(classes);
20
- });
21
-
22
- it("preserves native attributes and caller classes", () => {
23
- render(<CardTitle as="h2" role="presentation" className="text-xl" data-kind="summary">Summary</CardTitle>);
24
- const title = screen.getByText("Summary");
25
- expect(title).toHaveAttribute("role", "presentation");
26
- expect(title).toHaveAttribute("data-kind", "summary");
27
- expect(title).toHaveClass("text-xl");
28
- });
29
-
30
- it("uses the same gutter on every part and only removes subsequent top padding", () => {
31
- render(<Card>
32
- <CardHeader data-testid="header"><CardTitle>Details</CardTitle><CardDescription>Prose</CardDescription></CardHeader>
33
- <CardContent data-testid="content">Body</CardContent>
34
- <CardFooter data-testid="footer">Footer</CardFooter>
35
- </Card>);
36
- for (const part of ["header", "content", "footer"]) {
37
- expect(screen.getByTestId(part)).toHaveClass("p-4");
38
- expect(screen.getByTestId(part)).not.toHaveClass("p-6");
39
- }
40
- expect(screen.getByTestId("content")).toHaveClass("[&:not(:first-child)]:pt-0");
41
- expect(screen.getByTestId("footer")).toHaveClass("[&:not(:first-child)]:pt-0");
42
- // These are class/DOM contracts, not a browser-computed padding assertion.
43
- expect(screen.getByText("Prose").tagName).toBe("P");
44
- });
45
-
46
- it("does not strip the top padding unconditionally from content-only cards", () => {
47
- const ref = createRef<HTMLDivElement>();
48
- render(<Card><CardContent ref={ref}>Standalone</CardContent></Card>);
49
- expect(ref.current).toHaveClass("p-4");
50
- expect(ref.current).not.toHaveClass("pt-0");
51
- expect(ref.current?.matches(":first-child")).toBe(true);
52
- });
53
-
54
- it("keeps hover decorative and leaves keyboard activation to a real button", async () => {
55
- const user = userEvent.setup();
56
- const activate = vi.fn();
57
- const ref = createRef<HTMLDivElement>();
58
- render(<Card ref={ref} hover>
59
- <CardContent><button type="button" onClick={activate}>Open details</button></CardContent>
60
- </Card>);
61
- expect(ref.current?.tagName).toBe("DIV");
62
- expect(ref.current).not.toHaveAttribute("tabindex");
63
- expect(ref.current).not.toHaveAttribute("role");
64
- expect(ref.current).not.toHaveClass("cursor-pointer");
65
- await user.tab();
66
- expect(screen.getByRole("button", { name: "Open details" })).toHaveFocus();
67
- await user.keyboard("{Enter}");
68
- await user.keyboard(" ");
69
- expect(activate).toHaveBeenCalledTimes(2);
70
- });
71
- });
@@ -1,200 +0,0 @@
1
- import { fireEvent, render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import * as React from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { Chip } from "./chip";
6
- import { IconTile, initialsOf } from "./icon-tile";
7
- import { Tag } from "./tag";
8
- import { CATEGORY_TONES, TONE_CLASSES, toneFor } from "./tone";
9
-
10
- function FilterRow() {
11
- const [on, setOn] = React.useState<Record<string, boolean>>({ Drafts: true, Scheduled: false });
12
- return (
13
- <div>
14
- {Object.keys(on).map((name) => (
15
- <Chip
16
- key={name}
17
- selected={on[name]}
18
- onSelectedChange={(next) => setOn((prev) => ({ ...prev, [name]: next }))}
19
- >
20
- {name}
21
- </Chip>
22
- ))}
23
- </div>
24
- );
25
- }
26
-
27
- describe("Chip", () => {
28
- it("is a native button that runs onClick when it is an action chip", async () => {
29
- const onClick = vi.fn();
30
- render(<Chip onClick={onClick}>Draft a launch post</Chip>);
31
- const chip = screen.getByRole("button", { name: "Draft a launch post" });
32
- expect(chip).toHaveAttribute("type", "button");
33
- expect(chip).not.toHaveAttribute("aria-pressed");
34
- await userEvent.click(chip);
35
- expect(onClick).toHaveBeenCalledOnce();
36
- });
37
-
38
- it("toggles from the keyboard with Space and Enter and reports aria-pressed", async () => {
39
- const user = userEvent.setup();
40
- render(<FilterRow />);
41
- const drafts = screen.getByRole("button", { name: "Drafts" });
42
- const scheduled = screen.getByRole("button", { name: "Scheduled" });
43
- expect(drafts).toHaveAttribute("aria-pressed", "true");
44
- expect(scheduled).toHaveAttribute("aria-pressed", "false");
45
-
46
- await user.tab();
47
- expect(drafts).toHaveFocus();
48
- await user.keyboard(" ");
49
- expect(drafts).toHaveAttribute("aria-pressed", "false");
50
-
51
- await user.tab();
52
- expect(scheduled).toHaveFocus();
53
- await user.keyboard("{Enter}");
54
- expect(scheduled).toHaveAttribute("aria-pressed", "true");
55
- });
56
-
57
- // Selection must survive greyscale: the check replaces the icon only when on.
58
- it("shows a check glyph only while selected", () => {
59
- const { rerender } = render(
60
- <Chip selected={false} icon={<svg data-testid="icon" />}>
61
- Video
62
- </Chip>,
63
- );
64
- expect(screen.getByTestId("icon")).toBeInTheDocument();
65
- expect(document.querySelector(".lucide-check")).toBeNull();
66
- rerender(
67
- <Chip selected icon={<svg data-testid="icon" />}>
68
- Video
69
- </Chip>,
70
- );
71
- expect(screen.queryByTestId("icon")).toBeNull();
72
- expect(document.querySelector(".lucide-check")).not.toBeNull();
73
- });
74
-
75
- it("does not toggle while disabled", () => {
76
- const onSelectedChange = vi.fn();
77
- render(
78
- <Chip selected={false} disabled onSelectedChange={onSelectedChange}>
79
- Archived
80
- </Chip>,
81
- );
82
- fireEvent.click(screen.getByRole("button", { name: "Archived" }));
83
- expect(onSelectedChange).not.toHaveBeenCalled();
84
- });
85
-
86
- it("lets a consumer onClick veto the toggle with preventDefault", () => {
87
- const onSelectedChange = vi.fn();
88
- render(
89
- <Chip selected={false} onClick={(e) => e.preventDefault()} onSelectedChange={onSelectedChange}>
90
- Locked
91
- </Chip>,
92
- );
93
- fireEvent.click(screen.getByRole("button", { name: "Locked" }));
94
- expect(onSelectedChange).not.toHaveBeenCalled();
95
- });
96
- });
97
-
98
- describe("Tag", () => {
99
- it("is a static label with no tab stop of its own", async () => {
100
- render(<Tag tone="teal">Research</Tag>);
101
- expect(screen.getByText("Research")).toBeInTheDocument();
102
- expect(screen.queryByRole("button")).toBeNull();
103
- });
104
-
105
- it("removes from the keyboard through a named remove button", async () => {
106
- const user = userEvent.setup();
107
- const onRemove = vi.fn();
108
- render(<Tag onRemove={onRemove}>ada@example.com</Tag>);
109
- await user.tab();
110
- const remove = screen.getByRole("button", { name: "Remove ada@example.com" });
111
- expect(remove).toHaveFocus();
112
- await user.keyboard("{Enter}");
113
- await user.keyboard(" ");
114
- expect(onRemove).toHaveBeenCalledTimes(2);
115
- });
116
-
117
- it("takes an explicit remove label when the label is not plain text", () => {
118
- render(
119
- <Tag onRemove={() => {}} removeLabel="Remove the LinkedIn channel">
120
- <strong>LinkedIn</strong>
121
- </Tag>,
122
- );
123
- expect(screen.getByRole("button", { name: "Remove the LinkedIn channel" })).toBeInTheDocument();
124
- });
125
- });
126
-
127
- describe("Tag wrap", () => {
128
- const LONG = "Adafruit-BME680-Temperature-Humidity-Pressure-Gas-Sensor-Breakout";
129
-
130
- it("truncates a long label by default and names it in a title", () => {
131
- render(<Tag>{LONG}</Tag>);
132
- const label = screen.getByText(LONG);
133
- expect(label.className).toContain("truncate");
134
- expect(label.getAttribute("title")).toBe(LONG);
135
- expect(label.parentElement?.className).toContain("h-6");
136
- });
137
-
138
- it("wraps a long label across lines when asked, aligned to its first line", () => {
139
- render(<Tag wrap size="md">{LONG}</Tag>);
140
- const label = screen.getByText(LONG);
141
- expect(label.className).not.toContain("truncate");
142
- expect(label.className).toContain("[overflow-wrap:anywhere]");
143
- expect(label.className).toContain("py-[3px]");
144
- expect(label.parentElement?.className).toContain("items-start");
145
- expect(label.parentElement?.className).not.toMatch(/(^|\s)h-7(\s|$)/);
146
- });
147
- });
148
-
149
- describe("IconTile", () => {
150
- it("falls back from a broken image to the glyph, then to initials", () => {
151
- const { container, rerender } = render(<IconTile src="https://example.invalid/logo.png" name="Acme Robotics" />);
152
- const img = container.querySelector("img");
153
- expect(img).not.toBeNull();
154
- fireEvent.error(img as HTMLImageElement);
155
- expect(container.querySelector("img")).toBeNull();
156
- expect(container.textContent).toBe("AR");
157
-
158
- rerender(<IconTile name="Acme Robotics" icon={<svg data-testid="glyph" />} />);
159
- expect(screen.getByTestId("glyph")).toBeInTheDocument();
160
- });
161
-
162
- it("is decorative unless it is given a label", () => {
163
- const { container, rerender } = render(<IconTile name="Acme" />);
164
- expect(container.firstElementChild).toHaveAttribute("aria-hidden", "true");
165
- rerender(<IconTile name="Acme" label="Acme workspace" />);
166
- expect(screen.getByRole("img", { name: "Acme workspace" })).toBeInTheDocument();
167
- });
168
-
169
- it.each([
170
- ["Acme Robotics", "AR"],
171
- ["acme", "AC"],
172
- ["jane.doe", "JD"],
173
- [" ", ""],
174
- ["李 小龍", "李小"],
175
- ["🦊 fox", "🦊F"],
176
- ])("derives the initials of %j as %j", (name, initials) => {
177
- expect(initialsOf(name)).toBe(initials);
178
- });
179
- });
180
-
181
- describe("toneFor", () => {
182
- it("gives a key the same categorical tone every time", () => {
183
- expect(toneFor("linkedin")).toBe(toneFor("linkedin"));
184
- expect(CATEGORY_TONES).toContain(toneFor("linkedin"));
185
- });
186
-
187
- it("spreads keys across the categorical set", () => {
188
- const used = new Set(Array.from({ length: 64 }, (_, i) => toneFor(`project-${i}`)));
189
- expect(used.size).toBe(CATEGORY_TONES.length);
190
- });
191
-
192
- // The sandbox-ui style bundle and Tailwind read class names statically, so
193
- // every tone's classes must name their own tone's tokens literally.
194
- it.each(CATEGORY_TONES)("writes %s's classes against its own tokens", (tone) => {
195
- for (const value of Object.values(TONE_CLASSES[tone])) {
196
- expect(value).toContain(`--tone-${tone}-`);
197
- expect(value).not.toMatch(new RegExp(`--tone-(?!${tone}-)`));
198
- }
199
- });
200
- });
@@ -1,146 +0,0 @@
1
- import * as React from "react";
2
- import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { controlHeight } from "../lib/control-presentation";
6
- import { Button } from "./button";
7
- import { Calendar, formatCalendarDate, parseCalendarDate } from "./calendar";
8
- import { DatePicker } from "./date-picker";
9
- import { Select, SelectTrigger, SelectValue } from "./select";
10
- import { TimeSelect } from "./time-select";
11
- import { FilterField, Toolbar } from "./toolbar";
12
-
13
- afterEach(cleanup);
14
-
15
- describe("calendar dates", () => {
16
- it("round-trips YYYY-MM-DD in UTC and rejects impossible dates", () => {
17
- const time = parseCalendarDate("2026-03-08");
18
- expect(time).toBe(Date.UTC(2026, 2, 8));
19
- expect(formatCalendarDate(time!)).toBe("2026-03-08");
20
- expect(parseCalendarDate("2026-02-31")).toBeNull();
21
- expect(parseCalendarDate("03/08/2026")).toBeNull();
22
- });
23
- });
24
-
25
- describe("Calendar", () => {
26
- it("renders six weeks, marks today and the selection, and picks a day", async () => {
27
- const onChange = vi.fn();
28
- render(<Calendar value="2026-10-08" today="2026-10-08" onChange={onChange} locale="en-US" />);
29
- const grid = screen.getByRole("grid", { name: "October 2026" });
30
- expect(within(grid).getAllByRole("gridcell")).toHaveLength(42);
31
- const selected = screen.getByRole("button", { name: "Thursday, October 8, 2026" });
32
- expect(selected).toHaveAttribute("aria-current", "date");
33
- expect(selected.closest("[role=gridcell]")).toHaveAttribute("aria-selected", "true");
34
- await userEvent.click(screen.getByRole("button", { name: "Friday, October 16, 2026" }));
35
- expect(onChange).toHaveBeenCalledWith("2026-10-16");
36
- });
37
-
38
- it("moves by day, week and month from the keyboard and picks with Enter", async () => {
39
- const onChange = vi.fn();
40
- render(<Calendar value="2026-10-31" onChange={onChange} locale="en-US" />);
41
- const user = userEvent.setup();
42
- screen.getByRole("button", { name: "Saturday, October 31, 2026" }).focus();
43
- await user.keyboard("{ArrowRight}");
44
- expect(screen.getByRole("grid", { name: "November 2026" })).toBeTruthy();
45
- expect(document.activeElement).toHaveAccessibleName("Sunday, November 1, 2026");
46
- await user.keyboard("{ArrowDown}{PageUp}");
47
- expect(document.activeElement).toHaveAccessibleName("Thursday, October 8, 2026");
48
- await user.keyboard("{Enter}");
49
- expect(onChange).toHaveBeenCalledWith("2026-10-08");
50
- });
51
-
52
- it("refuses dates outside min and max", async () => {
53
- const onChange = vi.fn();
54
- render(<Calendar defaultMonth="2026-10-01" min="2026-10-10" onChange={onChange} locale="en-US" />);
55
- const early = screen.getByRole("button", { name: "Friday, October 9, 2026" });
56
- expect(early).toHaveAttribute("aria-disabled", "true");
57
- await userEvent.click(early);
58
- expect(onChange).not.toHaveBeenCalled();
59
- });
60
-
61
- it("starts weeks on Monday when asked", () => {
62
- render(<Calendar defaultMonth="2026-10-01" weekStartsOn={1} locale="en-US" />);
63
- expect(screen.getAllByRole("columnheader")[0]).toHaveAccessibleName("Monday");
64
- });
65
- });
66
-
67
- describe("DatePicker", () => {
68
- it("shows the date in the locale instead of a mm/dd/yyyy mask, on the control scale", () => {
69
- render(<DatePicker aria-label="Schedule for" value="2026-10-08" locale="en-US" />);
70
- const trigger = screen.getByRole("button", { name: "Schedule for" });
71
- expect(trigger).toHaveTextContent("Oct 8, 2026");
72
- expect(trigger.className.split(" ")).toContain(controlHeight.md);
73
- expect(trigger).toHaveAttribute("aria-haspopup", "dialog");
74
- });
75
-
76
- it("opens a calendar, reports the chosen date and closes", async () => {
77
- const onChange = vi.fn();
78
- render(<DatePicker aria-label="Schedule for" placeholder="Any date" defaultMonth="2026-10-01" onChange={onChange} locale="en-US" />);
79
- const trigger = screen.getByRole("button", { name: "Schedule for" });
80
- expect(trigger).toHaveTextContent("Any date");
81
- expect(trigger).toHaveAttribute("data-placeholder");
82
- await userEvent.click(trigger);
83
- await userEvent.click(await screen.findByRole("button", { name: "Tuesday, October 20, 2026" }));
84
- expect(onChange).toHaveBeenCalledWith("2026-10-20");
85
- expect(screen.queryByRole("grid")).toBeNull();
86
- });
87
-
88
- it("clears a chosen date and submits it with a name", async () => {
89
- const onChange = vi.fn();
90
- const { container } = render(<DatePicker aria-label="Due" name="due" value="2026-10-08" onChange={onChange} locale="en-US" />);
91
- expect(container.querySelector('input[type="hidden"][name="due"]')).toHaveValue("2026-10-08");
92
- await userEvent.click(screen.getByRole("button", { name: "Due" }));
93
- await userEvent.click(await screen.findByRole("button", { name: "Clear" }));
94
- expect(onChange).toHaveBeenCalledWith("");
95
- });
96
- });
97
-
98
- describe("TimeSelect", () => {
99
- it("lists step slots in the locale and keeps an off-step value", () => {
100
- render(<TimeSelect aria-label="Time" value="09:07" locale="en-US" step={30} />);
101
- const trigger = screen.getByRole("combobox", { name: "Time" });
102
- expect(trigger).toHaveTextContent("9:07 AM");
103
- expect(trigger.className.split(" ")).toContain(controlHeight.md);
104
- });
105
-
106
- it("reports HH:MM when a slot is picked", async () => {
107
- // jsdom lacks the layout APIs Radix Select calls while opening.
108
- Element.prototype.scrollIntoView ??= vi.fn();
109
- Element.prototype.hasPointerCapture ??= vi.fn(() => false);
110
- Element.prototype.releasePointerCapture ??= vi.fn();
111
- const onChange = vi.fn();
112
- render(<TimeSelect aria-label="Time" value="09:00" onChange={onChange} locale="en-US" step={60} />);
113
- const trigger = screen.getByRole("combobox", { name: "Time" });
114
- trigger.focus();
115
- fireEvent.keyDown(trigger, { key: "Enter" });
116
- fireEvent.click(await screen.findByRole("option", { name: "10:00 AM" }));
117
- expect(onChange).toHaveBeenCalledWith("10:00");
118
- });
119
- });
120
-
121
- describe("raised fields in a Toolbar", () => {
122
- it("lifts fields inside a Toolbar onto the card surface and leaves others in the well", () => {
123
- render(
124
- <>
125
- <Toolbar data-testid="toolbar" filters={<FilterField label="Format"><Select><SelectTrigger aria-label="Format"><SelectValue /></SelectTrigger></Select></FilterField>} />
126
- <Select><SelectTrigger aria-label="Elsewhere"><SelectValue /></SelectTrigger></Select>
127
- </>,
128
- );
129
- expect(screen.getByTestId("toolbar").className).toContain("[--field-surface:hsl(var(--card))]");
130
- for (const name of ["Format", "Elsewhere"]) {
131
- expect(screen.getByRole("combobox", { name }).className).toContain("bg-[var(--field-surface,var(--well))]");
132
- }
133
- });
134
- });
135
-
136
- describe("Button bare", () => {
137
- it("keeps focus and disabled behavior without surface or size classes", () => {
138
- render(<Button variant="bare" className="flex w-full px-4 py-2" disabled>Row</Button>);
139
- const row = screen.getByRole("button", { name: "Row" });
140
- expect(row).toBeDisabled();
141
- expect(row.className).toContain("focus-visible:ring-4");
142
- expect(row.className).not.toContain("h-[var(--control-height");
143
- expect(row.className).not.toContain("inline-flex");
144
- expect(row.className).toContain("px-4");
145
- });
146
- });
@@ -1,75 +0,0 @@
1
- import { createRef, type ComponentProps } from "react";
2
- import { render, screen, waitFor } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "./dialog";
6
-
7
- function Example(props: ComponentProps<typeof DialogContent>) {
8
- return (
9
- <Dialog defaultOpen>
10
- <DialogContent {...props}>
11
- <DialogTitle>Confirm operation</DialogTitle>
12
- <DialogDescription>Choose whether to continue.</DialogDescription>
13
- <DialogClose asChild><button type="button">Cancel operation</button></DialogClose>
14
- </DialogContent>
15
- </Dialog>
16
- );
17
- }
18
-
19
- describe("DialogContent close-button visibility", () => {
20
- it("retains the default accessible close button and its dismissal behavior", async () => {
21
- const user = userEvent.setup();
22
- render(<Example />);
23
- expect(screen.getByRole("dialog", { name: "Confirm operation" })).toBeInTheDocument();
24
- await user.click(screen.getByRole("button", { name: "Close", exact: true }));
25
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
26
- });
27
-
28
- it.each(["default", "sandbox"] as const)("hides only the built-in button for the %s variant", async (variant) => {
29
- const user = userEvent.setup();
30
- render(<Example hideCloseButton variant={variant} />);
31
- expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
32
- expect(screen.getByRole("dialog")).not.toHaveAttribute("hideCloseButton");
33
- expect(screen.getByRole("dialog")).toHaveAccessibleDescription("Choose whether to continue.");
34
- await user.click(screen.getByRole("button", { name: "Cancel operation" }));
35
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
36
- });
37
-
38
- it("restores the same default button when visibility changes", () => {
39
- const { rerender } = render(<Example hideCloseButton />);
40
- expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
41
- rerender(<Example hideCloseButton={false} />);
42
- expect(screen.getByRole("button", { name: "Close", exact: true })).toBeInTheDocument();
43
- rerender(<Example hideCloseButton />);
44
- expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
45
- });
46
-
47
- it("does not silently disable Escape when the close button is hidden", async () => {
48
- const user = userEvent.setup();
49
- render(<Example hideCloseButton />);
50
- await user.keyboard("{Escape}");
51
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
52
- });
53
-
54
- it("preserves the caller's Escape guard and an explicit keyboard-operable exit", async () => {
55
- const user = userEvent.setup();
56
- const guard = vi.fn((event: KeyboardEvent) => event.preventDefault());
57
- render(<Example hideCloseButton onEscapeKeyDown={guard} />);
58
- await user.keyboard("{Escape}");
59
- expect(guard).toHaveBeenCalledOnce();
60
- expect(screen.getByRole("dialog")).toBeInTheDocument();
61
- screen.getByRole("button", { name: "Cancel operation" }).focus();
62
- await user.keyboard("{Enter}");
63
- await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
64
- });
65
-
66
- it("forwards content refs, native attributes and class overrides", () => {
67
- const ref = createRef<HTMLDivElement>();
68
- render(<Example ref={ref} hideCloseButton id="operation-dialog" data-example="retained" className="max-w-xl" />);
69
- const dialog = screen.getByRole("dialog");
70
- expect(ref.current).toBe(dialog);
71
- expect(dialog).toHaveAttribute("id", "operation-dialog");
72
- expect(dialog).toHaveAttribute("data-example", "retained");
73
- expect(dialog).toHaveClass("max-w-xl");
74
- });
75
- });
@@ -1,161 +0,0 @@
1
- import * as React from "react";
2
- import { act, cleanup, render, screen, within } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { hydrateRoot, type Root } from "react-dom/client";
5
- import { renderToString } from "react-dom/server";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
- import { Input, Textarea } from "./input";
8
- import { Label } from "./label";
9
-
10
- afterEach(cleanup);
11
-
12
- describe.each([
13
- ["Input", Input],
14
- ["Textarea", Textarea],
15
- ] as const)("%s relationships", (_name, Field) => {
16
- it("gives repeated labels distinct, stable IDs and focuses the correct field", async () => {
17
- const user = userEvent.setup();
18
- const { rerender } = render(<><Field label="Name" /><Field label="Name" /></>);
19
- const fields = screen.getAllByRole("textbox");
20
- const ids = fields.map((field) => field.id);
21
- expect(ids.every(Boolean)).toBe(true);
22
- expect(new Set(ids).size).toBe(2);
23
- expect(screen.getAllByLabelText("Name")).toEqual(fields);
24
- for (const [index, label] of screen.getAllByText("Name", { selector: "label" }).entries()) {
25
- expect(label).toHaveAttribute("for", ids[index]);
26
- await user.click(label);
27
- expect(fields[index]).toHaveFocus();
28
- }
29
- rerender(<><Field label="Renamed" /><Field label="Renamed" /></>);
30
- expect(screen.getAllByRole("textbox").map((field) => field.id)).toEqual(ids);
31
- });
32
-
33
- it("preserves an explicit ID and merges caller, hint and error descriptions without duplicates", () => {
34
- render(<>
35
- <p id="external">External instructions.</p>
36
- <Field
37
- id="profile"
38
- label="Name"
39
- hint="Use your full name."
40
- error="Name is required."
41
- aria-describedby={" external external\tprofile-hint "}
42
- aria-invalid={false}
43
- />
44
- </>);
45
- const field = screen.getByRole("textbox", { name: "Name" });
46
- expect(field).toHaveAttribute("id", "profile");
47
- expect(field).toHaveAttribute("aria-describedby", "external profile-hint profile-error");
48
- expect(screen.getByText("Use your full name.")).toHaveAttribute("id", "profile-hint");
49
- expect(screen.getByText("Name is required.")).toHaveAttribute("id", "profile-error");
50
- expect(field).toHaveAccessibleDescription("External instructions. Use your full name. Name is required.");
51
- expect(field).toHaveAttribute("aria-invalid", "true");
52
- });
53
-
54
- it("associates descriptions without a convenience label and removes stale relationships", () => {
55
- const { rerender } = render(<Field aria-label="Name" hint="A hint" error="An error" />);
56
- const field = screen.getByRole("textbox", { name: "Name" });
57
- const id = field.id;
58
- expect(id).not.toBe("");
59
- expect(field).toHaveAccessibleDescription("A hint An error");
60
- rerender(<Field aria-label="Name" hint="A hint" />);
61
- expect(field.id).toBe(id);
62
- expect(field).toHaveAttribute("aria-describedby", `${id}-hint`);
63
- expect(field).not.toHaveAttribute("aria-invalid");
64
- expect(document.getElementById(`${id}-error`)).toBeNull();
65
- rerender(<Field aria-label="Name" />);
66
- const bareField = screen.getByRole("textbox", { name: "Name" });
67
- expect(bareField.id).toBe(id);
68
- expect(bareField).not.toHaveAttribute("aria-describedby");
69
- expect(document.getElementById(`${id}-hint`)).toBeNull();
70
- });
71
-
72
- it("preserves the bare-element API and caller invalid states", () => {
73
- const { container, rerender } = render(
74
- <Field aria-label="Name" aria-describedby="external" aria-invalid="spelling" className="caller-class" />,
75
- );
76
- const field = screen.getByRole("textbox");
77
- expect(container.firstElementChild).toBe(field);
78
- expect(container.childElementCount).toBe(1);
79
- expect(field).toHaveAttribute("aria-describedby", "external");
80
- expect(field).toHaveAttribute("aria-invalid", "spelling");
81
- expect(field).toHaveClass("caller-class");
82
- rerender(<Field aria-label="Name" aria-invalid={false} />);
83
- expect(field).toHaveAttribute("aria-invalid", "false");
84
- });
85
-
86
- it("uses the existing Label treatment", () => {
87
- render(<><Label className="block">Reference</Label><Field label="Name" /></>);
88
- expect(screen.getByText("Name", { selector: "label" }).className)
89
- .toBe(screen.getByText("Reference").className);
90
- });
91
- });
92
-
93
- it("keeps Input and Textarea IDs distinct even with the same label", () => {
94
- render(<><Input label="Notes" /><Textarea label="Notes" /></>);
95
- expect(new Set(screen.getAllByRole("textbox").map((field) => field.id)).size).toBe(2);
96
- });
97
-
98
- it("exposes Input's error variant without requiring error text", () => {
99
- render(<Input aria-label="Name" variant="error" />);
100
- expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
101
- });
102
-
103
- it("forwards native field refs, values and change handlers", async () => {
104
- const user = userEvent.setup();
105
- const inputRef = React.createRef<HTMLInputElement>();
106
- const textareaRef = React.createRef<HTMLTextAreaElement>();
107
- const inputChange = vi.fn();
108
- const textareaChange = vi.fn();
109
- const { unmount } = render(<>
110
- <Input ref={inputRef} label="Name" defaultValue="A" onChange={inputChange} />
111
- <Textarea ref={textareaRef} label="Notes" defaultValue="B" onChange={textareaChange} />
112
- </>);
113
- expect(inputRef.current).toBe(screen.getByRole("textbox", { name: "Name" }));
114
- expect(textareaRef.current).toBe(screen.getByRole("textbox", { name: "Notes" }));
115
- await user.type(inputRef.current!, "lice");
116
- await user.type(textareaRef.current!, "ody");
117
- expect(inputRef.current).toHaveValue("Alice");
118
- expect(textareaRef.current).toHaveValue("Body");
119
- expect(inputChange).toHaveBeenCalled();
120
- expect(textareaChange).toHaveBeenCalled();
121
- unmount();
122
- expect(inputRef.current).toBeNull();
123
- expect(textareaRef.current).toBeNull();
124
- });
125
-
126
- it("hydrates SSR IDs and all description relationships without mismatch", async () => {
127
- const ui = <React.StrictMode>
128
- <p id="external">External instructions.</p>
129
- <Input label="Name" hint="First hint" />
130
- <Input label="Name" error="Required" />
131
- <Textarea label="Name" hint="Notes hint" error="Too short" aria-describedby="external" />
132
- <Input id="explicit" label="Explicit" hint="Explicit hint" />
133
- </React.StrictMode>;
134
- const container = document.createElement("div");
135
- container.innerHTML = renderToString(ui, { identifierPrefix: "fields-" });
136
- document.body.appendChild(container);
137
- const serverHTML = container.innerHTML;
138
- const serverIds = within(container).getAllByRole("textbox").map((field) => field.id);
139
- const recoverableError = vi.fn();
140
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
141
- let root: Root | undefined;
142
- try {
143
- await act(async () => {
144
- root = hydrateRoot(container, ui, { identifierPrefix: "fields-", onRecoverableError: recoverableError });
145
- });
146
- expect(new Set(serverIds).size).toBe(4);
147
- expect(within(container).getAllByRole("textbox").map((field) => field.id)).toEqual(serverIds);
148
- expect(container.innerHTML).toBe(serverHTML);
149
- for (const field of within(container).getAllByRole("textbox")) {
150
- for (const id of field.getAttribute("aria-describedby")!.split(" ")) {
151
- expect(document.getElementById(id)).not.toBeNull();
152
- }
153
- }
154
- expect(recoverableError).not.toHaveBeenCalled();
155
- expect(consoleError).not.toHaveBeenCalled();
156
- } finally {
157
- await act(async () => { root?.unmount(); });
158
- consoleError.mockRestore();
159
- container.remove();
160
- }
161
- });