Skip to content

Commit de5fd39

Browse files
Goosterhofclaude
andcommitted
fix: per-package mutation testing and improve dialog/translation scores
Migrate Stryker from aggregate root config to per-package configs so each package is individually accountable to the 90% threshold. Improve mutation scores: dialog 84.81%→97.06% (killed 10 mutants via tests + dead code removal), translation 86.67%→93.33% (killed 2 mutants via targeted test). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0a115a1 commit de5fd39

24 files changed

Lines changed: 257 additions & 35 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dist/
33
coverage/
44
reports/
55
.stryker-tmp/
6+
.stryker-incremental.json
67
*.tsbuildinfo
78
.changeset/*.md
89
!.changeset/config.json

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "npm run build --workspaces",
99
"test": "vitest run",
1010
"test:coverage": "vitest run --coverage",
11-
"test:mutation": "stryker run",
11+
"test:mutation": "npm run test:mutation --workspaces",
1212
"lint": "oxlint .",
1313
"typecheck": "npm run typecheck --workspaces",
1414
"lint:pkg": "npm run lint:pkg --workspaces",

packages/adapter-store/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"typecheck": "tsc --noEmit",
3737
"lint:pkg": "publint && attw --pack",
3838
"test": "vitest run",
39-
"test:coverage": "vitest run --coverage"
39+
"test:coverage": "vitest run --coverage",
40+
"test:mutation": "stryker run"
4041
},
4142
"devDependencies": {
4243
"@script-development/fs-helpers": "^0.1.0",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
2+
export default {
3+
testRunner: "vitest",
4+
vitest: {
5+
configFile: "vitest.config.ts",
6+
},
7+
mutate: ["src/**/*.ts", "!src/**/types.ts"],
8+
thresholds: {
9+
high: 95,
10+
low: 90,
11+
break: 90,
12+
},
13+
reporters: ["clear-text", "progress"],
14+
incremental: true,
15+
incrementalFile: ".stryker-incremental.json",
16+
cleanTempDir: "always",
17+
};

packages/dialog/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"typecheck": "tsc --noEmit",
3737
"lint:pkg": "publint && attw --pack",
3838
"test": "vitest run",
39-
"test:coverage": "vitest run --coverage"
39+
"test:coverage": "vitest run --coverage",
40+
"test:mutation": "stryker run"
4041
},
4142
"dependencies": {
4243
"vue-component-type-helpers": "^2.0.0"

packages/dialog/src/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ export const createDialogService = (): DialogService => {
6767
};
6868

6969
const closeFrom = (index: number) => {
70-
if (index < 0 || index >= dialogs.value.length) return;
71-
7270
dialogs.value.splice(index);
7371
updateBodyScroll();
7472
};
@@ -99,13 +97,11 @@ export const createDialogService = (): DialogService => {
9997
}
10098
},
10199
onVnodeMounted: (vnode: VNode) => {
102-
const el = vnode.el as HTMLDialogElement | null;
103-
el?.showModal();
100+
(vnode.el as HTMLDialogElement).showModal();
104101
},
105102
},
106103
h(Suspense, null, {
107104
default: () => h(rawComponent, prepared),
108-
fallback: () => null,
109105
}),
110106
);
111107

packages/dialog/stryker.config.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
2+
export default {
3+
testRunner: "vitest",
4+
vitest: {
5+
configFile: "vitest.config.ts",
6+
},
7+
mutate: ["src/**/*.ts", "!src/**/types.ts"],
8+
thresholds: {
9+
high: 95,
10+
low: 90,
11+
break: 90,
12+
},
13+
reporters: ["clear-text", "progress"],
14+
incremental: true,
15+
incrementalFile: ".stryker-incremental.json",
16+
cleanTempDir: "always",
17+
};

packages/dialog/tests/dialog.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,70 @@ describe("dialog service", () => {
573573
expect(handler).not.toHaveBeenCalled();
574574
});
575575

576+
it("should not corrupt other handlers when the same handler is unregistered twice", async () => {
577+
// Arrange — register two handlers, unregister the second twice.
578+
// With a faulty splice guard, the second unregister would splice(-1, 1)
579+
// which removes the LAST element (handlerA), corrupting the chain.
580+
const handlerA = vi.fn(() => false);
581+
const handlerB = vi.fn(() => false);
582+
const ErrorComponent = defineComponent({
583+
props: { onClose: Function },
584+
setup() {
585+
throw new Error("Corruption test");
586+
},
587+
render() {
588+
return h("div");
589+
},
590+
});
591+
592+
const service = createDialogService();
593+
service.registerErrorMiddleware(handlerA);
594+
const unregisterB = service.registerErrorMiddleware(handlerB);
595+
596+
// Act — unregister B twice; the second call should be a no-op
597+
unregisterB();
598+
unregisterB();
599+
600+
mount(service.DialogContainerComponent);
601+
service.open(ErrorComponent, {});
602+
await nextTick();
603+
await nextTick();
604+
605+
// Assert — handlerA must still be in the chain
606+
expect(handlerA).toHaveBeenCalled();
607+
});
608+
609+
it("should propagate non-Error values to the app error handler", async () => {
610+
// Arrange — non-Error values must propagate (return true from handleError).
611+
// If handleError returned false for non-Errors, Vue would swallow them.
612+
const handler = vi.fn(() => false);
613+
const StringErrorComponent = defineComponent({
614+
props: { onClose: Function },
615+
setup() {
616+
// eslint-disable-next-line @typescript-eslint/only-throw-error, no-throw-literal
617+
throw "string error";
618+
},
619+
render() {
620+
return h("div");
621+
},
622+
});
623+
624+
const service = createDialogService();
625+
service.registerErrorMiddleware(handler);
626+
const appErrorHandler = vi.fn();
627+
mount(service.DialogContainerComponent, {
628+
global: { config: { errorHandler: appErrorHandler } },
629+
});
630+
631+
// Act
632+
service.open(StringErrorComponent, {});
633+
await nextTick();
634+
await nextTick();
635+
636+
// Assert — error propagated past onErrorCaptured to the app-level handler
637+
expect(appErrorHandler).toHaveBeenCalled();
638+
});
639+
576640
it("should not call middleware after it has been unregistered", async () => {
577641
// Arrange
578642
const handler = vi.fn(() => false);

packages/helpers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"typecheck": "tsc --noEmit",
3737
"lint:pkg": "publint && attw --pack",
3838
"test": "vitest run",
39-
"test:coverage": "vitest run --coverage"
39+
"test:coverage": "vitest run --coverage",
40+
"test:mutation": "stryker run"
4041
},
4142
"dependencies": {
4243
"string-ts": "^2.3.1"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
2+
export default {
3+
testRunner: "vitest",
4+
vitest: {
5+
configFile: "vitest.config.ts",
6+
},
7+
mutate: ["src/**/*.ts", "!src/**/types.ts"],
8+
thresholds: {
9+
high: 95,
10+
low: 90,
11+
break: 90,
12+
},
13+
reporters: ["clear-text", "progress"],
14+
incremental: true,
15+
incrementalFile: ".stryker-incremental.json",
16+
cleanTempDir: "always",
17+
};

0 commit comments

Comments
 (0)