@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,168 +0,0 @@
|
|
|
1
|
-
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
-
import { hydrateRoot } from "react-dom/client";
|
|
3
|
-
import { renderToString } from "react-dom/server";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
let ThemeToggle: typeof import("./theme-toggle").ThemeToggle;
|
|
7
|
-
let useTheme: typeof import("./theme-toggle").useTheme;
|
|
8
|
-
let dark = false;
|
|
9
|
-
let listeners: Set<() => void>;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
vi.resetModules();
|
|
13
|
-
for (const attr of ["class", "data-theme", "data-sandbox-theme"]) document.documentElement.removeAttribute(attr);
|
|
14
|
-
window.localStorage.clear();
|
|
15
|
-
dark = false;
|
|
16
|
-
listeners = new Set();
|
|
17
|
-
vi.stubGlobal("matchMedia", vi.fn(() => ({
|
|
18
|
-
get matches() { return dark; },
|
|
19
|
-
media: "(prefers-color-scheme: dark)",
|
|
20
|
-
addEventListener: (_: string, handler: () => void) => listeners.add(handler),
|
|
21
|
-
removeEventListener: (_: string, handler: () => void) => listeners.delete(handler),
|
|
22
|
-
})));
|
|
23
|
-
({ ThemeToggle, useTheme } = await import("./theme-toggle"));
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
cleanup();
|
|
28
|
-
vi.restoreAllMocks();
|
|
29
|
-
vi.unstubAllGlobals();
|
|
30
|
-
for (const attr of ["class", "data-theme", "data-sandbox-theme"]) document.documentElement.removeAttribute(attr);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
function systemMode(next: boolean) {
|
|
34
|
-
act(() => { dark = next; listeners.forEach((notify) => notify()); });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function PreferenceControl() {
|
|
38
|
-
const { theme, resolvedTheme, setTheme } = useTheme();
|
|
39
|
-
return <button onClick={() => setTheme("system")}>{theme}:{resolvedTheme}</button>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("root mode preference", () => {
|
|
43
|
-
it("tracks system changes in the DOM, icon label and click behavior", () => {
|
|
44
|
-
render(<ThemeToggle />);
|
|
45
|
-
expect(screen.getByRole("button", { name: "Switch to dark mode" })).toBeTruthy();
|
|
46
|
-
expect(document.documentElement.classList.contains("light")).toBe(true);
|
|
47
|
-
systemMode(true);
|
|
48
|
-
fireEvent.click(screen.getByRole("button", { name: "Switch to light mode" }));
|
|
49
|
-
expect(window.localStorage.getItem("theme")).toBe("light");
|
|
50
|
-
expect(document.documentElement.className).toBe("light");
|
|
51
|
-
systemMode(false); systemMode(true);
|
|
52
|
-
expect(screen.getByRole("button", { name: "Switch to dark mode" })).toBeTruthy();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("shares explicit choices and a return to system across mounted consumers", () => {
|
|
56
|
-
window.localStorage.setItem("theme", "dark");
|
|
57
|
-
render(<><ThemeToggle /><PreferenceControl /></>);
|
|
58
|
-
fireEvent.click(screen.getByRole("button", { name: "Switch to light mode" }));
|
|
59
|
-
expect(screen.getByRole("button", { name: "light:light" })).toBeTruthy();
|
|
60
|
-
fireEvent.click(screen.getByRole("button", { name: "light:light" }));
|
|
61
|
-
expect(window.localStorage.getItem("theme")).toBeNull();
|
|
62
|
-
systemMode(true);
|
|
63
|
-
expect(screen.getByRole("button", { name: "system:dark" })).toBeTruthy();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("remains usable when storage reads, writes and removals throw", () => {
|
|
67
|
-
for (const method of ["getItem", "setItem", "removeItem"] as const) {
|
|
68
|
-
vi.spyOn(Storage.prototype, method).mockImplementation(() => { throw new Error("denied"); });
|
|
69
|
-
}
|
|
70
|
-
render(<><ThemeToggle /><PreferenceControl /></>);
|
|
71
|
-
fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
|
|
72
|
-
expect(screen.getByRole("button", { name: "dark:dark" })).toBeTruthy();
|
|
73
|
-
fireEvent.click(screen.getByRole("button", { name: "dark:dark" }));
|
|
74
|
-
expect(screen.getByRole("button", { name: "system:light" })).toBeTruthy();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("ignores invalid stored values and handles cross-tab preference changes", () => {
|
|
78
|
-
window.localStorage.setItem("theme", "not-a-theme");
|
|
79
|
-
render(<ThemeToggle />);
|
|
80
|
-
expect(document.documentElement.className).toBe("light");
|
|
81
|
-
act(() => window.dispatchEvent(new StorageEvent("storage", { key: "theme", newValue: "dark" })));
|
|
82
|
-
expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeTruthy();
|
|
83
|
-
act(() => window.dispatchEvent(new StorageEvent("storage", { key: "theme", newValue: null })));
|
|
84
|
-
expect(document.documentElement.className).toBe("light");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it.each(["light", "dark"])("honors explicit data-theme=%s and keeps it in sync", (mode) => {
|
|
88
|
-
document.documentElement.dataset.theme = mode;
|
|
89
|
-
dark = mode !== "dark";
|
|
90
|
-
render(<ThemeToggle />);
|
|
91
|
-
expect(document.documentElement.className).toBe(mode);
|
|
92
|
-
fireEvent.click(screen.getByRole("button"));
|
|
93
|
-
expect(document.documentElement.dataset.theme).toBe(mode === "light" ? "dark" : "light");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it.each(["light", "dark"])("honors an explicit .%s class without a saved preference", (mode) => {
|
|
97
|
-
document.documentElement.className = mode;
|
|
98
|
-
dark = mode !== "dark";
|
|
99
|
-
render(<ThemeToggle />);
|
|
100
|
-
expect(document.documentElement.className).toBe(mode);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it.each([
|
|
104
|
-
["aubergine", "aubergine-light"], ["arena", "arena-light"], ["super", "super-light"], ["tangle-dark", "tangle-light"],
|
|
105
|
-
["agents", "agents-light"],
|
|
106
|
-
])("keeps the %s named family in both directions", (name, lightName) => {
|
|
107
|
-
document.documentElement.dataset.theme = name;
|
|
108
|
-
render(<ThemeToggle />);
|
|
109
|
-
fireEvent.click(screen.getByRole("button", { name: "Switch to light mode" }));
|
|
110
|
-
expect(document.documentElement.dataset.theme).toBe(lightName);
|
|
111
|
-
fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
|
|
112
|
-
expect(document.documentElement.dataset.theme).toBe(name);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it.each(["intelligence", "custom-product"])("does not overwrite unpaired identity %s", (name) => {
|
|
116
|
-
document.documentElement.dataset.theme = name;
|
|
117
|
-
document.documentElement.className = "dark";
|
|
118
|
-
render(<ThemeToggle />);
|
|
119
|
-
fireEvent.click(screen.getByRole("button"));
|
|
120
|
-
expect(document.documentElement.dataset.theme).toBe(name);
|
|
121
|
-
expect(document.documentElement.className).toBe("light");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it.each(["vault", "dawn"])("preserves legacy %s identity while changing mode", (name) => {
|
|
125
|
-
document.documentElement.dataset.sandboxTheme = name;
|
|
126
|
-
render(<ThemeToggle />);
|
|
127
|
-
fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
|
|
128
|
-
expect(document.documentElement.dataset.sandboxTheme).toBe(name);
|
|
129
|
-
expect(document.documentElement.className).toBe("dark");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("uses the dark fallback without matchMedia and cleans subscriptions on unmount", () => {
|
|
133
|
-
const view = render(<ThemeToggle />);
|
|
134
|
-
expect(listeners.size).toBe(1);
|
|
135
|
-
view.unmount();
|
|
136
|
-
expect(listeners.size).toBe(0);
|
|
137
|
-
vi.stubGlobal("matchMedia", undefined);
|
|
138
|
-
render(<ThemeToggle />);
|
|
139
|
-
expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeTruthy();
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("server rendering and hydration", () => {
|
|
144
|
-
it("renders without window, document, matchMedia or storage", () => {
|
|
145
|
-
vi.stubGlobal("window", undefined);
|
|
146
|
-
vi.stubGlobal("document", undefined);
|
|
147
|
-
expect(renderToString(<ThemeToggle />)).toContain("Switch to light mode");
|
|
148
|
-
vi.unstubAllGlobals();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("hydrates the server snapshot then resolves saved light without a dark write", async () => {
|
|
152
|
-
const html = renderToString(<ThemeToggle />);
|
|
153
|
-
window.localStorage.setItem("theme", "light");
|
|
154
|
-
document.documentElement.className = "light";
|
|
155
|
-
const host = document.createElement("div");
|
|
156
|
-
host.innerHTML = html;
|
|
157
|
-
document.body.append(host);
|
|
158
|
-
const recoverable = vi.fn();
|
|
159
|
-
const toggle = vi.spyOn(document.documentElement.classList, "toggle");
|
|
160
|
-
let root: ReturnType<typeof hydrateRoot>;
|
|
161
|
-
await act(async () => { root = hydrateRoot(host, <ThemeToggle />, { onRecoverableError: recoverable }); });
|
|
162
|
-
expect(host.querySelector("button")?.getAttribute("aria-label")).toBe("Switch to dark mode");
|
|
163
|
-
expect(recoverable).not.toHaveBeenCalled();
|
|
164
|
-
expect(toggle).not.toHaveBeenCalledWith("dark", true);
|
|
165
|
-
await act(async () => root.unmount());
|
|
166
|
-
host.remove();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { act, cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { ToastProvider, useToast } from "./toast";
|
|
4
|
-
|
|
5
|
-
function ToastTrigger({ title, duration }: { title: string; duration?: number }) {
|
|
6
|
-
const { toast } = useToast();
|
|
7
|
-
|
|
8
|
-
return (
|
|
9
|
-
<button type="button" onClick={() => toast({ title, duration })}>
|
|
10
|
-
Show {title}
|
|
11
|
-
</button>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.useFakeTimers();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
cleanup();
|
|
21
|
-
vi.restoreAllMocks();
|
|
22
|
-
vi.useRealTimers();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("ToastProvider auto-dismiss", () => {
|
|
26
|
-
it("cancels every pending dismissal on unmount without updating after teardown", () => {
|
|
27
|
-
const view = render(
|
|
28
|
-
<ToastProvider>
|
|
29
|
-
<ToastTrigger title="First" duration={1000} />
|
|
30
|
-
<ToastTrigger title="Second" duration={2000} />
|
|
31
|
-
</ToastProvider>,
|
|
32
|
-
);
|
|
33
|
-
const scheduleTimeout = globalThis.setTimeout;
|
|
34
|
-
const timeoutFired = vi.fn();
|
|
35
|
-
// Observe the real dismissal callbacks without mocking the provider or React state.
|
|
36
|
-
vi.spyOn(globalThis, "setTimeout").mockImplementation((callback, delay, ...args) =>
|
|
37
|
-
scheduleTimeout(() => {
|
|
38
|
-
timeoutFired();
|
|
39
|
-
callback(...args);
|
|
40
|
-
}, delay),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
fireEvent.click(screen.getByRole("button", { name: "Show First" }));
|
|
44
|
-
fireEvent.click(screen.getByRole("button", { name: "Show Second" }));
|
|
45
|
-
expect(screen.getAllByRole("alert")).toHaveLength(2);
|
|
46
|
-
expect(vi.getTimerCount()).toBe(2);
|
|
47
|
-
|
|
48
|
-
view.unmount();
|
|
49
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
50
|
-
|
|
51
|
-
act(() => {
|
|
52
|
-
vi.advanceTimersByTime(5000);
|
|
53
|
-
});
|
|
54
|
-
// A cancelled callback cannot reach dismiss() or its state update after unmount.
|
|
55
|
-
expect(timeoutFired).not.toHaveBeenCalled();
|
|
56
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("keeps a toast until its duration elapses, then removes it", () => {
|
|
60
|
-
render(
|
|
61
|
-
<ToastProvider>
|
|
62
|
-
<ToastTrigger title="Saved" duration={1000} />
|
|
63
|
-
</ToastProvider>,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
fireEvent.click(screen.getByRole("button", { name: "Show Saved" }));
|
|
67
|
-
expect(vi.getTimerCount()).toBe(1);
|
|
68
|
-
|
|
69
|
-
act(() => {
|
|
70
|
-
vi.advanceTimersByTime(999);
|
|
71
|
-
});
|
|
72
|
-
expect(screen.getByRole("alert")).toHaveTextContent("Saved");
|
|
73
|
-
|
|
74
|
-
act(() => {
|
|
75
|
-
vi.advanceTimersByTime(1);
|
|
76
|
-
});
|
|
77
|
-
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
78
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("clears only the manually dismissed toast's timer", () => {
|
|
82
|
-
render(
|
|
83
|
-
<ToastProvider>
|
|
84
|
-
<ToastTrigger title="First" duration={1000} />
|
|
85
|
-
<ToastTrigger title="Second" duration={2000} />
|
|
86
|
-
</ToastProvider>,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
fireEvent.click(screen.getByRole("button", { name: "Show First" }));
|
|
90
|
-
fireEvent.click(screen.getByRole("button", { name: "Show Second" }));
|
|
91
|
-
expect(vi.getTimerCount()).toBe(2);
|
|
92
|
-
|
|
93
|
-
fireEvent.click(within(screen.getAllByRole("alert")[0]!).getByRole("button", {
|
|
94
|
-
name: "Dismiss notification",
|
|
95
|
-
}));
|
|
96
|
-
expect(screen.queryByText("First")).not.toBeInTheDocument();
|
|
97
|
-
expect(screen.getByRole("alert")).toHaveTextContent("Second");
|
|
98
|
-
expect(vi.getTimerCount()).toBe(1);
|
|
99
|
-
|
|
100
|
-
act(() => {
|
|
101
|
-
vi.advanceTimersByTime(1000);
|
|
102
|
-
});
|
|
103
|
-
expect(screen.getByRole("alert")).toHaveTextContent("Second");
|
|
104
|
-
|
|
105
|
-
act(() => {
|
|
106
|
-
vi.advanceTimersByTime(1000);
|
|
107
|
-
});
|
|
108
|
-
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
109
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("schedules no timer when duration is zero", () => {
|
|
113
|
-
render(
|
|
114
|
-
<ToastProvider>
|
|
115
|
-
<ToastTrigger title="Persistent" duration={0} />
|
|
116
|
-
</ToastProvider>,
|
|
117
|
-
);
|
|
118
|
-
const scheduleTimeout = vi.spyOn(globalThis, "setTimeout");
|
|
119
|
-
|
|
120
|
-
fireEvent.click(screen.getByRole("button", { name: "Show Persistent" }));
|
|
121
|
-
expect(scheduleTimeout).not.toHaveBeenCalled();
|
|
122
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
123
|
-
|
|
124
|
-
act(() => {
|
|
125
|
-
vi.advanceTimersByTime(10000);
|
|
126
|
-
});
|
|
127
|
-
expect(screen.getByRole("alert")).toHaveTextContent("Persistent");
|
|
128
|
-
expect(vi.getTimerCount()).toBe(0);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen, waitFor } 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 { Wizard } from "./wizard";
|
|
6
|
-
|
|
7
|
-
function Flow(props: Partial<React.ComponentProps<typeof Wizard>> = {}) {
|
|
8
|
-
return (
|
|
9
|
-
<Wizard {...props}>
|
|
10
|
-
<Wizard.Step title="Number" nextLabel="Continue">pick a number</Wizard.Step>
|
|
11
|
-
<Wizard.Step title="Verify" nextLabel="I replied">verify panel</Wizard.Step>
|
|
12
|
-
<Wizard.Step title="Done" nextLabel="Finish">done panel</Wizard.Step>
|
|
13
|
-
</Wizard>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe("Wizard", () => {
|
|
18
|
-
it("renders the first step and the rail, marks the active step, hides future panels", () => {
|
|
19
|
-
render(<Flow />);
|
|
20
|
-
expect(screen.getByText("pick a number")).toBeVisible();
|
|
21
|
-
expect(screen.queryByText("verify panel")).toBeNull();
|
|
22
|
-
expect(screen.getByRole("list", { name: "Steps" })).toBeVisible();
|
|
23
|
-
expect(screen.getAllByText("Number").length).toBeGreaterThan(0);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("advances on the primary action and mirrors onStepChange", async () => {
|
|
27
|
-
const onStepChange = vi.fn();
|
|
28
|
-
render(<Flow onStepChange={onStepChange} />);
|
|
29
|
-
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
30
|
-
expect(screen.getByText("verify panel")).toBeVisible();
|
|
31
|
-
expect(onStepChange).toHaveBeenCalledWith(1);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("back returns to the previous step; the first step has no back", async () => {
|
|
35
|
-
render(<Flow />);
|
|
36
|
-
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
37
|
-
await userEvent.click(screen.getByRole("button", { name: "Back" }));
|
|
38
|
-
expect(screen.getByText("pick a number")).toBeVisible();
|
|
39
|
-
expect(screen.queryByRole("button", { name: "Back" })).toBeNull();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("future steps are not clickable; past steps are", async () => {
|
|
43
|
-
render(<Flow />);
|
|
44
|
-
fireEvent.click(screen.getByText("Verify")); // future: a span, not a button
|
|
45
|
-
expect(screen.getByText("pick a number")).toBeVisible();
|
|
46
|
-
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
47
|
-
await userEvent.click(screen.getAllByRole("button", { name: /Number/ })[0]!); // past: clickable
|
|
48
|
-
expect(screen.getByText("pick a number")).toBeVisible();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("an async onNext advances only on resolve; returning false keeps the step", async () => {
|
|
52
|
-
let resolveCheck: (value: unknown) => void = () => {};
|
|
53
|
-
const onNext = vi.fn(() => new Promise(resolve => { resolveCheck = resolve; }));
|
|
54
|
-
render(
|
|
55
|
-
<Wizard>
|
|
56
|
-
<Wizard.Step title="Check" nextLabel="Check" onNext={onNext}>checking</Wizard.Step>
|
|
57
|
-
<Wizard.Step title="Next">next panel</Wizard.Step>
|
|
58
|
-
</Wizard>,
|
|
59
|
-
);
|
|
60
|
-
await userEvent.click(screen.getByRole("button", { name: "Check" }));
|
|
61
|
-
expect(screen.getByText("checking")).toBeVisible(); // still here while pending
|
|
62
|
-
resolveCheck(true);
|
|
63
|
-
await waitFor(() => expect(screen.getByText("next panel")).toBeVisible());
|
|
64
|
-
expect(onNext).toHaveBeenCalledTimes(1);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("a rejected onNext stays on the step without crashing", async () => {
|
|
68
|
-
const onNext = vi.fn(() => Promise.reject(new Error("no")));
|
|
69
|
-
render(
|
|
70
|
-
<Wizard>
|
|
71
|
-
<Wizard.Step title="Check" nextLabel="Check" onNext={onNext}>checking</Wizard.Step>
|
|
72
|
-
<Wizard.Step title="Next">next panel</Wizard.Step>
|
|
73
|
-
</Wizard>,
|
|
74
|
-
);
|
|
75
|
-
await userEvent.click(screen.getByRole("button", { name: "Check" }));
|
|
76
|
-
await waitFor(() => expect(screen.getByRole("button", { name: "Check" })).toBeEnabled());
|
|
77
|
-
expect(screen.getByText("checking")).toBeVisible();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("the last step fires onFinished instead of advancing", async () => {
|
|
81
|
-
const onFinished = vi.fn();
|
|
82
|
-
render(<Flow onFinished={onFinished} defaultStep={2} />);
|
|
83
|
-
await userEvent.click(screen.getByRole("button", { name: "Finish" }));
|
|
84
|
-
expect(onFinished).toHaveBeenCalledTimes(1);
|
|
85
|
-
expect(screen.getByText("done panel")).toBeVisible();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("step can drive externally (controlled) and omit the default footer action", () => {
|
|
89
|
-
render(
|
|
90
|
-
<Wizard step={1}>
|
|
91
|
-
<Wizard.Step title="One">one</Wizard.Step>
|
|
92
|
-
<Wizard.Step title="Two"><button type="button">custom</button></Wizard.Step>
|
|
93
|
-
</Wizard>,
|
|
94
|
-
);
|
|
95
|
-
expect(screen.getAllByText("Two").length).toBeGreaterThan(0);
|
|
96
|
-
expect(screen.getByText("custom")).toBeVisible();
|
|
97
|
-
expect(screen.queryByRole("button", { name: "Continue" })).toBeNull();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("throws when a step is used outside a wizard", () => {
|
|
101
|
-
// Silence the expected console.error from React's error boundary path.
|
|
102
|
-
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
103
|
-
expect(() => render(<Wizard.Step title="Alone">x</Wizard.Step>)).toThrow(/must be used inside/);
|
|
104
|
-
spy.mockRestore();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { ExpandableContent } from "./expandable-content";
|
|
5
|
-
|
|
6
|
-
describe("ExpandableContent", () => {
|
|
7
|
-
let scrollHeightSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
8
|
-
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
scrollHeightSpy?.mockRestore();
|
|
11
|
-
scrollHeightSpy = undefined;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("renders no toggle when content fits within the cap", () => {
|
|
15
|
-
render(
|
|
16
|
-
<ExpandableContent collapsedMaxPx={180}>
|
|
17
|
-
<div>short</div>
|
|
18
|
-
</ExpandableContent>,
|
|
19
|
-
);
|
|
20
|
-
expect(
|
|
21
|
-
screen.queryByRole("button", { name: /show more/i }),
|
|
22
|
-
).not.toBeInTheDocument();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("when content overflows", () => {
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
scrollHeightSpy = vi
|
|
28
|
-
.spyOn(HTMLElement.prototype, "scrollHeight", "get")
|
|
29
|
-
.mockReturnValue(999);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("clamps with a Show more toggle that expands and collapses", async () => {
|
|
33
|
-
const user = userEvent.setup();
|
|
34
|
-
render(
|
|
35
|
-
<ExpandableContent collapsedMaxPx={180}>
|
|
36
|
-
<div>tall content</div>
|
|
37
|
-
</ExpandableContent>,
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const showMore = screen.getByRole("button", { name: /show more/i });
|
|
41
|
-
await user.click(showMore);
|
|
42
|
-
expect(
|
|
43
|
-
screen.getByRole("button", { name: /show less/i }),
|
|
44
|
-
).toBeInTheDocument();
|
|
45
|
-
|
|
46
|
-
await user.click(screen.getByRole("button", { name: /show less/i }));
|
|
47
|
-
expect(
|
|
48
|
-
screen.getByRole("button", { name: /show more/i }),
|
|
49
|
-
).toBeInTheDocument();
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
package/src/utils/format.test.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatBytes, formatUptime, plainTextFromMarkdown } from "./format";
|
|
3
|
-
|
|
4
|
-
describe("formatUptime", () => {
|
|
5
|
-
it("renders seconds for short durations", () => {
|
|
6
|
-
expect(formatUptime(0)).toBe("0s");
|
|
7
|
-
expect(formatUptime(7_500)).toBe("7s");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("renders minutes and seconds under an hour", () => {
|
|
11
|
-
expect(formatUptime(90_000)).toBe("1m 30s");
|
|
12
|
-
expect(formatUptime(59 * 60_000 + 12_000)).toBe("59m 12s");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("renders hours and minutes under a day", () => {
|
|
16
|
-
expect(formatUptime(3_600_000)).toBe("1h 0m");
|
|
17
|
-
expect(formatUptime(5 * 3_600_000 + 30 * 60_000)).toBe("5h 30m");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("renders days and hours for multi-day durations", () => {
|
|
21
|
-
expect(formatUptime(86_400_000)).toBe("1d 0h");
|
|
22
|
-
expect(formatUptime(86_400_000 * 3 + 3_600_000 * 4)).toBe("3d 4h");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("handles invalid input", () => {
|
|
26
|
-
expect(formatUptime(Number.NaN)).toBe("—");
|
|
27
|
-
expect(formatUptime(-1)).toBe("—");
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("formatBytes", () => {
|
|
32
|
-
it("renders bytes, KB, MB, GB progressively", () => {
|
|
33
|
-
expect(formatBytes(512)).toBe("512 B");
|
|
34
|
-
expect(formatBytes(2_048)).toBe("2.0 KB");
|
|
35
|
-
expect(formatBytes(5 * 1024 * 1024)).toBe("5.0 MB");
|
|
36
|
-
expect(formatBytes(2.5 * 1024 * 1024 * 1024)).toBe("2.50 GB");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("handles invalid input", () => {
|
|
40
|
-
expect(formatBytes(Number.NaN)).toBe("—");
|
|
41
|
-
expect(formatBytes(-1)).toBe("—");
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("plainTextFromMarkdown", () => {
|
|
46
|
-
it("drops emphasis, headings, list markers, code marks and link syntax", () => {
|
|
47
|
-
expect(plainTextFromMarkdown("**Ensuring workspace configuration** I need to check")).toBe("Ensuring workspace configuration I need to check");
|
|
48
|
-
expect(plainTextFromMarkdown("## Plan\n- item one\n- *soft* point")).toBe("Plan\nitem one\nsoft point");
|
|
49
|
-
expect(plainTextFromMarkdown("see [docs](https://example.com) and _this_")).toBe("see docs and this");
|
|
50
|
-
expect(plainTextFromMarkdown("**bold** and __also__ ~~gone~~")).toBe("bold and also gone");
|
|
51
|
-
expect(plainTextFromMarkdown("1. First\n2) Second")).toBe("First\nSecond");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("keeps characters that are not markdown marks", () => {
|
|
55
|
-
expect(plainTextFromMarkdown("read `src/a_b.ts` and snake_case_name")).toBe("read src/a_b.ts and snake_case_name");
|
|
56
|
-
expect(plainTextFromMarkdown("2 * 3 * 4 = 24")).toBe("2 * 3 * 4 = 24");
|
|
57
|
-
});
|
|
58
|
-
});
|