Conversation
Comment on lines
+299
to
+301
| cell.borders = { | ||
| top: { color: { r: 1, g: 0, b: 0 }, widthPt: 2, style: "dashed" }, | ||
| }; |
| for (let column = 0; column < 6; column++) { | ||
| sheet.cell(0, column).value = { kind: "number", value: column }; | ||
| } | ||
| sheet.printSettings = { ...BASE, repeatColumns: { start: 0, end: 1 } }; |
Mearman
force-pushed
the
feat/100-percent-mutation-documents.js
branch
from
September 15, 2026 10:34
75198d8 to
d098188
Compare
The 200,000-synthetic-row-divider test in lattice.test.ts completes in well under a second uncontended, but under Stryker's per-statement instrumentation combined with heavy concurrent host load it can exceed vitest's default 5000ms wall-clock timeout despite doing the same real work. Raise the timeout to 60 seconds, matching the same wall-clock-dominated-by-scheduling pattern already documented for read-graph.test.ts's docxToPdf conversion and its sibling ODS mergeCells test.
…and header labels
bridges.test.ts's ods<->xlsx round-trip test read only each cell's numeric/typed
`.value`, never the source ODS fixture's own rendered `.displayText`
(buildRichFixturePackage's per-cell text:p run) or the two unchecked header labels
("Amount", "Active") -- so a header cell or a cell's rendered text could silently
go blank without any assertion catching it.
Also remove gridOdsPackage/richOdsPackage/decoratedOdsPackage from test-support/ods.ts:
dead exports with no caller anywhere in the suite (only their *Bytes counterparts are
ever used), each one a whole function body with no test coverage.
embeddedHsqldbMultiIndexOdbPackage (odb.ts), odfFormulaPackage (odf.ts), sheetFormulaOdsPackage (ods-formula.ts), and pdfWithForeignHiddenAnnotationPdf (pdf.ts) had no caller anywhere in the suite -- each one a whole function body (plus, for the first three, a now-unused Package/decodePackage import) that existed purely as NoCoverage mutation surface with nothing exercising it. pdfWithForeignHiddenAnnotationPdf's own comment ties it to a readPageNotes /T-marker check in pdf/read.ts, but no such file or function exists anywhere in this package's current src -- the reader it was meant to exercise appears to have moved or been removed elsewhere, leaving this fixture orphaned.
…pdate/clear The borders getter (top/left/bottom/right resolution, nil/none exclusion, auto-color and missing-sz fallbacks, and the empty-map-to-undefined collapse) was only ever exercised indirectly through a docx-odt bridge round trip that reads back via ooxml.js's own separate reader, never through this getter itself. Add direct tests for the getter's full edge/style/color/width matrix, the nil/none exclusion path, the auto-color and missing-attribute fallbacks, and the all-edges-excluded case. Also cover DocxTableRow.heightPt being updated and cleared on a row that already carries a w:trHeight, which the existing round-trip test never exercised since it only ever set the value once.
…, background, and borders Each of DocxTableCell's colSpan/background/borders setters filters out any prior element of the same kind before inserting the new one, but every existing test only ever set each property once starting from an empty w:tcPr, so that filter's own predicate never ran against a real element -- setting a value while one already exists was untested.
createEmptyDocxPackage's own tests only checked that the right parts and elements existed, never the actual namespace URIs, content-type strings, relationship targets, page/margin dimensions, or style attributes createEmptyDocxPackage hardcodes -- so a corrupted namespace, content type, or page dimension would still pass every existing assertion. Add exact-value checks for every literal: the version/encoding/standalone declaration on every part, both Default extensions and Override content types in [Content_Types].xml, both relationship parts' Id/Type/Target, the US-Letter w:sectPr's pgSz/pgMar values, and the Normal style's type/id/name.
…ight's no-op branch Both helpers stamp DEFAULT_COLUMN_WIDTH_PT/DEFAULT_ROW_HEIGHT_PT only when the column/row has no width/height style yet, but no existing test ever called cell() (which triggers them) on a column/row that already had an explicit width/height set -- so the "already set, leave it alone" branch never actually ran with a real value to compare against.
…ts gridlines/headers parsing
readSheetPrintSettings/writeSheetPrintSettings had no dedicated test
at all; every existing test only ever set printSettings.gridlines
through a single shallow property check elsewhere, leaving pageSize,
margins, pageOrder, printRange, scalePercent, fitToPages, manualBreaks,
and repeatColumns/repeatRows entirely unexercised. Add direct coverage
for each field's round trip through the real OdsSheet.printSettings
getter/setter, the wrapRepeatRange gap-fill and stale-wrapper
dissolution behaviour, hasManualBreak's own break-detection, and
parsePrintRanges/parseScalePercentage/parseNonNegativeInteger's
malformed-input handling (poking the underlying XML directly for
shapes the writer itself never produces).
Also simplify the gridlines/headers reader: the previous
`new Set(...).filter((token) => token.length > 0)` guarded against
empty tokens from stray whitespace, but the only two things ever read
from that set are `.has("grid")`/`.has("headers")`, which an empty
token can never satisfy either way -- the filter's outcome was
unobservable through any real behaviour. Replaced with a plain
`.split(" ").includes(...)` check, which is equally robust to stray
whitespace without carrying an untestable branch.
createEmptyOdsPackage had no dedicated test at all -- every existing test only exercised it indirectly through createOds(), checking structural existence (a table exists, a cell can be written) without ever asserting the actual namespace URIs, the of: namespace LibreOffice needs to recalculate table:formula on open, media type, page-layout geometry, calculation-settings defaults, or office:meta field mapping this scaffold hardcodes. Add exact-value checks for the mimetype and manifest root entry, every part's declaration, content.xml's of: namespace and default sheet/style chain, styles.xml's page-layout/ master-page chain, and every office:meta field buildOfficeMeta writes (and omits) for a given LayoutMetadata.
… the ods scaffold test tsconfig.node.json's stricter typecheck (run separately from the default tsconfig by the pre-push hook) caught what tsconfig.json's own run missed: Array.prototype.find can return undefined, and attr() requires a real XmlElement.
…back in detectPackageManager userAgent ?? "" only mattered for the startsWith checks below it, and none of yarn/pnpm/bun's prefixes match an empty string either, so the fallback literal's own value was unobservable and the branch always returned npm regardless. Replace it with an explicit early return for the undefined case so the mutation-prone fallback literal no longer exists as an AST node.
…ectly No test in the package called applyOdfGeometry directly; its 33 covering tests all went through higher-level editors that never distinguished rotationDeg undefined from rotationDeg 0, so the combined "undefined || === 0" guard survived a mutation to a bare false. Add direct unit coverage for all three branches (undefined, exactly 0, and a real rotation) plus a sanity check on buildTransformAttr's own composition.
UnsupportedFontSourceFormatError, UnsupportedConversionError, and UnsupportedPackageFormatError were only ever checked by instanceof/toThrow(), so each constructor's own this.name assignment had no test observing its value and survived a mutation to an empty string.
…ion guard Neither constructor guard's error message had a test calling it with a wrong-kind ContentDocument, so both throw strings survived being mutated to an empty template literal.
…te string The presentation-slide-shape test only checked the recovered formula, never the locate path built from the slide/shape indices, so that template literal survived being mutated to an empty string.
…g count No test read sheet.rows at all, so table.rows.length + 1 (one sizing entry per data row plus the header row) survived being mutated to - 1.
The "single-length level whose slot is undefined" branch had no covering test at all (not reachable through any real odf.js-decoded report), so its error message survived being mutated to an empty string. Exercise it directly with a hand-built groups array carrying an explicit hole.
The only existing test's container held a single element that always matched regardless of the tag check, so node.tag === tag survived being mutated to true. Add a container whose one element has a different tag.
… option forwarding
Neither the markdown reader's images/signal object nor readDocumentLayout's
signal object had a test observing an effect from either field, so both
ObjectLiteral mutations to {} survived. Assert the images resolver is
actually invoked and that an aborted signal is checked before parsing (the
markdown reader's own signal check, and readPdf's own pre-page-loop check).
Also assert throwIfAborted's DOMException carries the exact name/message
rather than only its type.
…uards xls.content.write's non-spreadsheet refusal message and pdf.layout.write's signal forwarding to writePdf had no test observing either, so a StringLiteral mutation to the error message and an ObjectLiteral mutation of the options object both survived.
inferCellValue("") already returns undefined on its own (its own
text.length === 0 check), so field === "" ? undefined : inferCellValue(field)
restated that in a second place for no observable difference -- an empty
field's own type-inference path was unkillable by any test because both
branches always produced the same undefined. Call inferCellValue(field)
directly, and assert onCellTypeInference fires only for the populated
fields in a row that also carries empty ones.
Mirrors the identical CsvInvalidUtf8Error gap: only instanceof/toThrow(Class) was checked, so the constructor's own name/message string literals survived being mutated to empty strings.
…case Number.prototype.toFixed already normalizes -0 to "0.000000" on its own, so the trailing-zero trim below collapses any zero-valued coordinate (positive or negative) to a bare "0" through the exact same path every other value takes -- the early return produced no output any test could ever distinguish from the general path, for any input.
…no fonts No test called treeEmbeddedFontsOf directly, so its "return undefined rather than an empty array" branch (the whole point of the function per its own comment: a splice site can spread undefined without minting an empty table) had no test observing the difference between the two.
…-placeholder branches The existing table test's nested cell content never needed transforming, so recursing markdownTableCell into it versus not (a BlockStatement mutation skipping the whole branch) produced identical output. Add a case with a pageBreak nested in a cell, which only survives as marker text if the recursion genuinely ran. Also cover formulaParagraph's own placeholder-run branch (no presentation LaTeX), whose ArrayDeclaration mutation to an empty runs array had no test asserting the placeholder text reached the output.
…sting odf.js's own single shared paintOrder counter guarantees a shape and a vector can never collide on the same value, which made the "order < vectorOrder" boundary check unkillable through any real odf.js-decoded package -- the "<=" mutant produces identical grouping for every reachable input. Split the pure grouping algorithm out of collectSlideVectorGroups so it takes plain paintOrder-bearing data directly, and test the boundary (and the paintOrderOf throw guard) against a hand-built collision no real package can produce but the function's own contract still needs to hold for.
…e guard short-circuits Every legacy reader independently rejects non-CFB bytes too (each one's own first step is archive-codec's readCompoundFile), so the outcome alone (undefined either way) could never distinguish the outer isCompoundFile guard existing from it being skipped. Spy on readDocContent to prove it is never even invoked for bytes that fail the guard, rather than merely happening to fall through all three readers to the identical result.
…mutant
Slicing from indexOf(":") + 1 already returns the whole string when
there is no colon (indexOf yields -1, so the slice starts at 0), so
the colonIndex === -1 ternary guard was redundant: every input was
already handled correctly by the slice alone. That redundancy made a
ConditionalExpression mutation on the guard permanently equivalent
(unkillable by any test), since the branch it toggled produced the
same output either way. Dropping the guard removes the mutation
opportunity outright.
reduceRational(0n, 0n) exercises gcd's own 0/0 special case (defined
as 1 to avoid a BigInt division-by-zero crash) directly, rather than
only through a path decimalToRational can actually reach.
decimalToRational("-5") pins that the ^\d+$ digits check rejects a
literal whose only non-digit character sits before its digits, not
just one with non-digit characters at the end (already covered by the
existing "12a" case).
The existing test only exercised title/author being kept when overrides omits them; subject and keywords had the identical conditional-spread shape with no coverage of their own absent-field case.
…ent mutants compareCellKeys checked equality first, then used a nested less-than ternary for the remainder. Since the equality guard already rules out left === right before that nested comparison runs, a < -to- <= mutation on it produced identical output for every reachable input: the equal case never reaches that branch, and < versus <= only differ on equality. Reordering to check < (then >, then implicit equal) puts the equal-values input back in reach of that comparison, so a relational-operator mutation there is observable again.
The existing "outside Pictures/" case used a path whose Pictures/- prefix-length slice happens not to look like an image filename, so it could not distinguish a real prefix check from one that never actually skipped. Adds a path deliberately crafted so slicing at the Pictures/ prefix length spells a valid image filename by coincidence, pinning that the function only follows the real path prefix.
The early return for an empty vector-groups array only skipped allocating a shapes array that the rebuild loop below would otherwise reconstruct with identical contents (the insertion while-loop never runs when groups is empty, so every shapeIndex iteration just re-pushes the shape already at that index). The guard changed nothing observable, making its ConditionalExpression mutation permanently equivalent. Removing it drops the mutation opportunity.
The presence loop matched on element tag AND attribute value, but no test distinguished that from matching on the attribute value alone. Adds a case where an Override element carries an Extension attribute equal to the value being ensured, confirming ensureDefaultContentType still adds a genuine Default entry rather than mistaking the Override for one.
escapeRegExp's replacement text turning a special character into a literal match, rather than deleting it outright, needs a part whose extension contains one to distinguish -- the existing "p.g" case only proved over-matching was prevented, not that a genuine "p+g" match still works. Also mirrors src/odf-package/media.test.ts's own prefix-length coincidence case for this sibling OOXML-side implementation.
…r guard siblingRank !== -1 was always true by the time siblingRank > childRank runs, since -1 can never be greater than childRank (already known non-negative from the earlier childRank === -1 return above) -- the guard changed nothing observable, an equivalent mutant. Also adds insertBefore's own not-found-append-at-end case (insertAfter already had one) and a same-schema-rank case for insertInSchemaOrder, both previously unexercised.
Indexing a string past its end yields undefined in JavaScript, and undefined === " " is already false, so the i + runLength < text.length check could never change the loop's outcome on its own -- it only ever agreed with what the character comparison already decided, making three separate mutations on it (the bound itself, its operator, and its arithmetic) all permanently equivalent. Relying on the character comparison alone removes the redundant check.
… content None of content.xml/styles.xml/meta.xml's own root element tag was directly asserted -- every existing check reached into a child by tag name, which would still succeed even with an empty root tag. Also pins that CONTENT_NS_PREFIXES is genuinely spread into content.xml's attributes (xmlns:table specifically, distinct from the hand-declared xmlns:of already checked) and that styles.xml carries its own empty office:styles sibling.
…kers MATH_INLINE_SOURCE, MATH_INLINE_FONT_MARKER, and MATH_BLOCK_STYLE_ID had no test exercising formulaParagraph at all: a formula whose provenance source matches the inline marker renders as a \( \) span, and any other source renders as a $$ display block instead.
…utes Neither the XML declaration node nor the four xmlns attributes on cp:coreProperties were directly asserted anywhere -- every existing check reached into a child by tag name or attribute, which would still succeed with an empty declaration/attributes object.
Two independently-ANDed length checks (common < fromDirs.length && common < toDirs.length) meant relaxing either one in isolation never changed the loop's outcome: the sibling, still-correct check kept stopping the loop at the same common, and wherever the two paths' directories genuinely differ, fromDirs[common] === toDirs[common] already fails once one side runs out (a real segment can never equal undefined). That made both length checks, their operators, and the && joining them all permanently equivalent mutation targets. A single combined bound has no sibling clause left to mask a boundary mutation, observable via two identically-deep directory chains. Also drops relsPathFor's own redundant lastSlash === -1 guard for the filename split, the same "slicing from -1 + 1 already returns the whole string" equivalence already fixed in src/mathml/nodes.ts's localName.
…etry passthrough paragraphs() had no test proving it excludes non-paragraph blocks (a table specifically) from the fixture it already round-trips. startList's own task default had no case exercising the unset (false) path, only the explicit task: true one. Neither constructor guard (non-wordprocessing kind, an empty sections array) nor the pageSize/margins/clock passthrough into createMarkdownEditor had any coverage at all.
…line stamping readDocumentMetadata and readNativeDocumentTree had no coverage at all for abort-signal forwarding on either their pdf or non-pdf dispatch branch, nor for the markdown images resolver readNativeDocumentTree also forwards. The stampPdfPackageTables call had no test able to distinguish it running from being skipped either: the existing pdf fixture (docxToPdf of a plain docx) carries no outline or destinations of its own, so stamping is a no-op either way for that input. Building a small PDF directly through pdf-codec's own writePdf with a bare outline entry, bypassing every documents.js writer, gives the call something to actually stamp.
…message shapes The odf -> pdf special case checked source.format === "odf" && targetFormat === "pdf", but nothing exercised the targetFormat half: an odf source to any OTHER target had no test, so a request that should reject as unsupported (odf has no composition-engine route except to pdf) could have silently succeeded through odfToPdf instead. Also adds direct coverage for the char/substituted diagnostic's exact message text, the bold/italic weight suffixes in a font-substitution message, odfToPdf's own onDocument/signal forwarding, and that the odf->pdf route's options object genuinely reaches the call.
…ar from stripping it
nextPictureIndex's escapeRegExp replaces a matched special character with
an escaped copy ("\\$&"), but the existing regex-special-character test
used an extension/filename pair that stayed a non-match whether the
character was escaped or dropped entirely, so a replacement of "" survived.
Adding a case where the escaped form matches but the stripped form does not
makes the escaping itself observable.
…g, not just PartName The existing-override scan matched on PartName alone in its covering tests, so relaxing the tag check to always-true never changed the outcome. Adding a sibling element that carries a matching PartName but isn't an Override makes the tag comparison itself load-bearing: without it, the scan would mistake the decoy for a real entry and add nothing.
… bound The common-prefix scan bounded itself with Math.min(fromDirs.length, toDirs.length), but that bound is provably redundant: the instant the scan passes the end of the shorter array, indexing it yields undefined, which can never strictly equal a real path segment, so the comparison already stops the loop there on its own. That made a Math.min/Math.max swap on the bound an equivalent mutation no test could ever kill. Relying on the undefined comparison directly removes the mutation opportunity rather than leaving it unkillable.
…ling after, not before The prior same-rank test compared only the resulting tag sequence, which reads identically whether the new node lands before or after an existing sibling sharing its own tag -- so relaxing the rank comparison from strictly-greater to greater-or-equal never changed the assertion's outcome. Distinguishing the two elements by a marker attribute makes which physical node ended up first observable.
…bound The main character-walking loop's i < text.length bound admits a mutation (i <= text.length) that is unobservable: charAt past a string's end already returns "", and appending "" never changes the accumulated literal buffer, so no test could ever kill it. Comparing i directly against text.length with !== keeps the identical behaviour (i only ever advances by positive steps that land exactly on an untouched index or on text.length itself) while making the loop's own termination condition a real mutation target: flipping !== to === inverts it outright.
…ssert their own arguments readNativeDocumentTree's pdf branch forwards signal (and, for readPdf alone, sink) into two separate calls, but reconstructWordprocessing's own independent signal check already throws AbortError for an aborted signal, so a black-box test asserting the outer function throws could not tell whether readPdf itself ever received the signal or sink at all -- both an intact and a stripped call to readPdf produced the identical outer throw. Spying on each call directly makes its own argument object observable.
fontSubstitutionDiagnostic's else branch (the "missing-face" reason, reached when a caller-supplied family exists but not the exact bold/italic combination requested) had no test reaching it at all -- every existing substitution test exercised only "vendored-substitute". Requesting bold text while supplying just the family's regular face triggers the family-fallback path instead of the vendored table.
…tion branch odfFormulaBytes' mimetype part name, its stored-uncompressed requirement, and the presence/absence of the StarMath annotation element were never independently verified: every consuming test only reads content.xml back through a real ODF/MathML reader, which never inspects the mimetype entry's name or compression method and would tolerate either annotation branch's exact text unnoticed. A direct test on the fixture's own raw zip bytes and content.xml is what makes both properties observable.
… own tag guard Neither branch had any direct coverage: nothing exercised an a:ln-bearing shape the production reader can't recognise (the throw this test-support oracle exists to surface), and every consuming test's fixtures only ever built shapes whose tag already matched spPrTag, so the tag comparison itself was never load-bearing in any covering test.
… property-presence guard rotationsOf's vector.kind === "line" comparison was a genuine equivalent- mutant trap: forcing that comparison to always-true still type-narrows on the original condition text, so accessing rotationDeg in that branch stays valid, and a line vector's own missing key reports undefined either way -- indistinguishable from the correct branch's own explicit undefined, so no test could ever kill it. A "rotationDeg" in vector check narrows identically for every real input but has no such loophole: an always-true mutation of it fails to compile outright (accessing rotationDeg on the now-unnarrowed union), leaving only an always-false mutation, which a real rotationDeg value does kill. withoutRotation's own analogous survivors were a toEqual gap rather than an equivalent mutant: spreading an explicit rotationDeg: undefined onto a line vector (which never carries that key at all) still toEqual's the untouched original, since toEqual treats an undefined-valued key as indistinguishable from an absent one. toStrictEqual does not.
Mearman
force-pushed
the
feat/100-percent-mutation-documents.js
branch
from
September 15, 2026 18:08
5df1bf6 to
951f3af
Compare
…olResolver The LaTeX symbol table's own command-to-glyph lookup, id-minting scheme, and curated/minted resolver class had no direct test coverage at all -- only the prose scanner built on top of them was tested. Add per-command assertions for every entry in the glyph map, the plain-character passthrough and unmapped-command cases, and SymbolResolver's curated lookup, first-entry-wins duplicate handling, minting with reuse on repeat lookups, and first-mint ordering.
…ship target remove() previously spliced the p:sld root element itself out of sldIdLst, but sldIdLst holds p:sldId entries referencing each slide by r:id, not the slide part's own root element -- so the splice never matched anything and the presentation kept a dangling reference to the removed slide's part. Resolve the presentation's own relationships and match each p:sldId's r:id target against this slide's part path instead, mirroring the same lookup PptxEditor.removeSlideAt already does by index, then delete the slide part itself from the package. Export PRESENTATION_PART_PATH from scaffold.ts as the single source of truth instead of duplicating the literal in editor.ts.
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.
Working through survived/no-coverage mutants in packages/documents.js toward a genuine 100% mutation score, with no Stryker disable comments anywhere.
Progress tracked in commits as it lands. Will mark ready once the mutation score is 100% with the break threshold raised accordingly.