@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.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -0
- package/dist/chat/agent-timeline.d.ts +1 -1
- package/dist/chat/speaker.d.ts +2 -2
- package/dist/chat.d.ts +2 -2
- package/dist/index.d.ts +4 -4
- package/package.json +31 -1
- package/src/editor/source-markdown-editor.test.tsx +7 -7
- 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
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
-
import { FileArtifactPane } from "./file-artifact-pane";
|
|
4
|
-
import { CSV_PREVIEW_ROW_LIMIT, parseCsv } from "./file-preview";
|
|
5
|
-
import { MissingPdfjsError, loadPdfjs } from "./pdf-loader";
|
|
6
|
-
|
|
7
|
-
vi.mock("./pdf-loader", async (importOriginal) => ({
|
|
8
|
-
...(await importOriginal<typeof import("./pdf-loader")>()),
|
|
9
|
-
loadPdfjs: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
const BLOB_URL = "blob:https://app.tangle.tools/1f2e3d4c";
|
|
13
|
-
|
|
14
|
-
function csvWithRows(rowCount: number): string {
|
|
15
|
-
const rows = Array.from({ length: rowCount }, (_, i) => `"Ada, Countess ${i}",x,${i}`);
|
|
16
|
-
return ["name,note,n", ...rows].join("\n");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("FileArtifactPane previews", () => {
|
|
20
|
-
it("renders an image from blobUrl and toggles natural size on click", () => {
|
|
21
|
-
const { container } = render(
|
|
22
|
-
<FileArtifactPane filename="chart.png" blobUrl={BLOB_URL} mimeType="image/png" />,
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const img = container.querySelector("img");
|
|
26
|
-
expect(img).not.toBeNull();
|
|
27
|
-
expect(img).toHaveAttribute("src", BLOB_URL);
|
|
28
|
-
expect(img).toHaveAttribute("alt", "chart.png");
|
|
29
|
-
expect(img).toHaveClass("object-contain");
|
|
30
|
-
|
|
31
|
-
const toggle = screen.getByRole("button", { name: "Show image at natural size" });
|
|
32
|
-
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
|
33
|
-
fireEvent.click(toggle);
|
|
34
|
-
expect(screen.getByRole("button", { name: "Fit image to pane" })).toHaveAttribute(
|
|
35
|
-
"aria-pressed",
|
|
36
|
-
"true",
|
|
37
|
-
);
|
|
38
|
-
expect(container.querySelector("img")).toHaveClass("max-w-none");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("renders a PDF in PdfViewer, and in an <object> of the blobUrl without pdfjs-dist", async () => {
|
|
42
|
-
vi.mocked(loadPdfjs).mockRejectedValue(new MissingPdfjsError());
|
|
43
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
44
|
-
const { container } = render(
|
|
45
|
-
<FileArtifactPane filename="report.pdf" blobUrl={BLOB_URL} mimeType="application/pdf" />,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
expect(screen.getByRole("status")).toHaveTextContent("Loading report.pdf");
|
|
49
|
-
expect(await screen.findByText(/does not include the page viewer/)).toBeInTheDocument();
|
|
50
|
-
const viewer = container.querySelector("object");
|
|
51
|
-
expect(viewer).toHaveAttribute("data", BLOB_URL);
|
|
52
|
-
expect(viewer).toHaveAttribute("type", "application/pdf");
|
|
53
|
-
expect(viewer).toHaveAttribute("title", "report.pdf");
|
|
54
|
-
consoleError.mockRestore();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("renders video and audio with native controls", () => {
|
|
58
|
-
const video = render(<FileArtifactPane filename="demo.mp4" blobUrl={BLOB_URL} />);
|
|
59
|
-
const videoElement = video.container.querySelector("video");
|
|
60
|
-
expect(videoElement).not.toBeNull();
|
|
61
|
-
expect(videoElement).toHaveAttribute("src", BLOB_URL);
|
|
62
|
-
expect(videoElement).toHaveAttribute("controls");
|
|
63
|
-
video.unmount();
|
|
64
|
-
|
|
65
|
-
const audio = render(<FileArtifactPane filename="voice.m4a" blobUrl={BLOB_URL} />);
|
|
66
|
-
const audioElement = audio.container.querySelector("audio");
|
|
67
|
-
expect(audioElement).not.toBeNull();
|
|
68
|
-
expect(audioElement).toHaveAttribute("src", BLOB_URL);
|
|
69
|
-
expect(audioElement).toHaveAttribute("controls");
|
|
70
|
-
expect(audio.getAllByText("voice.m4a").length).toBeGreaterThan(1); // header + player label
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("parses quoted commas in CSV and caps the table at the row limit", () => {
|
|
74
|
-
const total = CSV_PREVIEW_ROW_LIMIT + 1;
|
|
75
|
-
const { container } = render(
|
|
76
|
-
<FileArtifactPane filename="people.csv" content={csvWithRows(total)} />,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const headers = Array.from(container.querySelectorAll("thead th")).map((th) => th.textContent);
|
|
80
|
-
expect(headers).toEqual(["name", "note", "n"]);
|
|
81
|
-
expect(container.querySelectorAll("thead th")[0]).toHaveClass("sticky");
|
|
82
|
-
|
|
83
|
-
const bodyRows = container.querySelectorAll("tbody tr");
|
|
84
|
-
expect(bodyRows).toHaveLength(CSV_PREVIEW_ROW_LIMIT);
|
|
85
|
-
expect(bodyRows[0].querySelectorAll("td")[0]).toHaveTextContent("Ada, Countess 0");
|
|
86
|
-
expect(bodyRows[0].querySelectorAll("td")).toHaveLength(3);
|
|
87
|
-
|
|
88
|
-
expect(
|
|
89
|
-
screen.getByText(`Showing ${CSV_PREVIEW_ROW_LIMIT} of ${total} rows · 3 columns`),
|
|
90
|
-
).toBeInTheDocument();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("reports the row count when the CSV is under the cap", () => {
|
|
94
|
-
render(<FileArtifactPane filename="people.csv" content={csvWithRows(3)} />);
|
|
95
|
-
expect(screen.getByText("3 rows · 3 columns")).toBeInTheDocument();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("renders a spreadsheet as a download card and calls onDownload", () => {
|
|
99
|
-
const onDownload = vi.fn();
|
|
100
|
-
render(
|
|
101
|
-
<FileArtifactPane filename="book.xlsx" size={1536} onDownload={onDownload} />,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
expect(screen.getByText("1.5 KB")).toBeInTheDocument();
|
|
105
|
-
expect(
|
|
106
|
-
screen.getByText("Download to open this workbook in a spreadsheet app."),
|
|
107
|
-
).toBeInTheDocument();
|
|
108
|
-
|
|
109
|
-
fireEvent.click(screen.getByRole("button", { name: "Download" }));
|
|
110
|
-
expect(onDownload).toHaveBeenCalledTimes(1);
|
|
111
|
-
|
|
112
|
-
// The header keeps its own download control.
|
|
113
|
-
fireEvent.click(screen.getByRole("button", { name: "Download book.xlsx" }));
|
|
114
|
-
expect(onDownload).toHaveBeenCalledTimes(2);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("renders markdown through the package Markdown renderer", () => {
|
|
118
|
-
const { container } = render(
|
|
119
|
-
<FileArtifactPane filename="README.md" content={"# Title\n\nBody with **bold**."} />,
|
|
120
|
-
);
|
|
121
|
-
expect(container.querySelector(".tangle-prose")).not.toBeNull();
|
|
122
|
-
expect(container.querySelector("h1")).toHaveTextContent("Title");
|
|
123
|
-
expect(container.querySelector("strong")).toHaveTextContent("bold");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("forwards Markdown URL transforms to file and rich editor previews", async () => {
|
|
127
|
-
const urlTransform = vi.fn((url: string, key: string) => (
|
|
128
|
-
key === "href" && url === "sibling.md"
|
|
129
|
-
? "/app/ws-1/vault?file=docs%2Fsibling.md"
|
|
130
|
-
: url
|
|
131
|
-
));
|
|
132
|
-
const readOnly = render(
|
|
133
|
-
<FileArtifactPane
|
|
134
|
-
filename="README.md"
|
|
135
|
-
content={"[Sibling](sibling.md)"}
|
|
136
|
-
urlTransform={urlTransform}
|
|
137
|
-
/>,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(screen.getByRole("link", { name: "Sibling" })).toHaveAttribute(
|
|
141
|
-
"href",
|
|
142
|
-
"/app/ws-1/vault?file=docs%2Fsibling.md",
|
|
143
|
-
);
|
|
144
|
-
readOnly.unmount();
|
|
145
|
-
|
|
146
|
-
render(
|
|
147
|
-
<FileArtifactPane
|
|
148
|
-
path="docs/README.md"
|
|
149
|
-
filename="README.md"
|
|
150
|
-
content={"[Sibling](sibling.md)"}
|
|
151
|
-
urlTransform={urlTransform}
|
|
152
|
-
editor={{ enabled: true, defaultMode: "preview" }}
|
|
153
|
-
/>,
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
// The rich editor lazy-loads its editor packages; under a loaded host that takes
|
|
157
|
-
// longer than findBy's 1s default.
|
|
158
|
-
expect(await screen.findByRole("link", { name: "Sibling" }, { timeout: 10_000 })).toHaveAttribute(
|
|
159
|
-
"href",
|
|
160
|
-
"/app/ws-1/vault?file=docs%2Fsibling.md",
|
|
161
|
-
);
|
|
162
|
-
expect(urlTransform).toHaveBeenCalledWith(
|
|
163
|
-
"sibling.md",
|
|
164
|
-
"href",
|
|
165
|
-
expect.objectContaining({ tagName: "a" }),
|
|
166
|
-
);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("renders a binary file with no download link as one muted sentence", () => {
|
|
170
|
-
const onDownload = vi.fn();
|
|
171
|
-
const withDownload = render(
|
|
172
|
-
<FileArtifactPane filename="weights.bin" onDownload={onDownload} />,
|
|
173
|
-
);
|
|
174
|
-
expect(
|
|
175
|
-
withDownload.getByText("This file needs a download link to preview."),
|
|
176
|
-
).toBeInTheDocument();
|
|
177
|
-
fireEvent.click(withDownload.getByRole("button", { name: "Download" }));
|
|
178
|
-
expect(onDownload).toHaveBeenCalledTimes(1);
|
|
179
|
-
withDownload.unmount();
|
|
180
|
-
|
|
181
|
-
render(<FileArtifactPane filename="weights.bin" />);
|
|
182
|
-
expect(screen.getByText("This file needs a download link to preview.")).toBeInTheDocument();
|
|
183
|
-
expect(screen.queryByRole("button", { name: "Download" })).toBeNull();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("renders the same sentence for an image, PDF, or video without a blobUrl", () => {
|
|
187
|
-
for (const filename of ["chart.png", "report.pdf", "demo.mp4"]) {
|
|
188
|
-
const { getByText, unmount } = render(<FileArtifactPane filename={filename} />);
|
|
189
|
-
expect(getByText("This file needs a download link to preview.")).toBeInTheDocument();
|
|
190
|
-
unmount();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("renders an unknown file's text content as text", () => {
|
|
195
|
-
const { container } = render(
|
|
196
|
-
<FileArtifactPane filename="Makefile.custom" content={"build:\n\tgo build"} />,
|
|
197
|
-
);
|
|
198
|
-
expect(container.querySelector("pre")).toHaveTextContent("go build");
|
|
199
|
-
expect(container.querySelector("code")).toBeNull();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("keeps code on the syntax highlighter and text on a plain pre", () => {
|
|
203
|
-
const code = render(
|
|
204
|
-
<FileArtifactPane filename="server.ts" content={"const x = 1;\nexport { x };"} />,
|
|
205
|
-
);
|
|
206
|
-
expect(code.container.querySelector("code")).not.toBeNull();
|
|
207
|
-
expect(code.container.querySelector("table")).toBeNull();
|
|
208
|
-
expect(code.getByText(/ts · 2 lines/)).toBeInTheDocument();
|
|
209
|
-
code.unmount();
|
|
210
|
-
|
|
211
|
-
const text = render(<FileArtifactPane filename="notes.txt" content="just words" />);
|
|
212
|
-
expect(text.container.querySelector("pre")).toHaveTextContent("just words");
|
|
213
|
-
expect(text.container.querySelector("code")).toBeNull();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("renders SVG source as code when the host read it as text", () => {
|
|
217
|
-
const { container } = render(
|
|
218
|
-
<FileArtifactPane filename="icon.svg" content={'<svg xmlns="http://www.w3.org/2000/svg"/>'} />,
|
|
219
|
-
);
|
|
220
|
-
expect(container.querySelector("code")).not.toBeNull();
|
|
221
|
-
expect(container.querySelector("img")).toBeNull();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("parseCsv", () => {
|
|
226
|
-
it("keeps commas, line breaks, and doubled quotes inside quoted cells", () => {
|
|
227
|
-
expect(parseCsv('a,b\n"x, y","line 1\nline 2"\n"say ""hi""",z')).toEqual([
|
|
228
|
-
["a", "b"],
|
|
229
|
-
["x, y", "line 1\nline 2"],
|
|
230
|
-
['say "hi"', "z"],
|
|
231
|
-
]);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("trims unquoted cells and accepts CRLF, blank lines, and a trailing line break", () => {
|
|
235
|
-
expect(parseCsv("a, b\r\n\r\n1 ,2\r\n")).toEqual([
|
|
236
|
-
["a", "b"],
|
|
237
|
-
["1", "2"],
|
|
238
|
-
]);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("returns no rows for empty input", () => {
|
|
242
|
-
expect(parseCsv("")).toEqual([]);
|
|
243
|
-
expect(parseCsv("\n\n")).toEqual([]);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
describe("FileArtifactPane controlled drafts", () => {
|
|
248
|
-
it("retains a host draft and saves it against the persisted baseline", async () => {
|
|
249
|
-
const onSave = vi.fn();
|
|
250
|
-
const draft = "# Unsaved source draft";
|
|
251
|
-
const props = {
|
|
252
|
-
filename: "notes.md",
|
|
253
|
-
content: draft,
|
|
254
|
-
editor: { enabled: true, savedContent: "# Saved notes", onSave },
|
|
255
|
-
};
|
|
256
|
-
const view = render(<FileArtifactPane {...props} />);
|
|
257
|
-
const save = await screen.findByRole("button", { name: "Save changes" }, { timeout: 10_000 });
|
|
258
|
-
expect(screen.getByRole("heading", { name: "Unsaved source draft" })).toBeVisible();
|
|
259
|
-
expect(screen.getByText("Unsaved changes")).toBeVisible();
|
|
260
|
-
expect(save).toBeEnabled();
|
|
261
|
-
fireEvent.click(save);
|
|
262
|
-
expect(onSave).toHaveBeenCalledWith(draft);
|
|
263
|
-
|
|
264
|
-
view.rerender(<FileArtifactPane {...props} editor={{ ...props.editor, savedContent: draft }} />);
|
|
265
|
-
// One save state: "Saved", with no Save button left to press.
|
|
266
|
-
expect(screen.getByRole("status")).toHaveTextContent("Saved");
|
|
267
|
-
expect(screen.queryByRole("button", { name: "Save changes" })).toBeNull();
|
|
268
|
-
expect(screen.queryByText("Local draft")).toBeNull();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("follows the host's dirty flag over its own comparison, and saves on Ctrl+S", async () => {
|
|
272
|
-
const onSave = vi.fn();
|
|
273
|
-
const content = "# Same text the host saved";
|
|
274
|
-
const props = { filename: "notes.md", content, editor: { enabled: true, dirty: true, onSave } };
|
|
275
|
-
const view = render(<FileArtifactPane {...props} />);
|
|
276
|
-
const save = await screen.findByRole("button", { name: "Save changes" }, { timeout: 10_000 });
|
|
277
|
-
expect(save).toBeEnabled();
|
|
278
|
-
expect(screen.getByRole("status")).toHaveTextContent("Unsaved changes");
|
|
279
|
-
|
|
280
|
-
fireEvent.keyDown(save, { key: "s", ctrlKey: true });
|
|
281
|
-
expect(onSave).toHaveBeenCalledWith(content);
|
|
282
|
-
|
|
283
|
-
view.rerender(<FileArtifactPane {...props} editor={{ ...props.editor, dirty: false }} />);
|
|
284
|
-
expect(screen.getByRole("status")).toHaveTextContent("Saved");
|
|
285
|
-
fireEvent.keyDown(screen.getByRole("status"), { key: "s", metaKey: true });
|
|
286
|
-
expect(onSave).toHaveBeenCalledTimes(1);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("shows Saving while the host saves", async () => {
|
|
290
|
-
render(
|
|
291
|
-
<FileArtifactPane
|
|
292
|
-
filename="notes.md"
|
|
293
|
-
content="# Draft"
|
|
294
|
-
editor={{ enabled: true, dirty: true, saving: true, onSave: vi.fn() }}
|
|
295
|
-
/>,
|
|
296
|
-
);
|
|
297
|
-
const save = await screen.findByRole("button", { name: "Save changes" }, { timeout: 10_000 });
|
|
298
|
-
expect(save).toBeDisabled();
|
|
299
|
-
expect(screen.getByRole("status")).toHaveTextContent("Saving…");
|
|
300
|
-
});
|
|
301
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { FileCard } from "./file-card";
|
|
4
|
-
|
|
5
|
-
const UPLOADED = new Date("2026-10-06T15:30:00Z");
|
|
6
|
-
|
|
7
|
-
describe("FileCard", () => {
|
|
8
|
-
it("names the file with its size, date and uploader", () => {
|
|
9
|
-
const { container } = render(
|
|
10
|
-
<FileCard
|
|
11
|
-
filename="Engagement letter.pdf"
|
|
12
|
-
size={2_516_582}
|
|
13
|
-
date={UPLOADED}
|
|
14
|
-
uploader={{ name: "Dana Ruiz", role: "Client" }}
|
|
15
|
-
/>,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
expect(screen.getByText("Engagement letter.pdf")).toBeInTheDocument();
|
|
19
|
-
expect(screen.getByText("2.4 MB")).toBeInTheDocument();
|
|
20
|
-
expect(container.querySelector("time")).toHaveAttribute("datetime", UPLOADED.toISOString());
|
|
21
|
-
expect(screen.getByText("Uploaded by Dana Ruiz (Client)")).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("leaves out what it does not know", () => {
|
|
25
|
-
const { container } = render(<FileCard filename="notes.txt" date="not a date" />);
|
|
26
|
-
expect(container.querySelector("time")).toBeNull();
|
|
27
|
-
expect(screen.queryByText(/Uploaded by/)).toBeNull();
|
|
28
|
-
expect(screen.queryByRole("button")).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("takes its icon tone from the file kind, with the MIME type outranking the name", () => {
|
|
32
|
-
const pdf = render(<FileCard filename="brief.pdf" />);
|
|
33
|
-
expect(pdf.container.querySelector("[aria-hidden='true']")?.className).toContain(
|
|
34
|
-
"--tone-orange-bg",
|
|
35
|
-
);
|
|
36
|
-
pdf.unmount();
|
|
37
|
-
|
|
38
|
-
const sheet = render(<FileCard filename="export" mimeType="text/csv" />);
|
|
39
|
-
expect(sheet.container.querySelector("[aria-hidden='true']")?.className).toContain(
|
|
40
|
-
"--tone-lime-bg",
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("offers Download and the host's own actions", () => {
|
|
45
|
-
const onDownload = vi.fn();
|
|
46
|
-
render(
|
|
47
|
-
<FileCard
|
|
48
|
-
filename="book.xlsx"
|
|
49
|
-
description="Download to open this workbook in a spreadsheet app."
|
|
50
|
-
onDownload={onDownload}
|
|
51
|
-
actions={<button type="button">Open</button>}
|
|
52
|
-
/>,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
expect(
|
|
56
|
-
screen.getByText("Download to open this workbook in a spreadsheet app."),
|
|
57
|
-
).toBeInTheDocument();
|
|
58
|
-
fireEvent.click(screen.getByRole("button", { name: "Download" }));
|
|
59
|
-
expect(onDownload).toHaveBeenCalledTimes(1);
|
|
60
|
-
expect(screen.getByRole("button", { name: "Open" })).toBeInTheDocument();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { FileList, type FileListItem } from "./file-list";
|
|
4
|
-
|
|
5
|
-
interface CaseDocument extends FileListItem {
|
|
6
|
-
matterId: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const FILES: CaseDocument[] = [
|
|
10
|
-
{
|
|
11
|
-
id: "lease",
|
|
12
|
-
matterId: "m-1",
|
|
13
|
-
filename: "Commercial lease.pdf",
|
|
14
|
-
size: 482_000,
|
|
15
|
-
date: "2026-10-01T12:00:00Z",
|
|
16
|
-
uploader: { name: "Dana Ruiz", role: "Client" },
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: "photos",
|
|
20
|
-
matterId: "m-1",
|
|
21
|
-
filename: "Water damage.jpg",
|
|
22
|
-
mimeType: "image/jpeg",
|
|
23
|
-
size: 3_100_000,
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
describe("FileList", () => {
|
|
28
|
-
it("shows each file's kind, size and uploader", () => {
|
|
29
|
-
render(<FileList files={FILES} aria-label="Case documents" />);
|
|
30
|
-
const rows = within(screen.getByRole("list", { name: "Case documents" })).getAllByRole(
|
|
31
|
-
"listitem",
|
|
32
|
-
);
|
|
33
|
-
expect(rows).toHaveLength(2);
|
|
34
|
-
expect(rows[0]).toHaveTextContent("Commercial lease.pdf");
|
|
35
|
-
// A dot between facts on screen, a comma for a screen reader.
|
|
36
|
-
expect(rows[0]).toHaveTextContent(/PDF · , 471 KB · , .+ · , Dana Ruiz \(Client\)/);
|
|
37
|
-
expect(rows[1]).toHaveTextContent("Image · , 3.0 MB");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("opens a file from its row and hands back the host's own record", () => {
|
|
41
|
-
const onOpen = vi.fn();
|
|
42
|
-
render(<FileList files={FILES} onOpen={onOpen} activeId="photos" />);
|
|
43
|
-
|
|
44
|
-
fireEvent.click(screen.getByRole("button", { name: /Commercial lease\.pdf/ }));
|
|
45
|
-
expect(onOpen).toHaveBeenCalledWith(FILES[0]);
|
|
46
|
-
expect(onOpen.mock.calls[0][0].matterId).toBe("m-1");
|
|
47
|
-
|
|
48
|
-
expect(screen.getByRole("button", { name: /Water damage\.jpg/ })).toHaveAttribute(
|
|
49
|
-
"aria-current",
|
|
50
|
-
"true",
|
|
51
|
-
);
|
|
52
|
-
expect(screen.getByRole("button", { name: /Commercial lease\.pdf/ })).not.toHaveAttribute(
|
|
53
|
-
"aria-current",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("downloads and deletes through per-file buttons", () => {
|
|
58
|
-
const onDownload = vi.fn();
|
|
59
|
-
const onDelete = vi.fn();
|
|
60
|
-
render(<FileList files={FILES} onDownload={onDownload} onDelete={onDelete} />);
|
|
61
|
-
|
|
62
|
-
fireEvent.click(screen.getByRole("button", { name: "Download Water damage.jpg" }));
|
|
63
|
-
expect(onDownload).toHaveBeenCalledWith(FILES[1]);
|
|
64
|
-
fireEvent.click(screen.getByRole("button", { name: "Delete Commercial lease.pdf" }));
|
|
65
|
-
expect(onDelete).toHaveBeenCalledWith(FILES[0]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("renders no controls the host did not ask for", () => {
|
|
69
|
-
render(<FileList files={FILES} />);
|
|
70
|
-
expect(screen.queryByRole("button")).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("shows the empty message in place of the list", () => {
|
|
74
|
-
const { rerender } = render(<FileList files={[]} />);
|
|
75
|
-
expect(screen.queryByRole("list")).toBeNull();
|
|
76
|
-
expect(screen.getByText("No files yet.")).toBeInTheDocument();
|
|
77
|
-
|
|
78
|
-
rerender(<FileList files={[]} empty="Upload the lease to start." />);
|
|
79
|
-
expect(screen.getByText("Upload the lease to start.")).toBeInTheDocument();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const PDFJS = "pdfjs-dist/legacy/build/pdf.mjs";
|
|
4
|
-
const WORKER = "pdfjs-dist/legacy/build/pdf.worker.min.mjs";
|
|
5
|
-
|
|
6
|
-
/** What the next import of pdf.js yields: a namespace, or a thrown error. */
|
|
7
|
-
let pdfjsImport: () => Record<string, unknown>;
|
|
8
|
-
let workerImports = 0;
|
|
9
|
-
|
|
10
|
-
function installedPdfjs() {
|
|
11
|
-
return { getDocument: () => ({}), GlobalWorkerOptions: { workerSrc: "", workerPort: null } };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Loads the module under test with pdf.js replaced. The loader reads it through
|
|
16
|
-
* a dynamic import, so the substitution happens before the graph is built.
|
|
17
|
-
*/
|
|
18
|
-
async function loadModule() {
|
|
19
|
-
vi.resetModules();
|
|
20
|
-
vi.doMock(PDFJS, () => pdfjsImport());
|
|
21
|
-
vi.doMock(WORKER, () => {
|
|
22
|
-
workerImports += 1;
|
|
23
|
-
return {};
|
|
24
|
-
});
|
|
25
|
-
return await import("./pdf-loader");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let consoleWarn: ReturnType<typeof vi.spyOn>;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
pdfjsImport = installedPdfjs;
|
|
32
|
-
workerImports = 0;
|
|
33
|
-
consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
consoleWarn.mockRestore();
|
|
38
|
-
vi.doUnmock(PDFJS);
|
|
39
|
-
vi.doUnmock(WORKER);
|
|
40
|
-
vi.resetModules();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("loadPdfjs", () => {
|
|
44
|
-
it("runs the worker on the main thread when no worker URL is configured", async () => {
|
|
45
|
-
const { loadPdfjs } = await loadModule();
|
|
46
|
-
const pdfjs = await loadPdfjs();
|
|
47
|
-
expect(typeof pdfjs.getDocument).toBe("function");
|
|
48
|
-
expect(workerImports).toBe(1);
|
|
49
|
-
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe("");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("uses the configured worker URL instead of importing the worker", async () => {
|
|
53
|
-
const { configurePdfViewer, loadPdfjs } = await loadModule();
|
|
54
|
-
configurePdfViewer({ workerSrc: "/assets/pdf.worker.min.mjs", assetsUrl: "/pdfjs/" });
|
|
55
|
-
const pdfjs = await loadPdfjs();
|
|
56
|
-
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe("/assets/pdf.worker.min.mjs");
|
|
57
|
-
expect(workerImports).toBe(0);
|
|
58
|
-
expect(consoleWarn).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("warns once that scanned pages need assetsUrl", async () => {
|
|
62
|
-
const { loadPdfjs } = await loadModule();
|
|
63
|
-
await loadPdfjs();
|
|
64
|
-
await loadPdfjs();
|
|
65
|
-
expect(consoleWarn).toHaveBeenCalledTimes(1);
|
|
66
|
-
expect(consoleWarn.mock.calls[0][0]).toMatch(/no assetsUrl.*JBIG2, CCITT fax/);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("treats a stub without the pdf.js API as missing, for the rest of the session", async () => {
|
|
70
|
-
// What a bundler told to ignore the uninstalled peer hands back.
|
|
71
|
-
pdfjsImport = () => ({ default: {}, getDocument: undefined });
|
|
72
|
-
const { loadPdfjs, isMissingPdfjsError } = await loadModule();
|
|
73
|
-
const first = await loadPdfjs().catch((error: unknown) => error);
|
|
74
|
-
expect(isMissingPdfjsError(first)).toBe(true);
|
|
75
|
-
expect((first as Error).message).toMatch(/Install pdfjs-dist/);
|
|
76
|
-
|
|
77
|
-
pdfjsImport = installedPdfjs;
|
|
78
|
-
expect(await loadPdfjs().catch((error: unknown) => error)).toBe(first);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("tries again after a failure that is not a missing package", async () => {
|
|
82
|
-
let attempts = 0;
|
|
83
|
-
pdfjsImport = () => {
|
|
84
|
-
attempts += 1;
|
|
85
|
-
if (attempts === 1) throw new Error("Failed to fetch dynamically imported module");
|
|
86
|
-
return installedPdfjs();
|
|
87
|
-
};
|
|
88
|
-
const { loadPdfjs, isMissingPdfjsError } = await loadModule();
|
|
89
|
-
const failure = await loadPdfjs().catch((error: unknown) => error);
|
|
90
|
-
expect(isMissingPdfjsError(failure)).toBe(false);
|
|
91
|
-
await expect(loadPdfjs()).resolves.toHaveProperty("getDocument");
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("asMissingPdfjsError", () => {
|
|
96
|
-
it("names pdfjs-dist only for a resolution failure about it", async () => {
|
|
97
|
-
const { asMissingPdfjsError, isMissingPdfjsError } = await loadModule();
|
|
98
|
-
for (const message of [
|
|
99
|
-
`Failed to resolve import "${PDFJS}". Does the file exist?`,
|
|
100
|
-
`Could not resolve "pdfjs-dist/legacy/build/pdf.mjs"`,
|
|
101
|
-
"Cannot find module 'pdfjs-dist/legacy/build/pdf.mjs'",
|
|
102
|
-
]) {
|
|
103
|
-
expect(isMissingPdfjsError(asMissingPdfjsError(new Error(message))), message).toBe(true);
|
|
104
|
-
}
|
|
105
|
-
const chunk = new Error("Failed to fetch dynamically imported module");
|
|
106
|
-
expect(asMissingPdfjsError(chunk)).toBe(chunk);
|
|
107
|
-
const other = new Error('Could not resolve "./chunk-abc.js"');
|
|
108
|
-
expect(asMissingPdfjsError(other)).toBe(other);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("pdfAssetParams", () => {
|
|
113
|
-
it("asks for nothing until assetsUrl is configured", async () => {
|
|
114
|
-
const { pdfAssetParams } = await loadModule();
|
|
115
|
-
expect(pdfAssetParams()).toEqual({});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("points every asset directory under an absolute assetsUrl", async () => {
|
|
119
|
-
const { configurePdfViewer, pdfAssetParams } = await loadModule();
|
|
120
|
-
configurePdfViewer({ assetsUrl: "/static/pdfjs" });
|
|
121
|
-
const base = new URL("/static/pdfjs/", document.baseURI).href;
|
|
122
|
-
expect(pdfAssetParams()).toEqual({
|
|
123
|
-
wasmUrl: `${base}wasm/`,
|
|
124
|
-
cMapUrl: `${base}cmaps/`,
|
|
125
|
-
cMapPacked: true,
|
|
126
|
-
standardFontDataUrl: `${base}standard_fonts/`,
|
|
127
|
-
iccUrl: `${base}iccs/`,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|