Skip to content

Commit 0a115a1

Browse files
Goosterhofclaude
andcommitted
refactor: make dialog v-model props reactive for testability
Replace stored VNodes with render functions in DialogEntry. VNodes are static snapshots — storing them prevents Vue's reactivity from detecting prop mutations. Render functions create fresh VNodes each render cycle, allowing reactive() on the prepared props object to trigger re-renders when v-model values change. This makes prepareVModelProps mutations observable through normal Vue rendering, killing 5 Stryker mutants. Dialog mutation score: 78% → 85%. Aggregate: 91.6% → 93.4%. Tests migrated from shallowMount to mount — render functions create Suspense during rendering which shallowMount would stub. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d690c6e commit 0a115a1

2 files changed

Lines changed: 72 additions & 67 deletions

File tree

packages/dialog/src/index.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Component, VNode } from "vue";
22
import type { ComponentProps } from "vue-component-type-helpers";
33

4-
import { Suspense, defineComponent, h, markRaw, onErrorCaptured, ref } from "vue";
4+
import { Suspense, defineComponent, h, markRaw, onErrorCaptured, reactive, ref } from "vue";
55

66
type UnregisterMiddleware = () => void;
77

@@ -21,7 +21,7 @@ export interface DialogService {
2121
}
2222

2323
interface DialogEntry {
24-
node: VNode;
24+
render: () => VNode;
2525
key: string;
2626
}
2727

@@ -31,7 +31,7 @@ const prepareVModelProps = (
3131
props: Record<string, unknown>,
3232
onClose: () => void,
3333
): Record<string, unknown> => {
34-
const prepared: Record<string, unknown> = { ...props, onClose };
34+
const prepared: Record<string, unknown> = reactive({ ...props, onClose });
3535

3636
for (const key of Object.keys(prepared)) {
3737
if (!key.startsWith("onUpdate:")) continue;
@@ -86,29 +86,30 @@ export const createDialogService = (): DialogService => {
8686
const onClose = () => closeFrom(index);
8787
const prepared = prepareVModelProps(props as Record<string, unknown>, onClose);
8888

89-
const node = h(
90-
"dialog",
91-
{
92-
key,
93-
style: DIALOG_STYLE,
94-
onCancel: (event: Event) => event.preventDefault(),
95-
onClick: (event: MouseEvent) => {
96-
if ((event.target as HTMLElement).tagName === "DIALOG") {
97-
onClose();
98-
}
89+
const render = () =>
90+
h(
91+
"dialog",
92+
{
93+
key,
94+
style: DIALOG_STYLE,
95+
onCancel: (event: Event) => event.preventDefault(),
96+
onClick: (event: MouseEvent) => {
97+
if ((event.target as HTMLElement).tagName === "DIALOG") {
98+
onClose();
99+
}
100+
},
101+
onVnodeMounted: (vnode: VNode) => {
102+
const el = vnode.el as HTMLDialogElement | null;
103+
el?.showModal();
104+
},
99105
},
100-
onVnodeMounted: (vnode: VNode) => {
101-
const el = vnode.el as HTMLDialogElement | null;
102-
el?.showModal();
103-
},
104-
},
105-
h(Suspense, null, {
106-
default: () => h(rawComponent, prepared),
107-
fallback: () => null,
108-
}),
109-
);
110-
111-
dialogs.value.push({ node, key });
106+
h(Suspense, null, {
107+
default: () => h(rawComponent, prepared),
108+
fallback: () => null,
109+
}),
110+
);
111+
112+
dialogs.value.push({ render, key });
112113
updateBodyScroll();
113114
};
114115

@@ -137,7 +138,7 @@ export const createDialogService = (): DialogService => {
137138
setup() {
138139
onErrorCaptured((error) => handleError(error));
139140

140-
return () => dialogs.value.map((dialog) => dialog.node);
141+
return () => dialogs.value.map((dialog) => dialog.render());
141142
},
142143
});
143144

0 commit comments

Comments
 (0)