The Wall's and the template's proofs become guards: four two-sided headless checks, in the plugins' own scripts/
Until now these ran from _qa-cycle, so they proved a landing once and guarded nothing after it. Each now lives with its plugin, locates the repo from its own path, and fails on its known-bad:
- wall-of-people-groups/scripts/check-wall-wysiwyg.mjs: the screen poster and the print are one layout on every preset (sheets, 2,107 card rects to 0.002 in, name size and wrap, reference size, chrome blocks). Planted: a card moved 0.01 in, a name a point larger, the footer missing on screen. About 0.6 s.
- wall-of-people-groups/scripts/check-wall-print-watch.mjs: the HD print counts its photos while they load, counts the ones that could not load, finishes at load or the 10-minute ceiling, and ends when its frame is gone. Planted copies of printWatch.js now go to the OS temp folder, never into the tree. About 0.3 s.
- wall-of-people-groups/scripts/check-wall-no-bg-photos.mjs + fixtures/pray-list-sample.json: the real store (through Vite) never requests a photo on its own, a group with no photo URL gets none, and one with a URL keeps it. Planted: the old background test, added through the store's $onAction. The store from before L12 (d9e9aa448) fails both arms: 12 photo requests in 3.5 s and 3 groups on the 404 placeholder. The fixture is a trimmed sample of the live list: 200 rows (120 with has_photo, 77 without, 3 synthetic with no image_url), only the fields the check reads, 66 KB. About 10 s.
- template-bundle/scripts/check-template-i18n.mjs: profile B renders from template.json in en, ar and zh, one sentence in each language's own order with both <code> islands, and no raw key. Planted: the English catalogue served as Arabic. About 4 s.
All four are headless: no browser seat.
This commit is contained in:
parent
a4622781a2
commit
f50b641b28
5 changed files with 319 additions and 0 deletions
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-template-i18n — the starter template renders its text from the catalogue. Headless (SSR).
|
||||
*
|
||||
* template-bundle is what every new plugin copies, so it has to SHOW the i18n pattern, not just pass
|
||||
* it. Profile B is rendered through Vue's server renderer with the REAL template.json catalogue in en,
|
||||
* ar and zh, and must:
|
||||
* - take its heading, its note line and its sentence from the catalogue of the page's language;
|
||||
* - render "Served from {file} in the {bundle} bundle." as ONE key, in that language's own word order
|
||||
* (zh puts the bundle first), with the file and bundle names as two <code dir="ltr"> islands;
|
||||
* - show no raw key and no {placeholder}.
|
||||
* Two-sided: the same render in ar with the ENGLISH catalogue served as Arabic (a catalogue someone
|
||||
* forgot to translate, or a component that ignores the locale) must fail.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createServer } from 'vite'
|
||||
import { createSSRApp, ref } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const cat = (lc) => ({ template: JSON.parse(readFileSync(join(ROOT, `library/i18n/locales/${lc}/template.json`), 'utf8')) })
|
||||
|
||||
const server = await createServer({ root: ROOT, configFile: join(ROOT, 'vite.config.js'), server: { middlewareMode: true, hmr: false, ws: false }, appType: 'custom', logLevel: 'error', optimizeDeps: { noDiscovery: true, include: [] } })
|
||||
|
||||
/** Render profile B in `lc`, serving `served` as that locale's catalogue; return what is wrong. */
|
||||
async function check(B, lc, served, noteGiven) {
|
||||
const i18n = createI18n({ legacy: false, locale: lc, fallbackLocale: 'en', messages: { en: cat('en'), [lc]: served } })
|
||||
const app = createSSRApp(B); app.use(i18n); app.provide('profileConfig', ref(noteGiven ? { note: noteGiven } : {}))
|
||||
const html = await renderToString(app)
|
||||
const want = cat(lc).template.profileB
|
||||
const text = html.replace(/<[^>]+>/g, '')
|
||||
const errs = []
|
||||
if (!html.includes(want.title)) errs.push('the heading is not the catalogue\'s')
|
||||
if ((html.match(/<code dir="ltr"[^>]*>/g) || []).length !== 2) errs.push('the sentence does not keep its two <code dir="ltr"> islands')
|
||||
if (!html.includes('>template-map-b.vue<') || !html.includes('>template-bundle<')) errs.push('the islands do not carry the file and bundle names')
|
||||
const sentence = want.servedFrom.replace('{file}', 'template-map-b.vue').replace('{bundle}', 'template-bundle')
|
||||
if (!text.includes(sentence)) errs.push(`the sentence does not read in the catalogue's order ("${sentence}")`)
|
||||
if (!(noteGiven ? text.includes(want.note.replace('{note}', noteGiven)) : text.includes(want.noNote))) errs.push('the note line is not the catalogue\'s')
|
||||
if (/\{\w+\}|template\.profileB/.test(text)) errs.push('a raw placeholder or key shows')
|
||||
return { errs, text: text.replace(/\s+/g, ' ').trim() }
|
||||
}
|
||||
|
||||
const fails = []
|
||||
try {
|
||||
const B = (await server.ssrLoadModule('/plugin-designs/template-bundle/template-map-b.vue')).default
|
||||
for (const [lc, note] of [['en', ''], ['ar', 'مرحبا'], ['zh', 'hello']]) {
|
||||
const { errs, text } = await check(B, lc, cat(lc), note)
|
||||
console.log(` ${errs.length ? 'FAIL' : 'ok '} ${lc}: ${text.slice(0, 120)}`)
|
||||
fails.push(...errs.map((e) => `${lc}: ${e}`))
|
||||
}
|
||||
const planted = await check(B, 'ar', cat('en'), '')
|
||||
if (!planted.errs.length) fails.push('SELF-TEST: the English catalogue served as Arabic was not caught')
|
||||
} catch (e) { fails.push('could not render: ' + e.message) }
|
||||
await server.close()
|
||||
|
||||
if (fails.length) { console.log('TEMPLATE-I18N: FAIL'); for (const f of fails) console.log(' - ' + f); process.exit(1) }
|
||||
console.log('TEMPLATE-I18N: PASS — profile B renders from template.json in en, ar and zh, its sentence in each language\'s own order with both <code> islands; the English catalogue served as Arabic is caught.')
|
||||
process.exit(0)
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-wall-no-bg-photos — the Wall's store never downloads photos on its own. Headless.
|
||||
*
|
||||
* Until 2026-09-29 the store "tested" every photo after loading the list: 932 photos, 132 MB, on every
|
||||
* visit, whether or not anyone zoomed in (rega measured 108–132 image requests in the first 60 s; 0
|
||||
* after the fix). The canvas loads only the photos in view, so the store must load none. This runs the
|
||||
* REAL store (through Vite, so @map resolves) against a trimmed copy of the live list
|
||||
* (fixtures/pray-list-sample.json), with fetch stubbed to serve it and Image stubbed to COUNT every
|
||||
* photo request, and asserts:
|
||||
* - no photo is requested by the store, before or after its list has loaded;
|
||||
* - a group with no photo URL gets none (not the stock "no image" photo, which answers 404);
|
||||
* - a group with a photo URL keeps it.
|
||||
* Two-sided: the same run with a planted background photo test (the old behaviour, added through the
|
||||
* store's own $onAction hook) must fail.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createServer } from 'vite'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const LIST = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'fixtures/pray-list-sample.json'), 'utf8'))
|
||||
const WAIT_MS = 3500 // the old test's first batches: 3 photos at once, 3 more each second
|
||||
|
||||
// ── the stubs ──────────────────────────────────────────────────────────────────────────────────────
|
||||
const photoRequests = []
|
||||
globalThis.Image = class { constructor() { this._src = '' }
|
||||
set src(u) { this._src = u; if (u) { photoRequests.push(u); setTimeout(() => this.onload && this.onload(), 5) } }
|
||||
get src() { return this._src } }
|
||||
const mem = new Map()
|
||||
globalThis.localStorage = { getItem: (k) => (mem.has(k) ? mem.get(k) : null), setItem: (k, v) => mem.set(k, String(v)), removeItem: (k) => mem.delete(k) }
|
||||
globalThis.fetch = async (url) => {
|
||||
if (/\/api\/people-groups\/list/.test(String(url))) return { ok: true, status: 200, headers: { get: () => 'application/json' }, json: async () => LIST }
|
||||
return { ok: false, status: 404, headers: { get: () => 'text/html' }, json: async () => ({}), text: async () => '' }
|
||||
}
|
||||
|
||||
const server = await createServer({ root: ROOT, configFile: join(ROOT, 'vite.config.js'), server: { middlewareMode: true, hmr: false, ws: false }, appType: 'custom', logLevel: 'error', optimizeDeps: { noDiscovery: true, include: [] } })
|
||||
const { createPinia } = await server.ssrLoadModule('pinia')
|
||||
const { usePeopleGroupsStore } = await server.ssrLoadModule('/plugin-designs/wall-of-people-groups/src/stores/peopleGroupsStore.js')
|
||||
|
||||
/** One run of the real store. `plant` may add the old background test through $onAction. */
|
||||
async function run(plant = false) {
|
||||
photoRequests.length = 0
|
||||
const store = usePeopleGroupsStore(createPinia())
|
||||
store.profileConfig = {}
|
||||
if (plant) {
|
||||
store.$onAction(({ name, after }) => {
|
||||
if (name === 'fetchFromPrayAPI') after(() => { for (const g of store.peopleGroups.slice(0, 3)) if (g.imageUrl) new Image().src = g.imageUrl })
|
||||
})
|
||||
}
|
||||
await store.fetchPeopleGroups()
|
||||
await new Promise((r) => setTimeout(r, WAIT_MS))
|
||||
const groups = store.peopleGroups
|
||||
const errs = []
|
||||
if (!groups.length) errs.push('the store loaded no groups (the stub or the fixture is wrong, so nothing else here means anything)')
|
||||
if (photoRequests.length) errs.push(`the store requested ${photoRequests.length} photo(s) on its own; the canvas loads only what is in view`)
|
||||
const noUrl = groups.filter((g) => !g.image_url)
|
||||
if (!noUrl.length) errs.push('the fixture has no group without a photo URL, so that arm cannot run')
|
||||
const bad = noUrl.filter((g) => g.imageUrl)
|
||||
if (bad.length) errs.push(`${bad.length} group(s) with no photo URL carry one (e.g. ${bad[0].imageUrl}), so the print shows a broken image instead of its grey box`)
|
||||
const lost = groups.filter((g) => g.image_url && g.imageUrl !== g.image_url)
|
||||
if (lost.length) errs.push(`${lost.length} group(s) with a photo lost their photo URL`)
|
||||
return { errs, groups: groups.length, noUrl: noUrl.length }
|
||||
}
|
||||
|
||||
const fails = []
|
||||
let real = null
|
||||
try {
|
||||
real = await run(false)
|
||||
fails.push(...real.errs)
|
||||
const planted = await run(true)
|
||||
if (!planted.errs.some((e) => /requested \d+ photo/.test(e))) fails.push('SELF-TEST: a planted background photo test was not caught')
|
||||
} catch (e) { fails.push('could not run the store: ' + e.message) }
|
||||
await server.close()
|
||||
|
||||
if (fails.length) { console.log('WALL-NO-BG-PHOTOS: FAIL'); for (const f of fails) console.log(' - ' + f); process.exit(1) }
|
||||
console.log(`WALL-NO-BG-PHOTOS: PASS — the real store loads ${real.groups} groups and requests 0 photos in ${WAIT_MS} ms; the ${real.noUrl} groups without a photo URL get none; a planted background photo test is caught.`)
|
||||
process.exit(0)
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-wall-print-watch — the Wall's HD poster print never prints silently incomplete. Headless.
|
||||
*
|
||||
* The print frame prints on its own 'load', which waits for every photo (library printInPlace.js);
|
||||
* src/printWatch.js reports while it waits. This asserts, with fake timers and a fake frame whose images
|
||||
* load over fake time, that the watch:
|
||||
* - counts the photos as they load (the button's "Preparing the poster: n of 2,107 photos");
|
||||
* - counts the ones that could not load (complete, no pixels), which the toast then names;
|
||||
* - finishes exactly once, at 'load', or at the 10-minute ceiling for a load that never ends;
|
||||
* - ends at once when the frame is gone, so the busy button never outlives it.
|
||||
* Two-sided: each rule is removed from a copy of printWatch.js (written to the OS temp folder, never into
|
||||
* the tree), and every copy must fail at least one arm.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { readFileSync, writeFileSync, rmSync, mkdtempSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const SRC = join(ROOT, 'plugin-designs/wall-of-people-groups/src/printWatch.js')
|
||||
const TMP = mkdtempSync(join(tmpdir(), 'wall-print-watch-'))
|
||||
|
||||
// fake timers
|
||||
let now = 0, seq = 0
|
||||
const timers = new Map()
|
||||
globalThis.setTimeout = (fn, ms = 0) => { const id = ++seq; timers.set(id, { at: now + ms, fn }); return id }
|
||||
globalThis.clearTimeout = (id) => timers.delete(id)
|
||||
globalThis.setInterval = (fn, ms) => { const id = ++seq; const t = { at: now + ms, fn, every: ms }; timers.set(id, t); return id }
|
||||
globalThis.clearInterval = (id) => timers.delete(id)
|
||||
function advanceTo(t) {
|
||||
for (;;) {
|
||||
let next = null
|
||||
for (const [id, x] of timers) if (x.at <= t && (!next || x.at < next[1].at)) next = [id, x]
|
||||
if (!next) break
|
||||
const [id, x] = next; now = x.at
|
||||
if (x.every) x.at += x.every; else timers.delete(id)
|
||||
x.fn()
|
||||
}
|
||||
now = t
|
||||
}
|
||||
function fakeWin(n) {
|
||||
const images = Array.from({ length: n }, () => ({ complete: false, naturalWidth: 0 }))
|
||||
const on = {}
|
||||
return { images, document: { images }, addEventListener: (type, fn) => { on[type] = fn }, fire: (type) => on[type] && on[type]() }
|
||||
}
|
||||
const settle = (win, k, fail = 0) => { for (let i = 0; i < k; i++) { win.images[i].complete = true; win.images[i].naturalWidth = i < fail ? 0 : 320 } }
|
||||
|
||||
async function arms(mod) {
|
||||
const out = []
|
||||
const ok = (c, what) => { if (!c) out.push(what) }
|
||||
// A. all load: progress climbs, onDone once at 'load' with 0 failed
|
||||
{ now = 0; timers.clear(); const w = fakeWin(10), seen = [], done = []
|
||||
mod.watchPrintFrame(w, { onProgress: (c) => seen.push(c.n), onDone: (c) => done.push(c) })
|
||||
settle(w, 4); advanceTo(600); settle(w, 10); advanceTo(1200); w.fire('load'); advanceTo(700000)
|
||||
ok(seen[0] === 0 && seen.includes(4) && seen.includes(10), `progress counts the photos as they load (saw ${seen.join(',')})`)
|
||||
ok(done.length === 1 && done[0] && done[0].n === 10 && done[0].total === 10 && done[0].failed === 0, 'onDone once at load: 10 of 10, 0 failed') }
|
||||
// B. some fail: complete with no pixels are counted as failed
|
||||
{ now = 0; timers.clear(); const w = fakeWin(10), done = []
|
||||
mod.watchPrintFrame(w, { onDone: (c) => done.push(c) })
|
||||
settle(w, 10, 3); advanceTo(1000); w.fire('load')
|
||||
ok(done.length === 1 && done[0] && done[0].failed === 3, `3 photos that could not load are counted (got ${done[0] && done[0].failed})`) }
|
||||
// C. a load that never ends: onDone once, at the ceiling
|
||||
{ now = 0; timers.clear(); const w = fakeWin(10), done = []
|
||||
mod.watchPrintFrame(w, { onDone: (c) => done.push([now, c]) })
|
||||
settle(w, 6); advanceTo(599999)
|
||||
ok(done.length === 0, 'no onDone before the ceiling while the frame is still loading')
|
||||
advanceTo(600000)
|
||||
ok(done.length === 1 && done[0][0] === 600000 && done[0][1].n === 6, 'onDone once at the 10-minute ceiling, with the count so far') }
|
||||
// D. the frame is removed before its load: the watch ends at the next tick, not at the ceiling
|
||||
{ now = 0; timers.clear(); const w = fakeWin(10), done = []
|
||||
mod.watchPrintFrame(w, { onDone: (c) => done.push([now, c]) })
|
||||
advanceTo(60000); w.closed = true; advanceTo(61000)
|
||||
ok(done.length === 1 && done[0][0] <= 61000 && done[0][1] === null, `a frame that is gone ends the watch within a tick (ended at ${done[0] ? done[0][0] : 'never'})`) }
|
||||
return out
|
||||
}
|
||||
|
||||
let fails = []
|
||||
const real = await import(pathToFileURL(SRC).href)
|
||||
fails = await arms(real)
|
||||
for (const f of fails) console.log(' FAIL ' + f)
|
||||
|
||||
// planted defects: each must make at least one arm fail
|
||||
const src = readFileSync(SRC, 'utf8')
|
||||
const PLANTS = [
|
||||
['failed photos are not counted', 'if (!im.naturalWidth) failed++', ''],
|
||||
['no ceiling: a load that never ends never finishes', 'cap = setTimeout(() => finish(countImages(win)), ceilingMs)', 'cap = null'],
|
||||
['no progress while loading', 'iv = setInterval(tick, every)', 'iv = null'],
|
||||
['a removed frame holds the watch until the ceiling', 'if (win.closed) return finish(null); ', ''],
|
||||
]
|
||||
let planted = 0
|
||||
for (const [name, from, to] of PLANTS) {
|
||||
if (!src.includes(from)) { fails.push(`SELF-TEST: the planted defect "${name}" no longer matches the source`); continue }
|
||||
const p = join(TMP, `printWatch.planted-${planted}.mjs`) // printWatch.js imports nothing, so it runs from anywhere
|
||||
writeFileSync(p, src.replace(from, to))
|
||||
try { const bad = await arms(await import(pathToFileURL(p).href)); if (!bad.length) fails.push(`SELF-TEST: the planted defect "${name}" was not caught`); else planted++ }
|
||||
finally { rmSync(p, { force: true }) }
|
||||
}
|
||||
rmSync(TMP, { recursive: true, force: true })
|
||||
if (fails.length) { console.log(`WALL-PRINT-WATCH: FAIL — ${fails.length}`); for (const f of fails) console.log(' - ' + f); process.exit(1) }
|
||||
console.log(`WALL-PRINT-WATCH: PASS — the print counts its photos while they load, counts the ones that could not load, and finishes at load or at the ceiling; ${planted} planted defects each caught.`)
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-wall-wysiwyg — the Wall's screen IS its poster. Headless.
|
||||
*
|
||||
* The Bless view draws the poster on screen (screenPoster) and prints it (buildBlessPosterHtml) from one
|
||||
* layout (blessPoster.js). This asserts they stay one, for every preset: the same number of sheets, the
|
||||
* same 2,107 card rects (to 0.002 in), the same per-card name size and wrap (cardText), the same verse
|
||||
* reference size, the same chrome blocks (banner, themes, footer, journey), and sheets at the preset's
|
||||
* aspect. A change to either path that is not made to both fails here.
|
||||
*
|
||||
* Two-sided: each arm is also run against a planted defect (one screen card moved 0.01 in, one name
|
||||
* printed a point larger, one chrome block missing on screen), and each must be caught.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const m = await import(pathToFileURL(join(ROOT, 'plugin-designs/wall-of-people-groups/src/blessPoster.js')).href)
|
||||
const names = Array.from({ length: 2107 }, (_, i) => (i % 97 === 0 ? 'Han Chinese, Mandarin-speaking lowlands ' + i : i % 13 === 0 ? 'Accukattavandlu' : 'Group ' + i))
|
||||
const groups = names.map((name) => ({ name }))
|
||||
const CARD_RE = /<div class="card" style="left:([\d.]+)in;top:([\d.]+)in;width:([\d.]+)in;height:([\d.]+)in"><div class="nm( w2)?" dir="auto" style="font-size:([\d.]+)pt(?:;-webkit-line-clamp:\d)?">/g
|
||||
|
||||
/** Every way the screen and the print disagree for one preset. `plant` may damage the screen side. */
|
||||
function compare(id, plant = null) {
|
||||
const html = m.buildBlessPosterHtml(groups, id, {})
|
||||
const sp = m.screenPoster(groups, id, {})
|
||||
let cardText = m.cardText
|
||||
if (plant) ({ cardText = m.cardText } = plant(sp) || {})
|
||||
const P = sp.plan.preset, px = sp.pxPerIn, step = P.sheetH + m.SCREEN_GAP_IN
|
||||
const cards = [...html.matchAll(CARD_RE)]
|
||||
const pages = (html.match(/<section class="sheet">/g) || []).length
|
||||
const errs = []
|
||||
if (pages !== P.sheets || sp.sheets.length !== P.sheets) errs.push(`sheets: print ${pages}, screen ${sp.sheets.length}`)
|
||||
if (cards.length !== groups.length || sp.cards.length !== groups.length) errs.push(`cards: print ${cards.length}, screen ${sp.cards.length}`)
|
||||
for (const R of sp.sheets) if (Math.abs(R.w / R.h - P.sheetW / P.sheetH) > 1e-9) errs.push('a screen sheet is not at the preset\'s aspect')
|
||||
sp.cards.forEach((c, k) => {
|
||||
const pc = sp.plan.cards[k], h = cards[k]
|
||||
if (!h) return
|
||||
const ct = cardText(sp.plan, names[c.i])
|
||||
const d = Math.max(Math.abs(c.x / px - +h[1]), Math.abs(c.y / px - (pc.s * step + +h[2])), Math.abs(c.w / px - +h[3]), Math.abs(c.h / px - +h[4]))
|
||||
if (d > 0.002) errs.push(`card ${c.i}: rect off by ${d.toFixed(4)} in`)
|
||||
if (+h[6] !== ct.name.pt || !!h[5] !== ct.name.wrap) errs.push(`card ${c.i}: name prints ${h[6]} pt${h[5] || ''}, screen says ${ct.name.pt} pt${ct.name.wrap ? ' w2' : ''}`)
|
||||
})
|
||||
const ref = /\.card \.rf\{font-size:([\d.]+)pt/.exec(html)
|
||||
if (!ref || +ref[1] !== cardText(sp.plan, '').refPt) errs.push('the verse reference size differs')
|
||||
for (const k of ['class="ban"', 'class="themes"', 'class="ftr"', 'class="jny"']) {
|
||||
if (html.includes(k) !== sp.chromeHtml.includes(k)) errs.push(`chrome block ${k} is on one side only`)
|
||||
}
|
||||
return { errs, P, sp }
|
||||
}
|
||||
|
||||
let fails = 0
|
||||
for (const id of Object.keys(m.PRESETS)) {
|
||||
const { errs, P, sp } = compare(id)
|
||||
console.log(` ${errs.length ? 'FAIL' : 'ok '} ${id}: ${P.sheets} sheet(s) of ${P.sheetW}×${P.sheetH} in, ${groups.length} cards at the same rects and sizes; screen world ${sp.worldW}×${sp.worldH} px${errs.length ? '\n ' + errs.slice(0, 5).join('\n ') : ''}`)
|
||||
fails += errs.length ? 1 : 0
|
||||
}
|
||||
|
||||
// Planted defects: each must make the comparison fail.
|
||||
const id0 = Object.keys(m.PRESETS)[0]
|
||||
const PLANTS = [
|
||||
['one screen card moved 0.01 in', (sp) => { sp.cards[5].x += 0.01 * sp.pxPerIn }],
|
||||
['one name a point larger on screen', () => ({ cardText: (plan, name) => { const t = m.cardText(plan, name); return name === names[7] ? { ...t, name: { ...t.name, pt: t.name.pt + 1 } } : t } })],
|
||||
['the footer missing on screen', (sp) => { sp.chromeHtml = sp.chromeHtml.replace('class="ftr"', 'class="xx"') }],
|
||||
]
|
||||
let caught = 0
|
||||
for (const [what, plant] of PLANTS) {
|
||||
if (compare(id0, plant).errs.length) caught++
|
||||
else { console.log(` FAIL SELF-TEST: a planted defect was not caught (${what})`); fails++ }
|
||||
}
|
||||
|
||||
if (fails) { console.log(`WALL-WYSIWYG: FAIL — ${fails} problem(s)`); process.exit(1) }
|
||||
console.log(`WALL-WYSIWYG: PASS — screen and print are one layout on every preset (${Object.keys(m.PRESETS).join(', ')}); ${caught} planted defects each caught.`)
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue