Skip to content

Commit 5df1bf6

Browse files
committed
fix(documents.js): replace rotationsOf's equivalent kind check with a 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.
1 parent 5b2fae9 commit 5df1bf6

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { ContentVector } from "document-schema.js";
2+
import { describe, expect, it } from "vitest";
3+
import { rotationsOf, withoutRotation } from "./vectors";
4+
5+
const rect: ContentVector = {
6+
kind: "rect",
7+
frame: { xPt: 0, yPt: 0, widthPt: 10, heightPt: 10 },
8+
rotationDeg: 30,
9+
};
10+
11+
const line: ContentVector = {
12+
kind: "line",
13+
from: { xPt: 0, yPt: 0 },
14+
to: { xPt: 10, yPt: 10 },
15+
stroke: { widthPt: 1, color: { r: 0, g: 0, b: 0 } },
16+
};
17+
18+
describe("withoutRotation", () => {
19+
it("strips rotationDeg from a non-line vector", () => {
20+
expect(withoutRotation([rect])).toEqual([
21+
{ ...rect, rotationDeg: undefined },
22+
]);
23+
});
24+
25+
it("leaves a line vector, which has no rotationDeg field at all, untouched -- not spread with an explicit rotationDeg: undefined key added", () => {
26+
// toStrictEqual, not toEqual: toEqual treats an explicit `rotationDeg: undefined` key as indistinguishable from the key being absent altogether, which is exactly the difference this test needs to catch.
27+
expect(withoutRotation([line])).toStrictEqual([line]);
28+
});
29+
});
30+
31+
describe("rotationsOf", () => {
32+
it("reports a non-line vector's own rotationDeg", () => {
33+
expect(rotationsOf([rect])).toEqual([30]);
34+
});
35+
36+
it("reports undefined for a line, positionally", () => {
37+
expect(rotationsOf([rect, line])).toEqual([30, undefined]);
38+
});
39+
});

packages/documents.js/src/test-support/vectors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ export function withoutRotation(
8080
);
8181
}
8282

83-
// The rotations withoutRotation drops, positionally. 'line' has no rotationDeg on ContentVectorSchema at all, so it always reports undefined here.
83+
// The rotations withoutRotation drops, positionally. A property-presence check ("rotationDeg" in vector), not a vector.kind === "line" comparison: 'line' is the only variant lacking rotationDeg, so the two guards narrow identically -- but a kind comparison here is a genuine equivalent-mutant trap TypeScript itself cannot rescue: forcing that comparison's own condition to always-true still type-narrows on the ORIGINAL condition text, so `vector.rotationDeg` stays valid in the branch reached, and a 'line' object with no such key simply reports undefined either way, indistinguishable from the correct branch's own explicit undefined. The `in` check has no such loophole -- Stryker's own typescript-checker rejects an always-true mutation of it outright (accessing rotationDeg on the still-fully-widened union fails to compile), leaving only an always-false mutation, which a real rotationDeg value on a non-line vector does kill.
8484
export function rotationsOf(
8585
vectors: readonly ContentVector[],
8686
): (number | undefined)[] {
8787
return vectors.map((vector) =>
88-
vector.kind === "line" ? undefined : vector.rotationDeg,
88+
"rotationDeg" in vector ? vector.rotationDeg : undefined,
8989
);
9090
}

0 commit comments

Comments
 (0)