Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
7ac17c9
fix(web): give Stryker's typescript-checker a program covering router…
Mearman Sep 12, 2026
eec07fb
test(web): cover the filename-extension and relative-time helpers
Mearman Sep 12, 2026
711f210
fix(web): remove the unreachable '.bin' fallback in the native save-p…
Mearman Sep 12, 2026
54693fc
test(web): cover the IndexedDB-backed recent-files store
Mearman Sep 12, 2026
da62909
test(web): cover every RPC-client-wrapping hook and the worker docume…
Mearman Sep 12, 2026
5081a36
test(web): cover the structural PDF/content inspection hooks
Mearman Sep 12, 2026
a4471f1
test(web): exercise the router's own oRPC procedures end to end
Mearman Sep 12, 2026
e31adfd
test(web): cover DiagnosticsPanel's collapse-threshold branching
Mearman Sep 12, 2026
beea699
refactor(web): extract mountApp out of main.tsx for direct unit testing
Mearman Sep 12, 2026
910fef0
test(web): cover every branch of appendMathMlNodes' node-kind dispatch
Mearman Sep 12, 2026
be38f7f
test(web): cover notifySuccess's warning-count branching and notifyEr…
Mearman Sep 12, 2026
01ac2cb
test(web): cover the pending-reopen mailbox's write-once, read-once c…
Mearman Sep 12, 2026
c09be0f
test(web): cover StructureTree's empty-vs-populated render branch
Mearman Sep 12, 2026
9660a71
test(web): cover InspectPanel, the format-neutral preview shells, and…
Mearman Sep 12, 2026
e779085
test(web): cover SlidesPreview's shape/vector rendering and paint ord…
Mearman Sep 12, 2026
f93fa55
test(web): cover FileUpload's drop/click file handling and accept nor…
Mearman Sep 12, 2026
61ad845
test(web): cover RecentFilesPanel's reopen permission flow and size f…
Mearman Sep 12, 2026
4f1b71e
test(web): cover MarkdownPreview's paragraph styling and list grouping
Mearman Sep 12, 2026
0c1f496
test(web): cover the / route's unconditional redirect to /convert
Mearman Sep 12, 2026
5e8c4fe
refactor(web): extract the root layout's colour-scheme cycling as pur…
Mearman Sep 12, 2026
a3fa2f0
fix(web): disable route auto-code-splitting under vitest
Mearman Sep 12, 2026
3c155bb
test(web): cover FontsPage's extraction trigger and unrecognised-form…
Mearman Sep 12, 2026
9a50be5
test(web): add a QueryClientProvider-wrapped mount harness
Mearman Sep 12, 2026
eed8fcb
test(web): reach 100% mutation coverage for the .odb browse route
Mearman Sep 13, 2026
df4dc80
test(web): reach 100% mutation coverage for the inspect route
Mearman Sep 13, 2026
5644b78
test(web): reach 100% mutation coverage for the .odm render route
Mearman Sep 13, 2026
6d6f0a7
chore(web): raise the mutation breakThreshold to the newly measured f…
Mearman Sep 13, 2026
f6a4002
test(web): reach 100% mutation coverage for the metadata and package …
Mearman Sep 13, 2026
b8848ae
test(web): give the package route's mocked content fixture a real sch…
Mearman Sep 13, 2026
b1e68bd
test(web): cover title/author-override and in-flight-read carry-over …
Mearman Sep 13, 2026
66e5e3f
test(web): reach 100% mutation coverage for the editors route
Mearman Sep 13, 2026
7039982
test(web): cover editors route accept map, per-paragraph identity, an…
Mearman Sep 13, 2026
0893e64
chore(web): raise the mutation breakThreshold to the newly measured f…
Mearman Sep 13, 2026
870e43e
test(web): cover the convert route's format-family branching and full…
Mearman Sep 13, 2026
56b77d7
test(web): cover convert route's option lists, inspection backing, an…
Mearman Sep 13, 2026
9b1fbe9
test(web): reach 100% mutation coverage for the convert route
Mearman Sep 13, 2026
d86e4fc
test(web): wait for the async structure-inspection calls before asser…
Mearman Sep 13, 2026
53f842a
chore(web): raise the mutation breakThreshold to the newly measured f…
Mearman Sep 13, 2026
1d9f97c
test(web): enable real CSS injection for vanilla-extract mutation cov…
Mearman Sep 13, 2026
e972fc1
test(web): make jsdom's matchMedia/ResizeObserver/document.fonts stub…
Mearman Sep 13, 2026
a70fb2b
test(web): cover mountComponent/renderHook's own append, rerender, an…
Mearman Sep 13, 2026
f9f0f8c
chore(web): exclude vanilla-extract style modules from mutation testing
Mearman Sep 13, 2026
1a8f93d
test(web): pin the exact queryKey and Blob content useConversions/use…
Mearman Sep 13, 2026
586ea08
test(web): cover getRpcClient's Worker construction, caching, and tra…
Mearman Sep 13, 2026
bcc3250
refactor(web): collapse paragraphsOf/appendParagraphOf switches to th…
Mearman Sep 13, 2026
d4c9d10
test(web): reach 100% mutation coverage for useRecentFiles.ts
Mearman Sep 13, 2026
2e86648
test(web): pin the remaining boundary and direct-array-element cases
Mearman Sep 13, 2026
ffb72bd
test(web): cover contentSummary's presentation and drawing branches
Mearman Sep 13, 2026
d5b0b1d
test(web): pin the exact previewFrame variant each preview component …
Mearman Sep 13, 2026
3b6a154
test(web): reach 100% mutation coverage for DiagnosticsPanel
Mearman Sep 13, 2026
c0a4f81
test(web): reach 100% mutation coverage for the documents.worker entr…
Mearman Sep 13, 2026
c95df26
fix(web): stop the worker test from pulling documents.worker.ts into …
Mearman Sep 13, 2026
c6c09b0
refactor(web): extract Sidebar's version-label logic into testable he…
Mearman Sep 13, 2026
3013a14
test(web): cover RootLayout's colour-scheme button and outlet rendering
Mearman Sep 13, 2026
48f1a22
refactor(web): extract RootLayout's tooltip label and navbar config
Mearman Sep 13, 2026
d10e49e
test(web): cover contentBlocks' run formatting, tables, images, and l…
Mearman Sep 13, 2026
3641b21
refactor(web): remove dead sentinel and redundant guards from content…
Mearman Sep 13, 2026
5fa6af0
fix(web): omit the router codegen plugin entirely under test mode
Mearman Sep 13, 2026
ac9d702
test(web): cover odb.read and odm.render against real minimal packages
Mearman Sep 13, 2026
1781ce8
test(web): cover router's list/heading/code-block/rule heuristics and…
Mearman Sep 14, 2026
5c5b2ef
test(web): cover Sheet/SlidesPreview interaction, colour math, and ab…
Mearman Sep 14, 2026
86af952
fix(web): use the real "string" cell-value kind in the alignment test…
Mearman Sep 14, 2026
a0707a5
test(web): exercise normalizeDocxListKinds against a real docx number…
Mearman Sep 14, 2026
18602fd
test(web): pin RecentFilesPanel's size boundaries, permission flow, a…
Mearman Sep 14, 2026
385e0b5
test(web): pin dexie orderBy ordering and cover markdown/odt content.…
Mearman Sep 14, 2026
64c408c
test(web): distinguish FileUpload's icon slots, multiple flag, and ac…
Mearman Sep 14, 2026
56e01e8
test(web): cover FontsPage's reset, error notification, and bold/ital…
Mearman Sep 14, 2026
12e0916
test(web): pin jsonTree's length-cap boundaries and kind-suffix edge …
Mearman Sep 14, 2026
3b2dd27
test(web): pin the fallback picker's input type, once-listener, and b…
Mearman Sep 14, 2026
8de97b7
test(web): assert InspectPanel's pdf item-kind table by exact row con…
Mearman Sep 14, 2026
4360e09
test(web): pin MarkdownPreview's no-loading state and ordered-list DO…
Mearman Sep 14, 2026
0e74dd4
test(web): cover mountWithClassName's DOM lifecycle and remaining sma…
Mearman Sep 14, 2026
be21b7b
test(web): cover main entry mounting and RecentPage's heading/panel r…
Mearman Sep 14, 2026
184a695
refactor(web): drop FileUpload's unreachable native-picker guard and …
Mearman Sep 14, 2026
0a0934e
refactor(web): resolve heading match without a fallback string, drop …
Mearman Sep 14, 2026
de47f05
refactor(web): derive RecentFilesPanel's reopen tooltip from the same…
Mearman Sep 14, 2026
cef5683
test(web): pin SheetPreview's percentage/currency alignment and drop …
Mearman Sep 14, 2026
66867db
refactor(web): drop SlidesPreview's redundant fontScale and empty-sli…
Mearman Sep 14, 2026
e9098b2
refactor(web): create FileUpload's file access port via useState's la…
Mearman Sep 14, 2026
199ebfc
refactor(web): give useRecentFiles' live query a module-scope, never-…
Mearman Sep 14, 2026
9fd358d
test(web): resolve a docx list against a genuine non-bullet numFmt
Mearman Sep 14, 2026
df6efac
test(web): resolve a none numFmt as bullet and a cross-format numId a…
Mearman Sep 14, 2026
0332cd1
refactor(web): find a filename's final path segment without an unreac…
Mearman Sep 14, 2026
5e34025
refactor(web): drop the redundant ceil around an image asset's byteLe…
Mearman Sep 14, 2026
5fec87c
test(web): reject a leading-dot final segment once a directory prefix…
Mearman Sep 14, 2026
76bc88b
chore(web): raise the mutation breakThreshold to 100
Mearman Sep 14, 2026
705638c
chore(web): reword the stryker.config.ts break-threshold comment arou…
Mearman Sep 14, 2026
f873bba
test(web): type matchMedia listener mocks as void-returning
Mearman Sep 14, 2026
a1fc7d1
test(web): build the dropped-file fixture with defineProperties inste…
Mearman Sep 14, 2026
da188c2
test(web): give flushPromises' setTimeout callback a block body
Mearman Sep 14, 2026
a40135a
fix(web): sync the ooxml.js devDependency pin to its released version
Mearman Sep 14, 2026
22c34f0
fix(web): give StubResizeObserver a real constructor matching ResizeO…
Mearman Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/web/eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,38 @@ export default tseslint.config(
"@typescript-eslint/only-throw-error": "off",
},
},
{
// doubleStrokeKeys is a pure helper exported alongside the SlidesPreview component purely so a test can pin its return value directly: the React `key` strings it computes never reach rendered DOM output at all (React keys are consumed internally, not written to markup), so a rendering-only test has no way to observe them -- the one case in this file where allowConstantExport's "still a component-shaped module" carve-out doesn't apply, since the export is a function, not a constant.
files: ["src/ui/SlidesPreview.tsx"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true, allowExportNames: ["doubleStrokeKeys"] },
],
},
},
{
// formatBytes and reopenTooltipLabel are pure helpers exported alongside RecentFilesPanel for the identical reason __root.tsx's own colorSchemeTooltipLabel/navbarConfig are: Mantine's Tooltip only mounts its floating label content once genuinely open, which a render-only test cannot drive, so the label logic needs to be callable directly.
files: ["src/ui/RecentFilesPanel.tsx"],
rules: {
"react-refresh/only-export-components": [
"warn",
{
allowConstantExport: true,
allowExportNames: ["formatBytes", "reopenTooltipLabel"],
},
],
},
},
{
files: ["src/workers/**/*.ts"],
languageOptions: { globals: { ...globals.worker } },
},
{
// setup.ts's own window.matchMedia stub implements MediaQueryList's legacy addListener/removeListener members because the real interface still declares them as required -- deprecated does not mean absent, and a stub that only satisfies the non-deprecated half of the type would be an incomplete implementation of what it stands in for. Testing that those two members are genuinely present and callable (setup.test.ts) means calling them by name, which is exactly what this rule exists to flag in ordinary application code; scoped to the one test file that has a legitimate reason to.
files: ["src/test/setup.test.ts"],
rules: { "@typescript-eslint/no-deprecated": "off" },
},
{
files: [
"vite.config.ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-react-refresh": "0.5.3",
"fake-indexeddb": "6.2.5",
"globals": "17.9.0",
"husky": "9.1.7",
"jiti": "2.7.0",
"jsdom": "30.0.1",
"ooxml.js": "8.14.7",
"semantic-release": "25.0.9",
"typescript": "6.0.3",
"typescript-eslint": "8.66.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it, vi } from "vitest";

import { createMockRpcClient } from "../../test/mockRpcClient";

vi.mock("../../rpc/client", () => ({ getRpcClient: vi.fn() }));

import { getRpcClient } from "../../rpc/client";
import { convertViaWorker } from "./workerDocumentConverter";

describe("convertViaWorker", () => {
it("calls the RPC client's convert with only the source, targetFormat, and bytes fields", async () => {
const client = createMockRpcClient();
const output = {
document: { format: "pdf" as const, bytes: new Uint8Array([9]) },
diagnostics: [],
content: undefined,
};
vi.mocked(client.convert).mockResolvedValue(output);
vi.mocked(getRpcClient).mockReturnValue(client);

const controller = new AbortController();
const result = await convertViaWorker({
source: "docx",
targetFormat: "pdf",
bytes: new Uint8Array([1, 2]),
signal: controller.signal,
});

expect(client.convert).toHaveBeenCalledWith(
{ source: "docx", targetFormat: "pdf", bytes: new Uint8Array([1, 2]) },
{ signal: controller.signal },
);
expect(result).toEqual(output);
});

it("passes an undefined signal through unchanged when the caller supplies none", async () => {
const client = createMockRpcClient();
vi.mocked(client.convert).mockResolvedValue({
document: { format: "pdf" as const, bytes: new Uint8Array() },
diagnostics: [],
content: undefined,
});
vi.mocked(getRpcClient).mockReturnValue(client);

await convertViaWorker({
source: "docx",
targetFormat: "pdf",
bytes: new Uint8Array([1]),
});

expect(client.convert).toHaveBeenCalledWith(expect.anything(), {
signal: undefined,
});
});
});
24 changes: 24 additions & 0 deletions packages/web/src/adapters/fileAccess/createFileAccess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference lib="dom" />
/// <reference types="wicg-file-system-access" />
import { afterEach, describe, expect, it } from "vitest";

import { createFileAccess } from "./createFileAccess";

afterEach(() => {
Reflect.deleteProperty(window, "showOpenFilePicker");
});

describe("createFileAccess", () => {
it("returns the native adapter when the browser exposes showOpenFilePicker", () => {
window.showOpenFilePicker = (): Promise<[FileSystemFileHandle]> =>
Promise.reject(new Error("not used by this test"));
const access = createFileAccess();
expect(access.supportsNativePicker()).toBe(true);
});

it("returns the fallback adapter when the browser has no showOpenFilePicker", () => {
Reflect.deleteProperty(window, "showOpenFilePicker");
const access = createFileAccess();
expect(access.supportsNativePicker()).toBe(false);
});
});
154 changes: 154 additions & 0 deletions packages/web/src/adapters/fileAccess/fallbackFileAccess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/// <reference lib="dom" />
import { afterEach, describe, expect, it, vi } from "vitest";

import { createFallbackFileAccess } from "./fallbackFileAccess";

afterEach(() => {
vi.restoreAllMocks();
});

// The adapter only ever reads input.files?.[0] -- a numeric-indexed, length-and-item object is all FileList's real interface requires for that, so this builds one directly rather than via object-spreading a File[] (which TypeScript flags as overwriting length/index properties it considers already declared by the array's own structural type).
function fileList(files: File[]): FileList {
const list: FileList = {
length: files.length,
item: (index: number) => files[index] ?? null,
[Symbol.iterator]: () => files[Symbol.iterator](),
};
files.forEach((file, index) => {
list[index] = file;
});
return list;
}

// The adapter drives the picker via input.click(), which a real browser resolves only after the user interacts -- here we intercept the click itself to synthesize the OS picker's outcome (a chosen file, or none) before dispatching the 'change' listener the code awaits.
function stubPickedFiles(files: File[]): void {
vi.spyOn(HTMLInputElement.prototype, "click").mockImplementation(function (
this: HTMLInputElement,
) {
Object.defineProperty(this, "files", {
value: fileList(files),
configurable: true,
});
this.dispatchEvent(new Event("change"));
});
}

describe("createFallbackFileAccess", () => {
it("reports no native picker support", () => {
expect(createFallbackFileAccess().supportsNativePicker()).toBe(false);
});

it("resolves the opened file's bytes and name when a file is chosen", async () => {
const file = new File([new Uint8Array([1, 2, 3])], "report.pdf", {
type: "application/pdf",
});
stubPickedFiles([file]);
const opened = await createFallbackFileAccess().openFile({});
expect(opened?.name).toBe("report.pdf");
expect(Array.from(opened?.bytes ?? [])).toEqual([1, 2, 3]);
expect(opened?.handle).toBeUndefined();
});

it("creates a genuine file-type input element", async () => {
let capturedType = "";
vi.spyOn(HTMLInputElement.prototype, "click").mockImplementation(function (
this: HTMLInputElement,
) {
capturedType = this.type;
Object.defineProperty(this, "files", {
value: fileList([]),
configurable: true,
});
this.dispatchEvent(new Event("change"));
});
await createFallbackFileAccess().openFile({});
expect(capturedType).toBe("file");
});

it("registers the change listener with once: true, so it never re-fires on a later change event", async () => {
const addEventListenerSpy = vi.spyOn(
HTMLInputElement.prototype,
"addEventListener",
);
stubPickedFiles([]);
await createFallbackFileAccess().openFile({});
const [, , options] = addEventListenerSpy.mock.calls.find(
([type]) => type === "change",
)!;
expect(options).toEqual({ once: true });
});

it("resolves undefined when the picker is dismissed with no file chosen", async () => {
stubPickedFiles([]);
const opened = await createFallbackFileAccess().openFile({});
expect(opened).toBeUndefined();
});

it("flattens and joins an accept map's extension groups into the input's accept attribute", async () => {
let capturedAccept = "";
vi.spyOn(HTMLInputElement.prototype, "click").mockImplementation(function (
this: HTMLInputElement,
) {
capturedAccept = this.accept;
Object.defineProperty(this, "files", {
value: fileList([]),
configurable: true,
});
this.dispatchEvent(new Event("change"));
});
await createFallbackFileAccess().openFile({
accept: {
"application/pdf": [".pdf"],
"text/markdown": [".md", ".markdown"],
},
});
expect(capturedAccept).toBe(".pdf,.md,.markdown");
});

it("leaves the input's accept attribute empty when no accept option is given", async () => {
let capturedAccept = "not set";
vi.spyOn(HTMLInputElement.prototype, "click").mockImplementation(function (
this: HTMLInputElement,
) {
capturedAccept = this.accept;
Object.defineProperty(this, "files", {
value: fileList([]),
configurable: true,
});
this.dispatchEvent(new Event("change"));
});
await createFallbackFileAccess().openFile({});
expect(capturedAccept).toBe("");
});

it("saves via a Blob-URL download anchor and revokes the object URL afterwards", async () => {
const createObjectURLSpy = vi
.spyOn(URL, "createObjectURL")
.mockReturnValue("blob:mock-url");
const revokeObjectURLSpy = vi
.spyOn(URL, "revokeObjectURL")
.mockImplementation(() => {});
let clickedHref = "";
let clickedDownload = "";
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function (
this: HTMLAnchorElement,
) {
clickedHref = this.href;
clickedDownload = this.download;
});

const result = await createFallbackFileAccess().saveFile(
new Uint8Array([1, 2, 3]),
{ suggestedName: "out.pdf", mimeType: "application/pdf" },
);

expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
const [blob] = createObjectURLSpy.mock.calls[0] as [Blob];
expect(blob.size).toBe(3);
expect(blob.type).toBe("application/pdf");
expect(clickedHref).toBe("blob:mock-url");
expect(clickedDownload).toBe("out.pdf");
expect(revokeObjectURLSpy).toHaveBeenCalledWith("blob:mock-url");
expect(result).toEqual({});
});
});
Loading