Files
archy/scripts/test-app-starter-component.cjs

156 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
// Source-only DOM regressions. No browser, server, build, network or app lifecycle.
// Usage: node scripts/test-app-starter-component.cjs /path/to/frontend/node_modules [App.vue]
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const { createRequire } = require('node:module')
const { createHash } = require('node:crypto')
const deps = path.resolve(process.argv[2] || 'neode-ui/node_modules')
const requireDep = createRequire(path.join(deps, '.starter-test.cjs'))
const { JSDOM } = requireDep('jsdom')
const dom = new JSDOM('<div id="app"></div>', { url: 'https://starter.invalid/' })
for (const key of ['window', 'document', 'HTMLElement', 'SVGElement', 'Element', 'Node']) {
global[key] = key === 'window' ? dom.window : dom.window[key]
}
const { parse, compileScript } = requireDep('@vue/compiler-sfc')
const ts = requireDep('typescript')
const { createApp, nextTick } = requireDep('vue')
const source = process.argv[3] ? path.resolve(process.argv[3])
: path.resolve(__dirname, '../examples/archipelago-app-starter/src/App.vue')
const input = fs.readFileSync(source, 'utf8')
const parsed = parse(input, { filename: source })
assert.deepEqual(parsed.errors, [])
const compiled = compileScript(parsed.descriptor, { id: 'starter-test', inlineTemplate: true })
const transformed = ts.transpileModule(compiled.content, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
reportDiagnostics: true,
})
assert.deepEqual(transformed.diagnostics, [])
const component = { exports: {} }
const localRequire = specifier => specifier.startsWith('.')
? require(path.resolve(path.dirname(source), specifier)) : requireDep(specifier)
new Function('require', 'module', 'exports', transformed.outputText)(localRequire, component, component.exports)
const { STORAGE_KEY } = require(path.join(path.dirname(source), 'items.mjs'))
const root = document.getElementById('app')
let app
let passed = 0
async function mount(raw = null) {
if (app) app.unmount()
window.localStorage.clear()
if (raw !== null) window.localStorage.setItem(STORAGE_KEY, raw)
app = createApp(component.exports.default)
app.mount(root)
await nextTick()
}
const button = label => [...root.querySelectorAll('button')].find(node => node.textContent.trim() === label)
async function click(label) {
const node = button(label)
assert.ok(node, `Missing button ${label}`)
node.focus()
node.click()
await nextTick()
await nextTick()
}
async function check(name, run) {
await run()
passed += 1
console.log(`PASS ${name}`)
}
async function main() {
await check('empty add persists and transfers focus from removed empty action', async () => {
await mount()
assert.match(root.textContent, /No items yet/)
await click('Create first item')
assert.equal(root.querySelectorAll('li').length, 1)
assert.equal(document.activeElement, button('Add item'))
assert.equal(JSON.parse(window.localStorage.getItem(STORAGE_KEY))[0].id, 1)
})
await check('remount restores actual local data', async () => {
const saved = window.localStorage.getItem(STORAGE_KEY)
await mount(saved)
assert.equal(root.querySelectorAll('li').length, 1)
assert.match(root.textContent, /Item 1/)
})
await check('mounted add action retains keyboard focus', async () => {
await click('Add item')
assert.equal(root.querySelectorAll('li').length, 2)
assert.equal(document.activeElement, button('Add item'))
})
await check('invalid data shows alert without resetting; retry restores focus', async () => {
await mount('broken')
assert.ok(root.querySelector('[role="alert"]'))
await click('Try again')
assert.equal(window.localStorage.getItem(STORAGE_KEY), 'broken')
assert.equal(document.activeElement, button('Try again'))
window.localStorage.removeItem(STORAGE_KEY)
await click('Try again')
assert.match(root.textContent, /No items yet/)
assert.equal(document.activeElement, button('Add item'))
})
await check('capacity and exhausted ID refusals keep readable records visible', async () => {
for (const records of [
Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, name: 'Keep', detail: '' })),
[{ id: Number.MAX_SAFE_INTEGER, name: 'Last', detail: '' }],
]) {
const raw = JSON.stringify(records)
await mount(raw)
await click('Add item')
assert.equal(root.querySelectorAll('li').length, records.length)
assert.match(root.querySelector('[role="alert"]').textContent, /1000|exhausted/)
assert.equal(window.localStorage.getItem(STORAGE_KEY), raw)
assert.equal(document.activeElement, button('Add item'))
}
})
await check('quota failure preserves empty/populated data and mounted action focus', async () => {
for (const raw of [null, JSON.stringify([{ id: 7, name: 'Keep', detail: 'Existing data' }])]) {
await mount(raw)
const proto = Object.getPrototypeOf(window.localStorage)
const original = proto.setItem
proto.setItem = () => { throw Error('quota') }
const action = raw === null ? 'Create first item' : 'Add item'
try {
await click(action)
assert.match(root.querySelector('[role="alert"]').textContent, /quota/)
assert.equal(root.querySelectorAll('li').length, raw === null ? 0 : 1)
assert.equal(window.localStorage.getItem(STORAGE_KEY), raw)
assert.equal(document.activeElement, button(action))
} finally { proto.setItem = original }
}
})
await check('unfocused activation does not steal focus', async () => {
await mount()
button('Refresh').focus()
button('Create first item').click()
await nextTick()
await nextTick()
assert.equal(document.activeElement, button('Refresh'))
})
await check('focus moved before restoration is retained', async () => {
await mount()
const trigger = button('Create first item')
trigger.focus()
trigger.click()
button('Refresh').focus()
await nextTick()
await nextTick()
assert.equal(document.activeElement, button('Refresh'))
})
await check('blocked storage renders error without uncaught lifecycle error', async () => {
const descriptor = Object.getOwnPropertyDescriptor(window, 'localStorage')
if (app) { app.unmount(); app = null }
Object.defineProperty(window, 'localStorage', { configurable: true, get() { throw Error('blocked') } })
try {
app = createApp(component.exports.default)
app.mount(root)
await nextTick()
assert.ok(root.querySelector('[role="alert"]'))
} finally { Object.defineProperty(window, 'localStorage', descriptor) }
})
console.log(JSON.stringify({ tests: passed, app_sha256: createHash('sha256').update(input).digest('hex'), dependencies: Object.fromEntries(['vue', '@vue/compiler-sfc', 'typescript', 'jsdom'].map(name => [name, requireDep(`${name}/package.json`).version])), scope: 'SFC compilation and jsdom runtime only; no typecheck, browser/layout, package build or UAT acceptance' }))
}
main().catch(error => { console.error(error); process.exitCode = 1 }).finally(() => {
if (app) app.unmount()
dom.window.close()
})