test(web): work toward 100% mutation coverage - #1255
Draft
Mearman wants to merge 24 commits into
Draft
Conversation
….ts and workers/**
tsconfig.json (the main app program) deliberately excludes src/rpc/router.ts and every
src/workers/**/*.ts file, since they belong to tsconfig.worker.json's own DOM-vs-WebWorker lib
split instead. Stryker's typescript-checker plugin requires every mutated file to belong to the
one program its tsconfigFile resolves, so pointing it at tsconfig.json crashed outright the
moment a mutant landed inside router.ts or the worker entry point ("no watcher is registered for
it"). tsconfig.stryker.json is a checker-only program: the same include as tsconfig.json but
without the router.ts/workers exclusions, with DOM and WebWorker unioned (skipLibCheck makes the
pair compile together) so both halves of the app typecheck under the one program the checker
needs.
inferFormatFromFilename and relativeTime had no unit coverage at all despite being pure, easily-tested functions -- every extension/alias mapping, the lowercase-before-match step, the dotfile and no-extension edge cases, and each relativeTime unit boundary (minute/hour/day, floored not rounded) are now exercised directly.
…icker's accept extension
String.split('.').pop() can never return undefined for any input, including a string with no '.'
at all -- split always returns at least one element -- so the '?? "bin"' fallback in
createNativeFileAccess's saveFile was dead code with no test able to reach it. Removed the guard
and added full unit coverage for all three file-access adapters (createFileAccess's native/
fallback selection, the fallback picker's file-chosen/dismissed/accept-attribute paths and its
Blob-URL download-anchor save, and the native picker's open/save flows including the
AbortError-vs-real-failure branches and the accept-extension derivation this fix touches).
Neither src/db/dexie.ts nor src/hooks/useRecentFiles.ts had any unit coverage -- jsdom implements no IndexedDB of its own, so nothing could construct the Dexie instance at module load without one. Adds fake-indexeddb (installed globally in the unit project's test setup, ahead of any test's own import of the db module) and exercises the database's own table schema plus recordRecentFile's 20-entry FIFO eviction and removeRecentFile.
…nt converter None of src/hooks/**'s useMutation/useQuery wrappers around getRpcClient(), nor workerDocumentConverter's own convertViaWorker, had any unit coverage. Adds a small, dependency-free render harness (mounting a hook inside a real jsdom tree via react-dom/client and a fresh QueryClientProvider, the same approach src/ui/contentBlocks.test.tsx already established for component-level tests) and a fully-typed mock RPC client fixture (one vi.fn() per router procedure), then uses both to exercise useConversions, useDocumentFormats, useReadMetadata, useWriteMetadata, useExtractSourceFonts, useReadContent, useRestoreContent, useReadOdb, useOdmRender, useConvert, the five useEditorSession mutations, usePdfObjectUrl's blob-URL lifecycle, and convertViaWorker's own field-narrowing of its RPC call.
contentInspectResult, useReadContent, useInspectPdfBytes, and useInspectDocument (src/hooks/ useInspect.ts) had no coverage -- exercises the pure content-backed result builder, the content.read/pdf.inspect RPC calls, and useInspectDocument's own branch between inspecting PDF bytes directly versus converting a non-PDF source to PDF first and carrying the conversion's own diagnostics through.
router.test.ts already covered normalizeContentForSource and the editor-session helper functions directly, but none of router.ts's actual exported procedures (formats.list/listConversions, convert, content.read/restore, metadata.read/write, fonts.extractSourceFonts, pdf.inspect, and the full editor.open/setParagraphText/addParagraph/removeParagraph/save lifecycle including its unknown-session-id error path) had ever been called through oRPC's own dispatch. Uses @orpc/ server's call() to invoke each procedure directly against real markdown/docx fixtures. Forced onto vitest's node environment: jsdom's own TextEncoder constructs its Uint8Array in a different realm than the bare Uint8Array a z.instanceof(Uint8Array) input schema checks against under jsdom, which otherwise rejects every real byte payload as "expected Uint8Array, received Uint8Array".
Adds a Mantine-aware mount harness (mountWithMantine) and a real DiagnosticsPanel test suite, asserting on the Spoiler wrapper's own class marker rather than its "Show N more" label text: jsdom has no layout engine, so Spoiler's internal measured-height-vs-maxHeight comparison can never observe a real overflow and the label never renders regardless of item count. Stubs window.matchMedia and ResizeObserver in the shared jsdom test setup, guarded on `typeof window` since router.procedures.test.ts forces a node environment for the same file. Both APIs are called unconditionally by MantineProvider/Spoiler on mount, so any test that mounts a Mantine component needs them regardless of what it actually exercises. Restates vitest.mutation.config.ts's own setupFiles key, dropped by the same object-literal override that already restates environment: "jsdom", since fake-indexeddb needs to install before dexie.ts's module-scope Dexie construction runs.
main.tsx called its own root-mounting logic unconditionally at module scope, so the only way to exercise the missing-#root failure path was to import main.tsx itself -- which immediately mounts the real App against whatever #root element the test environment's own document happens to have. Moving the logic into mountApp.tsx, parameterised on the target Document, lets mountApp.test.tsx drive both branches directly against a throwaway jsdom Document, with createRoot mocked so the real router/worker stack is never pulled in. tsconfig.node.json's own program never included src/vite-env.d.ts, so the ambient __APP_COMMIT_SHA__ family of build-time globals were invisible whenever a test transitively imported far enough into the app (App -> router -> routeTree.gen -> every route, including -Sidebar.tsx, which reads them) to pull those files into that program. mountApp.test.tsx's own import chain is the first test to reach that deep, surfacing the gap.
Adds direct coverage for the three cases contentBlocks.test.tsx never exercised: an <annotation> element skipped along with its children, a cdata/comment/declaration/pi node producing no displayable content at all, and interleaved text/skip siblings rendering in order. Removes the redundant containerRef.current null check in MathMlView's effect: the ref is attached to an unconditionally rendered element of the same component instance, and React attaches refs during commit, strictly before a passive effect can observe them, so the guard could never genuinely take its true branch.
…ror's Error/non-Error split notifySuccess picks colour, title suffix, message, and autoClose entirely off whether any diagnostic is warning-severity and how many there are; notifyError reads .message off a real Error but stringifies anything else thrown. Neither had a test before this.
…ontract Asserts the empty-mailbox default, a plain set-then-take round trip, that a take clears the entry so a second take sees nothing, and that a later set overwrites an earlier entry nobody ever took.
Asserts no Tree root renders for a value with no browsable children (an empty object, or a primitive), and that one does once the value has at least one array/object entry to browse.
… SheetPreview Adds a shared src/test/fixtures.ts (a real DocumentTreeJson and a page size, built once outside src/ui/** so no UI test needs to import documents.js's conversion functions directly and trip the package's own import-boundary lint rule). InspectPanel: loading/error/empty branches, content-backed summary + structure tree, pdf-backed page count (singular vs plural), item-kind table, and conditional title/producer lines. FormulaPreview / WordProcessingPreview: the shared loading/error/ no-content/wrong-kind-of-document branches every format-specific preview repeats, plus each one's own real rendering path (MathML for a formula document, section blocks for a wordprocessing one). SheetPreview: single-vs-multiple-sheet SegmentedControl visibility, hidden row/column filtering, index-based ordering independent of array position, the empty-sheet fallback for no visible rows/columns, and a cell's own displayText rendering.
…ering Covers the presentation-vs-drawing content split (slides vs pages), single-vs-multiple-slide SegmentedControl visibility, every vector kind (rect, ellipse, line, path with line/cubic segments and open vs closed subpaths), solid/dashed/dotted/double stroke rendering (the double case simulated as a thick underlay plus a thin gap overlay, gap colour falling back to white when the shape has no fill), rotation transforms, paintOrder-driven ordering with an unset order sorting last, and a shape's own fontScale/lineSpacingReduction CSS derivation.
…malisation Mocks @mantine/dropzone's own Dropzone with a plain button exposing onDrop/onClick directly, since FileUpload's own logic (reading a dropped file's bytes, recording it when its extension resolves to a known format, opening the native picker when supported, normalising a single accept extension string into the array Dropzone expects) is what this package's mutate glob covers -- not the third-party drag-and-drop machinery Dropzone itself provides. Covers: file-present vs empty state (icon, name, hint visibility), loading/disabled passthrough, accept normalisation (string vs array, undefined), native-picker-driven onClick/activateOnClick wiring, a dropped file with no entries, and an unrecognised extension being handed to onFile without being recorded.
…ormatting Mocks useRecentFiles/removeRecentFile, useNavigate, notifyError, and setPendingReopen directly rather than exercising real IndexedDB and routing, since those are already covered by their own dedicated test suites -- this file's own logic is the permission-then-read-then- navigate chain, byte-size formatting thresholds, and the disabled/ unrecognised-format guards around it. Covers: the loading/empty/populated list states, B/KB/MB size formatting boundaries, the reopen action disabled with no handle, remove-by-id, a granted-on-first-query reopen, a granted-only-after request, a denied permission (notifies, never navigates), a read failure (notifies with the thrown error), and an unrecognised stored format (does nothing, silently).
Covers every recognised paragraph styleId (heading-1..6, quote, code-block, horizontal-rule, and the plain-paragraph fallback), image and table block delegation (the latter recursing back through this same markdown pipeline for cell content), and the list-grouping behaviour specific to this component: consecutive ordered/bullet runs collapse into one <ol>/<ul>, a type change between adjacent siblings splits into two lists, a deeper-level item nests inside its parent <li>, and a non-list paragraph interrupting a run starts a fresh list group afterward rather than merging with it.
Excludes *.test.ts(x) from the router plugin's route-tree scan (routeFileIgnorePattern) so a route's own unit test file doesn't itself get treated as an undeclared route -- the existing dash-prefix convention in this directory is for genuine non-route support files (-Sidebar.tsx), not a fit for a test file that belongs named like every other test in the package.
…e, testable lookups activeColorSchemeOption/nextColorSchemeOption/optionAt were inline RootLayout logic reachable only by mounting the full AppShell inside a real router and Mantine tree. Extracted as plain functions over a string value, __root.test.ts now drives every branch directly: optionAt's out-of-range throw (never reachable through RootLayout's own two call sites, since the modulo arithmetic guarantees a valid index, but a real invariant worth asserting explicitly rather than papering over with a silent fallback), an unrecognised current value falling back to the first option, and the wrap-around from the last option back to the first.
The router plugin's autoCodeSplitting rewrites every real route file's component behind a dynamic import, entirely independent of whether anything actually imports through the generated routeTree.gen.ts -- the transform keys off the route file's own path. A route-level unit test necessarily imports a route file directly (there is no other way to reach Route.options.component), so mounting it genuinely suspended waiting on a chunk vitest has no build pipeline reason to ever resolve quickly, and the very first such mount in a whole run could take several real seconds. Gated off under mode "test" the same way `base` above is already gated on `command`: a production bundle-size optimisation has no business affecting whether or how fast a test can render a route's component. Confirmed the real build still code-splits every route into its own chunk exactly as before.
…at guard Mocks the RPC client and FileUpload directly, exercising FontsPage's own composition: a recognised format triggers extractSourceFonts and renders each family with its bold/italic flags, an empty result shows the no-embedded-fonts message, an unrecognised extension neither calls extraction nor loses the alert, and a rejected extraction leaves no font table behind.
mountWithProviders wraps MantineProvider around a fresh QueryClientProvider per mount, for a route component that calls a react-query hook (useMutation/useQuery/useLiveQuery) itself rather than only through a hook this package already tests in isolation -- mirroring renderHookWithQueryClient's own per-mount QueryClient.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the Stryker typescript-checker crash for this package (router.ts and workers/** belong to tsconfig.worker.json, not the app tsconfig.json Stryker's checker was pointed at -- neither program alone covers everything the mutate glob touches) and adds real unit coverage across the previously-untested pure helpers, file-access adapters, the IndexedDB-backed recent-files store, every RPC-client-wrapping hook, every format-neutral preview component, and several route/UI files.
Progress so far, not yet complete:
?? "bin"fallback in the native save pickerautoCodeSplittingrewrites every route file's component behind a dynamic import regardless of whether anything goes through the generated route tree, so mounting any route'sRoute.options.componentdirectly in a test genuinely suspended on the first render. Gated off under vitest's own test mode, the same way this config already gatesbaseon the build/serve command -- confirmed the real production build still code-splits every route exactly as before.Remaining gap: src/rpc/router.ts's harder procedures (fonts.describe, odb.read, odm.render, non-markdown editor.save) are still untested; most of src/routes/** (recent.tsx, odb.tsx, inspect.tsx, package.tsx, odm.tsx, metadata.tsx, editors.tsx, convert.tsx, -Sidebar.tsx) has no unit tests yet. A first real Stryker baseline run is in progress (partial data so far: roughly 1100+/2870 mutants tested, ~50 survived) but has not completed within this session -- the package's real size (3000+ mutants across 81 mutated files) combined with heavy contention on the shared machine this ran on means a full run takes upward of an hour. Work continues on this branch.
No Stryker disable comments anywhere in the package.