import { test } from 'brittle'
import { readFileSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { parse as acornParse } from '../vendor/acorn/dist/acorn.mjs'
const HERE = dirname(fileURLToPath(import.meta.url))
const LIB = resolve(HERE, '../lib')
// The floor view is served as real files, so nothing lints or type-checks it and
// a stray brace ships a white page. Same gate as render.test.js, same reason.
test('floor view: level.js parses as a module', (t) => {
const src = readFileSync(resolve(LIB, 'floor/level.js'), 'utf-8')
try {
acornParse(src, { ecmaVersion: 'latest', sourceType: 'module' })
t.pass('parses')
} catch (err) {
t.fail('level.js is not valid JS: ' + err.message)
}
})
test('floor view: no duplicate top-level function declarations', (t) => {
const src = readFileSync(resolve(LIB, 'floor/level.js'), 'utf-8')
const ast = acornParse(src, { ecmaVersion: 'latest', sourceType: 'module' })
const seen = new Set()
const dupes = []
for (const node of ast.body) {
if (node.type !== 'FunctionDeclaration' || !node.id) continue
if (seen.has(node.id.name)) dupes.push(node.id.name)
seen.add(node.id.name)
}
// A second declaration silently replaces the first for the whole module —
// exactly the escapeHtml bug that shipped in render.js.
t.alike(dupes, [], 'no name declared twice: ' + dupes.join(', '))
})
test('floor view: every function it calls is declared or imported', (t) => {
// A parse gate cannot catch a MISSING function — the file still parses fine.
// This test exists because a scripted extraction removed `load` and
// `currentSymbol` along with the functions it was told to cut, and the page
// would have shipped broken with every other gate green.
const src = readFileSync(resolve(LIB, 'floor/level.js'), 'utf-8')
const ast = acornParse(src, { ecmaVersion: 'latest', sourceType: 'module' })
const declared = new Set()
for (const n of ast.body) {
if (n.type === 'FunctionDeclaration') declared.add(n.id.name)
if (n.type === 'VariableDeclaration') {
for (const d of n.declarations) if (d.id.type === 'Identifier') declared.add(d.id.name)
}
if (n.type === 'ImportDeclaration') {
for (const sp of n.specifiers) declared.add(sp.local.name)
}
}
const called = new Set()
const walk = (node) => {
if (!node || typeof node.type !== 'string') return
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
called.add(node.callee.name)
}
for (const k of Object.keys(node)) {
const v = node[k]
if (Array.isArray(v)) v.forEach(walk)
else if (v && typeof v === 'object' && typeof v.type === 'string') walk(v)
}
}
walk(ast)
const builtins = new Set(['fetch', 'parseInt', 'parseFloat', 'String', 'Number',
'Set', 'Map', 'Array', 'Object', 'encodeURIComponent', 'decodeURIComponent', 'isNaN'])
const missing = [...called].filter(n => !declared.has(n) && !builtins.has(n))
t.alike(missing, [], 'nothing calls a function that does not exist: ' + missing.join(', '))
})
test('floor view: the card builders live in a module node can import', (t) => {
const src = readFileSync(resolve(LIB, 'floor/level.js'), 'utf-8')
t.ok(src.includes("from '/floor/cards.js'"), 'the view imports them')
t.absent(/function buildSymbolLevel/.test(src), 'and does not define its own copy')
const server = readFileSync(resolve(LIB, 'server.js'), 'utf-8')
t.ok(server.includes("'/floor/cards.js'"), 'the server routes that path')
})
test('floor view: it imports the real layout module, not a copy', (t) => {
const src = readFileSync(resolve(LIB, 'floor/level.js'), 'utf-8')
t.ok(src.includes("from '/floor/layout.js'"), 'imports the served layout')
const server = readFileSync(resolve(LIB, 'server.js'), 'utf-8')
t.ok(server.includes("'/floor/layout.js'"), 'and the server routes that path')
t.ok(server.includes("sendFile(req, res, 'layout.js'"), 'to lib/layout.js itself')
})
test('floor view: the page loads the script it is served with', (t) => {
const html = readFileSync(resolve(LIB, 'floor/level.html'), 'utf-8')
t.ok(html.includes('src="/floor/level.js"'), 'script tag points at the served path')
t.ok(html.includes('type="module"'), 'as a module, since it imports layout')
t.ok(html.includes('id="world"'), 'and the SVG group the view draws into exists')
})