@tangle-network/ui 11.51.7 → 11.51.9
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/CHANGELOG.md +10 -0
- package/README.md +6 -0
- package/dist/chat/chat-message.js +2 -7
- package/dist/chat/user-message.js +3 -5
- package/dist/files/file-card.js +1 -1
- package/dist/files/file-tree.d.ts +0 -6
- package/dist/files/file-tree.js +2 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/primitives/hover-card.d.ts +22 -13
- package/dist/primitives/hover-card.js +149 -40
- package/dist/primitives/popover.d.ts +3 -1
- package/dist/primitives/popover.js +15 -12
- package/dist/primitives/upload-progress.d.ts +0 -6
- package/dist/primitives/upload-progress.js +2 -6
- package/dist/run/run-group.js +2 -2
- package/dist/run/run-item-primitives.js +2 -2
- package/dist/run/run-row-shell.js +2 -2
- package/dist/utils/format.d.ts +14 -12
- package/dist/utils/format.js +50 -30
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +2 -2
- package/package.json +11 -2
- package/src/chat/chat-message.tsx +2 -7
- package/src/chat/user-message.tsx +3 -5
- package/src/editor/source-markdown-editor.test.tsx +7 -7
- package/src/files/file-tree.tsx +2 -7
- package/src/primitives/hover-card.tsx +64 -74
- package/src/primitives/popover.tsx +3 -3
- package/src/primitives/upload-progress.tsx +2 -7
- package/src/utils/format.ts +50 -33
- package/src/utils/index.ts +1 -1
- package/src/editor/document-editor-pane.test.tsx +0 -60
- package/src/editor/editor-lazy.test.tsx +0 -102
- package/src/editor/editor-peers.test.ts +0 -250
- package/src/editor/editor-provider.test.tsx +0 -226
- package/src/editor/markdown-document-editor.test.tsx +0 -126
- package/src/editor/tiptap-editor.test.tsx +0 -228
- package/src/files/file-artifact-pane.test.tsx +0 -301
- package/src/files/file-card.test.tsx +0 -62
- package/src/files/file-list.test.tsx +0 -81
- package/src/files/pdf-loader.test.ts +0 -130
- package/src/files/pdf-viewer.test.tsx +0 -245
- package/src/files/rich-file-tree.test.tsx +0 -90
- package/src/markdown/api-presentation.test.tsx +0 -63
- package/src/markdown/code-block.test.tsx +0 -201
- package/src/markdown/markdown-table.test.tsx +0 -76
- package/src/markdown/markdown.test.tsx +0 -166
- package/src/openui/schema.test.tsx +0 -158
- package/src/primitives/button.test.tsx +0 -230
- package/src/primitives/card.test.tsx +0 -71
- package/src/primitives/chip.test.tsx +0 -200
- package/src/primitives/date-picker.test.tsx +0 -146
- package/src/primitives/dialog.test.tsx +0 -75
- package/src/primitives/input.test.tsx +0 -161
- package/src/primitives/interaction-states.test.tsx +0 -287
- package/src/primitives/pagination-alert.test.tsx +0 -180
- package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +0 -165
- package/src/primitives/radio-group.test.tsx +0 -26
- package/src/primitives/segmented-control.test.tsx +0 -451
- package/src/primitives/table.test.tsx +0 -72
- package/src/primitives/theme-toggle.test.tsx +0 -168
- package/src/primitives/toast.test.tsx +0 -130
- package/src/primitives/wizard.test.tsx +0 -106
- package/src/run/expandable-content.test.tsx +0 -52
- package/src/utils/format.test.ts +0 -58
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { act, cleanup, fireEvent, 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 { Button } from "./button";
|
|
8
|
-
|
|
9
|
-
afterEach(cleanup);
|
|
10
|
-
|
|
11
|
-
describe.each(["disabled", "loading"] as const)("%s controls", (state) => {
|
|
12
|
-
it.each([false, true])("natively disables button elements (asChild=%s)", async (asChild) => {
|
|
13
|
-
const user = userEvent.setup();
|
|
14
|
-
const click = vi.fn();
|
|
15
|
-
const submit = vi.fn((event: React.FormEvent) => event.preventDefault());
|
|
16
|
-
render(<form onSubmit={submit}>
|
|
17
|
-
<Button {...{ [state]: true }} asChild={asChild} onClick={click}>
|
|
18
|
-
{asChild ? <button disabled={false} aria-busy={false}>Save</button> : "Save"}
|
|
19
|
-
</Button>
|
|
20
|
-
<input aria-label="Next field" />
|
|
21
|
-
</form>);
|
|
22
|
-
const button = screen.getByRole("button", { name: "Save" });
|
|
23
|
-
expect(button).toBeDisabled();
|
|
24
|
-
expect(button).toHaveAccessibleName("Save");
|
|
25
|
-
if (state === "loading") expect(button).toHaveAttribute("aria-busy", "true");
|
|
26
|
-
await user.click(button);
|
|
27
|
-
button.click();
|
|
28
|
-
button.focus();
|
|
29
|
-
expect(button).not.toHaveFocus();
|
|
30
|
-
await user.keyboard("{Enter} ");
|
|
31
|
-
expect(click).not.toHaveBeenCalled();
|
|
32
|
-
expect(submit).not.toHaveBeenCalled();
|
|
33
|
-
await user.tab();
|
|
34
|
-
expect(screen.getByRole("textbox")).toHaveFocus();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("blocks slotted anchor activation before child AND slot capture/bubble handlers", async () => {
|
|
38
|
-
const user = userEvent.setup();
|
|
39
|
-
const activation = vi.fn();
|
|
40
|
-
const handlers = {
|
|
41
|
-
onClick: activation, onClickCapture: activation,
|
|
42
|
-
onAuxClick: activation, onAuxClickCapture: activation,
|
|
43
|
-
onDoubleClick: activation, onDoubleClickCapture: activation,
|
|
44
|
-
onPointerDown: activation, onPointerDownCapture: activation,
|
|
45
|
-
onPointerUp: activation, onPointerUpCapture: activation,
|
|
46
|
-
onMouseDown: activation, onMouseDownCapture: activation,
|
|
47
|
-
onMouseUp: activation, onMouseUpCapture: activation,
|
|
48
|
-
onKeyDown: activation, onKeyDownCapture: activation,
|
|
49
|
-
onKeyUp: activation, onKeyUpCapture: activation,
|
|
50
|
-
};
|
|
51
|
-
render(<div onClick={activation}>
|
|
52
|
-
<Button asChild {...{ [state]: true }} aria-busy={false} {...handlers}>
|
|
53
|
-
<a href="#destination" tabIndex={0} aria-disabled={false} aria-busy={false} {...handlers}>
|
|
54
|
-
<span>Open account</span>
|
|
55
|
-
</a>
|
|
56
|
-
</Button>
|
|
57
|
-
<button>Next action</button>
|
|
58
|
-
</div>);
|
|
59
|
-
const link = screen.getByRole("link", { name: "Open account" });
|
|
60
|
-
expect(link).toHaveAttribute("aria-disabled", "true");
|
|
61
|
-
expect(link).toHaveAttribute("tabindex", "-1");
|
|
62
|
-
expect(link).not.toHaveAttribute("disabled");
|
|
63
|
-
expect(link).not.toHaveAttribute("href");
|
|
64
|
-
if (state === "loading") expect(link).toHaveAttribute("aria-busy", "true");
|
|
65
|
-
const target = screen.getByText("Open account");
|
|
66
|
-
expect(fireEvent.click(target)).toBe(false);
|
|
67
|
-
expect(fireEvent.doubleClick(target)).toBe(false);
|
|
68
|
-
expect(fireEvent(target, new MouseEvent("auxclick", { bubbles: true, cancelable: true, button: 1 }))).toBe(false);
|
|
69
|
-
expect(fireEvent.pointerDown(target)).toBe(false);
|
|
70
|
-
expect(fireEvent.pointerUp(target)).toBe(false);
|
|
71
|
-
expect(fireEvent.mouseDown(target)).toBe(false);
|
|
72
|
-
expect(fireEvent.mouseUp(target)).toBe(false);
|
|
73
|
-
for (const key of ["Enter", " "]) {
|
|
74
|
-
expect(fireEvent.keyDown(link, { key })).toBe(false);
|
|
75
|
-
expect(fireEvent.keyUp(link, { key })).toBe(false);
|
|
76
|
-
}
|
|
77
|
-
await user.click(target);
|
|
78
|
-
link.click();
|
|
79
|
-
// Even programmatically focused, a disabled link cannot activate by keyboard.
|
|
80
|
-
link.focus();
|
|
81
|
-
await user.keyboard("{Enter} ");
|
|
82
|
-
expect(activation).not.toHaveBeenCalled();
|
|
83
|
-
link.blur();
|
|
84
|
-
await user.tab();
|
|
85
|
-
expect(screen.getByRole("button", { name: "Next action" })).toHaveFocus();
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("retains the action name and hides the reduced-motion spinner from accessibility", () => {
|
|
90
|
-
const { rerender } = render(<Button loading aria-busy={false}>Save changes</Button>);
|
|
91
|
-
const button = screen.getByRole("button", { name: "Save changes" });
|
|
92
|
-
expect(button).toHaveAttribute("aria-busy", "true");
|
|
93
|
-
const spinner = button.querySelector("svg");
|
|
94
|
-
expect(spinner).toHaveAttribute("aria-hidden", "true");
|
|
95
|
-
expect(spinner).toHaveAttribute("focusable", "false");
|
|
96
|
-
expect(spinner).toHaveClass("motion-reduce:animate-none");
|
|
97
|
-
expect(button).toHaveClass("motion-reduce:transition-none", "motion-reduce:active:scale-100");
|
|
98
|
-
expect(button.querySelector("title")).toBeNull();
|
|
99
|
-
rerender(<Button loading size="icon" aria-label="Refresh reports"><span aria-hidden="true">↻</span></Button>);
|
|
100
|
-
expect(screen.getByRole("button")).toHaveAccessibleName("Refresh reports");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it.each(["disabled", "loading"] as const)("restores link attributes and original Slot handler order when %s ends", async (state) => {
|
|
104
|
-
const user = userEvent.setup();
|
|
105
|
-
const calls: string[] = [];
|
|
106
|
-
const ui = (unavailable: boolean) => <Button asChild {...{ [state]: unavailable }} onClick={() => calls.push("slot")}>
|
|
107
|
-
<a href="#destination" tabIndex={2} onClick={(event) => { event.preventDefault(); calls.push("child"); }}>Open</a>
|
|
108
|
-
</Button>;
|
|
109
|
-
const { rerender } = render(ui(true));
|
|
110
|
-
const link = screen.getByRole("link");
|
|
111
|
-
rerender(ui(false));
|
|
112
|
-
expect(link).toHaveAttribute("href", "#destination");
|
|
113
|
-
expect(link).toHaveAttribute("tabindex", "2");
|
|
114
|
-
expect(link).not.toHaveAttribute("aria-disabled");
|
|
115
|
-
expect(link).not.toHaveAttribute("aria-busy");
|
|
116
|
-
expect(link).not.toHaveAttribute("role");
|
|
117
|
-
await user.click(link);
|
|
118
|
-
expect(calls).toEqual(["child", "slot"]);
|
|
119
|
-
calls.length = 0;
|
|
120
|
-
link.focus();
|
|
121
|
-
await user.keyboard("{Enter}");
|
|
122
|
-
expect(calls).toEqual(["child", "slot"]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("preserves non-activation keyboard handlers on disabled links", () => {
|
|
126
|
-
const calls: string[] = [];
|
|
127
|
-
render(<Button asChild disabled onKeyDownCapture={() => calls.push("slot capture")} onKeyDown={() => calls.push("slot bubble")}>
|
|
128
|
-
<a href="#destination" onKeyDownCapture={() => calls.push("child capture")} onKeyDown={() => calls.push("child bubble")}>Open</a>
|
|
129
|
-
</Button>);
|
|
130
|
-
expect(fireEvent.keyDown(screen.getByRole("link"), { key: "ArrowRight" })).toBe(true);
|
|
131
|
-
expect(calls).toEqual(["child capture", "slot capture", "child bubble", "slot bubble"]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("respects a native child's own disabled state", () => {
|
|
135
|
-
render(<Button asChild disabled={false}><button disabled>Save</button></Button>);
|
|
136
|
-
expect(screen.getByRole("button")).toBeDisabled();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("composes refs on a slotted native button", () => {
|
|
140
|
-
const outerRef = React.createRef<HTMLButtonElement>();
|
|
141
|
-
const childRef = React.createRef<HTMLButtonElement>();
|
|
142
|
-
const { unmount } = render(<Button asChild ref={outerRef}><button ref={childRef}>Save</button></Button>);
|
|
143
|
-
expect(outerRef.current).toBe(screen.getByRole("button"));
|
|
144
|
-
expect(childRef.current).toBe(outerRef.current);
|
|
145
|
-
unmount();
|
|
146
|
-
expect(outerRef.current).toBeNull();
|
|
147
|
-
expect(childRef.current).toBeNull();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("preserves caller busy state when Button is not loading", () => {
|
|
151
|
-
render(<><Button aria-busy>Save</Button><Button asChild aria-busy><a href="#destination">Open</a></Button></>);
|
|
152
|
-
expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true");
|
|
153
|
-
expect(screen.getByRole("link")).toHaveAttribute("aria-busy", "true");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("preserves implicit submission, explicit types, and enabled keyboard activation", async () => {
|
|
157
|
-
const user = userEvent.setup();
|
|
158
|
-
const submit = vi.fn((event: React.FormEvent) => event.preventDefault());
|
|
159
|
-
render(<form onSubmit={submit}>
|
|
160
|
-
<Button>Implicit submit</Button>
|
|
161
|
-
<Button asChild><button>Slotted submit</button></Button>
|
|
162
|
-
<Button type="button">Explicit button</Button>
|
|
163
|
-
<Button type="submit">Explicit submit</Button>
|
|
164
|
-
<Button asChild type="submit"><button type="button">Child button</button></Button>
|
|
165
|
-
</form>);
|
|
166
|
-
const implicit = screen.getByRole("button", { name: "Implicit submit" });
|
|
167
|
-
expect(implicit).not.toHaveAttribute("type");
|
|
168
|
-
await user.click(implicit);
|
|
169
|
-
await user.click(screen.getByRole("button", { name: "Slotted submit" }));
|
|
170
|
-
await user.click(screen.getByRole("button", { name: "Explicit submit" }));
|
|
171
|
-
expect(submit).toHaveBeenCalledTimes(3);
|
|
172
|
-
await user.click(screen.getByRole("button", { name: "Explicit button" }));
|
|
173
|
-
await user.click(screen.getByRole("button", { name: "Child button" }));
|
|
174
|
-
expect(submit).toHaveBeenCalledTimes(3);
|
|
175
|
-
implicit.focus();
|
|
176
|
-
await user.keyboard("{Enter} ");
|
|
177
|
-
expect(submit).toHaveBeenCalledTimes(5);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("forwards native refs and composes both refs on a slotted anchor", () => {
|
|
181
|
-
const nativeRef = React.createRef<HTMLButtonElement>();
|
|
182
|
-
const anchorRef = React.createRef<HTMLAnchorElement>();
|
|
183
|
-
const slotRef = vi.fn();
|
|
184
|
-
const { rerender, unmount } = render(<>
|
|
185
|
-
<Button ref={nativeRef}>Save</Button>
|
|
186
|
-
<Button asChild loading ref={slotRef}><a ref={anchorRef} href="#destination">Open</a></Button>
|
|
187
|
-
</>);
|
|
188
|
-
const link = screen.getByRole("link");
|
|
189
|
-
expect(nativeRef.current).toBe(screen.getByRole("button"));
|
|
190
|
-
expect(anchorRef.current).toBe(link);
|
|
191
|
-
expect(slotRef).toHaveBeenLastCalledWith(link);
|
|
192
|
-
rerender(<>
|
|
193
|
-
<Button ref={nativeRef}>Save</Button>
|
|
194
|
-
<Button asChild ref={slotRef}><a ref={anchorRef} href="#destination">Open</a></Button>
|
|
195
|
-
</>);
|
|
196
|
-
expect(anchorRef.current).toBe(link);
|
|
197
|
-
expect(slotRef).toHaveBeenLastCalledWith(link);
|
|
198
|
-
unmount();
|
|
199
|
-
expect(nativeRef.current).toBeNull();
|
|
200
|
-
expect(anchorRef.current).toBeNull();
|
|
201
|
-
expect(slotRef).toHaveBeenLastCalledWith(null);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("hydrates disabled/loading native and slotted controls without changing their names", async () => {
|
|
205
|
-
const ui = <>
|
|
206
|
-
<Button loading>Save</Button>
|
|
207
|
-
<Button asChild loading><button>Submit</button></Button>
|
|
208
|
-
<Button asChild disabled><a href="#destination">Open</a></Button>
|
|
209
|
-
</>;
|
|
210
|
-
const container = document.createElement("div");
|
|
211
|
-
container.innerHTML = renderToString(ui);
|
|
212
|
-
document.body.appendChild(container);
|
|
213
|
-
const html = container.innerHTML;
|
|
214
|
-
const recoverableError = vi.fn();
|
|
215
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
216
|
-
let root: Root | undefined;
|
|
217
|
-
try {
|
|
218
|
-
await act(async () => { root = hydrateRoot(container, ui, { onRecoverableError: recoverableError }); });
|
|
219
|
-
expect(container.innerHTML).toBe(html);
|
|
220
|
-
expect(within(container).getByRole("button", { name: "Save" })).toBeDisabled();
|
|
221
|
-
expect(within(container).getByRole("button", { name: "Submit" })).toBeDisabled();
|
|
222
|
-
expect(within(container).getByRole("link", { name: "Open" })).not.toHaveAttribute("href");
|
|
223
|
-
expect(recoverableError).not.toHaveBeenCalled();
|
|
224
|
-
expect(consoleError).not.toHaveBeenCalled();
|
|
225
|
-
} finally {
|
|
226
|
-
await act(async () => { root?.unmount(); });
|
|
227
|
-
consoleError.mockRestore();
|
|
228
|
-
container.remove();
|
|
229
|
-
}
|
|
230
|
-
});
|
|
@@ -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
|
-
});
|