diff --git a/internal/check-poster-wall-host-seams.mjs b/internal/check-poster-wall-host-seams.mjs index 177266a80..28a1d2f2c 100644 --- a/internal/check-poster-wall-host-seams.mjs +++ b/internal/check-poster-wall-host-seams.mjs @@ -109,6 +109,10 @@ const builtScriptUrl = { // in the build. The gate runs Vite without the Vue plugin, so without this hook a child .vue import fails to LOAD and // the gate goes red on correct code. Arm L proves the hook with a parent/child fixture. const CLIENT = '?gate-client' +// --self-test: a file's source replaced in memory (a mutant), so an arm can be shown to FAIL on broken code. +const SOURCE_OVERRIDES = new Map() +const SELF_TEST = process.argv.includes('--self-test') +const SECTION = join(ROOT, 'library/components/poster/PosterSection.vue') const clientCompile = { name: 'gate-client-sfc', // 'pre': Vite's own resolver would otherwise resolve a relative './Child.vue' first and load it raw. @@ -124,7 +128,7 @@ const clientCompile = { if (!id.endsWith(CLIENT)) return null const file = id.slice(0, -CLIENT.length) const sfc = await import('@vue/compiler-sfc') - const { descriptor, errors } = sfc.parse(readFileSync(file, 'utf8'), { filename: file }) + const { descriptor, errors } = sfc.parse(SOURCE_OVERRIDES.get(file) ?? readFileSync(file, 'utf8'), { filename: file }) if (errors.length) throw new Error(`${relative(ROOT, file)} did not parse: ${errors[0].message}`) return sfc.compileScript(descriptor, { id: `gate-${basename(file, '.vue')}`, inlineTemplate: true }).content }, @@ -174,7 +178,9 @@ try { parentNode: (n) => n.parent, nextSibling: (n) => { const p = n.parent; return p ? p.children[p.children.indexOf(n) + 1] || null : null }, patchProp: (el, key, _prev, next) => { el.props[key] = key === 'class' ? classStr(next) : next }, }) - const PosterWall = (await server.ssrLoadModule('virtual:poster-wall-client')).default + let PosterWall = (await server.ssrLoadModule('virtual:poster-wall-client')).default + // Reload PosterWall after SOURCE_OVERRIDES changed (the module graph caches the compiled files). + async function reloadWall() { server.moduleGraph.invalidateAll(); PosterWall = (await server.ssrLoadModule('virtual:poster-wall-client')).default } const item = { id: 'gate-item', label: 'Gate Item', count: 7 } async function mountWall(extra, items = [item]) { @@ -390,23 +396,52 @@ try { app.unmount() } - // M: captionUnderPreview. OFF (the default): a drawn card shows only its picture, as every wall always has. ON: a - // pending card is not captioned (its placeholder already names it), and the drawn card names itself under the picture. - { + // M: captionUnderPreview. OFF (the default): a drawn card shows only its picture and its frame has no has-caption, + // as every wall always has. ON: a pending card is not captioned (its placeholder already names it), and the drawn card + // names itself under the picture in a has-caption frame. A function, so --self-test can run it against mutants. + async function armM() { + const fs = [] const items = [{ id: 'm1', label: 'Mone', count: 1 }] const ps = () => Promise.resolve('blob:gate-m') for (const on of [false, true]) { - const w = await mountWall({ previewSrc: ps, instanceId: `gate-m-${on ? 'on' : 'off'}`, ...(on ? { captionUnderPreview: true } : {}) }, items) + const w = await mountWall({ previewSrc: ps, instanceId: `gate-m-${on ? 'on' : 'off'}-${Math.random()}`, ...(on ? { captionUnderPreview: true } : {}) }, items) await settle() - const caps = () => { const out = []; walk(w.root, (n) => { if (hasClass(n, 'cap')) out.push(n) }); return out } - if (caps().length) fail(`M: a PENDING card must not be captioned (${on ? 'on' : 'off'})`) + const all = (cls) => { const out = []; walk(w.root, (n) => { if (hasClass(n, cls)) out.push(n) }); return out } + if (all('cap').length) fs.push(`M: a PENDING card must not be captioned (${on ? 'on' : 'off'})`) + if (all('has-caption').length) fs.push(`M: a PENDING card's frame must not be has-caption (${on ? 'on' : 'off'})`) const ph = phFor(w.root, 'Mone'); if (ph) inView(ph) await settle(); await settle() - if (imgs(w.root).length !== 1) fail(`M: the card should be drawn (${on ? 'on' : 'off'}), got ${imgs(w.root).length} img`) - if (!on && caps().length) fail('M: with captionUnderPreview OFF (the default) a drawn card must show no caption') - if (on && (caps().length !== 1 || textOf(caps()[0]).trim() !== 'Mone')) fail(`M: with captionUnderPreview ON the drawn card should be captioned "Mone", got ${JSON.stringify(caps().map((c) => textOf(c)))}`) + if (imgs(w.root).length !== 1) fs.push(`M: the card should be drawn (${on ? 'on' : 'off'}), got ${imgs(w.root).length} img`) + if (!on && all('cap').length) fs.push('M: with captionUnderPreview OFF (the default) a drawn card must show no caption') + if (!on && all('has-caption').length) fs.push('M: with captionUnderPreview OFF (the default) no frame may be has-caption') + if (on && (all('cap').length !== 1 || textOf(all('cap')[0]).trim() !== 'Mone')) fs.push(`M: with captionUnderPreview ON the drawn card should be captioned "Mone", got ${JSON.stringify(all('cap').map((c) => textOf(c)))}`) + if (on && all('has-caption').length !== 1) fs.push(`M: with captionUnderPreview ON the drawn card's frame should be has-caption, got ${all('has-caption').length}`) w.app.unmount() } + return fs + } + ;(await armM()).forEach(fail) + + if (SELF_TEST) { + const real = readFileSync(SECTION, 'utf8') + const mutate = (from, to) => { if (!real.includes(from)) throw new Error(`self-test: PosterSection.vue no longer contains ${JSON.stringify(from)}; update the mutant`); return real.replace(from, to) } + const mutants = [ + ['a caption shown with the option off', mutate('captionUnderPreview.value === true && ', '')], + ['a caption never shown', mutate('