Files
vela-salah/tests/page.cjs
T

200 lines
6.7 KiB
JavaScript

// Page wiring tests — extract the <script> from index.ux, run it in a VM, drive update().
'use strict'
const assert = require('assert/strict')
const fs = require('fs')
const vm = require('vm')
const src = fs.readFileSync(__dirname + '/../src/pages/index/index.ux', 'utf8')
const m = src.match(/<script>([\s\S]*?)<\/script>/)
assert.ok(m, 'index.ux has a <script> block')
let body = m[1]
// strip the import (prayer module provided via sandbox), replace `export default` with a spec capture
body = body.replace(/import\s+\w+\s+from\s+'[^']*';?/, '')
body = body.replace(/export\s+default/, 'globalThis.spec =')
// strip comments FIRST so trailing // comments don't pollute const spans
body = body.replace(/\/\/[^\n]*/g, '')
// strip module-level const declarations and re-inject them as sandbox globals
// (skill pitfall: a top-level const breaks the `(body)` wrap).
// replace() can only remove the MATCH span, so rebuild the string manually:
// skip [matchStart, exprEnd) for each `const NAME = <literal>` at line start.
const constDecls = {}
{
const re = /^const\s+(\w+)\s*=\s*/gm
const matches = [...body.matchAll(re)]
let out = ''
let pos = 0
for (const mt of matches) {
const start = mt.index
const name = mt[1]
// scan forward from the match end, balancing (), [], {} and strings
let i = start + mt[0].length
const opens = { '(': ')', '[': ']', '{': '}' }
const stack = []
let inStr = null
while (i < body.length) {
const c = body[i]
if (inStr) {
if (c === '\\') { i += 2; continue }
if (c === inStr) inStr = null
} else if (c === "'" || c === '"' || c === '`') {
inStr = c
} else if (opens[c]) {
stack.push(opens[c])
} else if (stack.length && c === stack[stack.length - 1]) {
stack.pop()
if (!stack.length) { i++; break }
} else if (!stack.length && (c === ';' || c === '\n')) {
break
}
i++
}
const expr = body.slice(start + mt[0].length, i).replace(/[;\n]+$/, '').trim()
try {
constDecls[name] = vm.runInNewContext('(' + expr + ')', {})
} catch (e) {
constDecls[name] = undefined
}
// also skip a trailing comment if the literal ended mid-line before one
let end = i
const semi = body.indexOf('//', i)
const nl = body.indexOf('\n', i)
if (semi !== -1 && nl !== -1 && semi < nl) end = nl
else if (end < body.length && body[end] !== '\n') end = nl === -1 ? body.length : nl
out += body.slice(pos, start)
pos = end
}
body = out + body.slice(pos)
}
const sandbox = {
require,
console,
prayer: require('../src/common/prayer.js'),
globalThis: {},
setInterval: () => 0,
clearInterval: () => {},
setTimeout: () => 0,
clearTimeout: () => {},
...constDecls
}
sandbox.globalThis = sandbox
vm.createContext(sandbox)
vm.runInContext('(' + body + ')', sandbox)
const spec = sandbox.spec
assert.ok(spec && spec.private, 'spec captured with private state')
// page() helper: clone spec + private state into a fresh object, wire method contexts
function page() {
const p = {}
Object.assign(p, JSON.parse(JSON.stringify(spec.private)))
p.onInit = spec.onInit
p.onShow = spec.onShow
p.onHide = spec.onHide
p.onDestroy = spec.onDestroy
p.update = spec.update
p.applyFocus = spec.applyFocus
p.onScroll = spec.onScroll
p.rows = []
return p
}
let passed = 0
function test(name, fn) {
try { fn(); passed++; console.log(' ok -', name) }
catch (e) { console.error(' FAIL -', name, '\n ', e.message); process.exitCode = 1 }
}
// Label map: labels must exist for every prayer key (sandbox global, captured at load)
const LABELS = sandbox.LABELS || spec.LABELS
console.log('page.cjs tests')
test('private state has all bound fields (clock, countdown, nextKey, nextName, rows)', () => {
for (const f of ['clock', 'countdown', 'nextKey', 'nextName', 'rows']) {
assert.ok(f in spec.private, f + ' in private')
}
})
test('LABELS covers every prayer key + no extras', () => {
assert.deepEqual(Object.keys(LABELS), ['fajr', 'sunrise', 'dhuhr', 'asr', 'maghrib', 'isha'])
})
test('template binds: every {{ }} expression field is in private or LABELS-adjacent fields', () => {
const tpl = fs.readFileSync(__dirname + '/../src/pages/index/index.ux', 'utf8').match(/<template>([\s\S]*?)<\/template>/)[1]
const binds = [...tpl.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)].map((x) => x[1])
for (const b of binds) {
const root = b.split('.')[0]
assert.ok(root in spec.private || ['isDay', 'row'].includes(root), 'template bind {{ ' + b + ' }} has a private root (' + root + ')')
}
})
test('update() populates clock as HH:MM', () => {
const p = page()
p.update()
assert.match(p.clock, /^\d{2}:\d{2}$/)
})
test('update() rows: six entries, key+label+time wired', () => {
const p = page()
p.update()
assert.equal(p.rows.length, 6)
const t = require('../src/common/prayer.js').times(new Date(), 33.5731, -7.5898, 1)
for (const row of p.rows) {
assert.ok(row.key in LABELS, 'row key ' + row.key)
assert.equal(row.label, LABELS[row.key])
assert.match(row.time, /^\d{2}:\d{2}$/)
assert.equal(typeof row.passed, 'boolean')
assert.ok(row.rowStyle && /^\d+px$/.test(row.rowStyle.width), 'rowStyle.width set (' + JSON.stringify(row.rowStyle) + ')')
}
})
test('all rows fit the scroll viewport', () => {
const widths = sandbox.ROW_WIDTHS
assert.equal(widths.length, 6)
widths.forEach(width => assert.equal(width, 320))
})
test('bound rowStyle width equals ROW_WIDTHS[i]', () => {
const ROW_WIDTHS = spec.ROW_WIDTHS || sandbox.ROW_WIDTHS
const p = page()
p.update()
p.rows.forEach((row, i) => {
assert.equal(row.rowStyle.width, ROW_WIDTHS[i] + 'px')
})
})
test('update() nextKey/nextName agree with nextPrayer', () => {
const p = page()
p.update()
const now = new Date()
const t = require('../src/common/prayer.js').times(now, 33.5731, -7.5898, 1)
const next = require('../src/common/prayer.js').nextPrayer(t, now.getHours() * 60 + now.getMinutes())
assert.equal(p.nextKey, next.key)
assert.equal(p.nextName, LABELS[next.key])
})
test('countdown is minutes/hours format or +1d wrap', () => {
const p = page()
p.update()
assert.match(p.countdown, /^(\+1d )?(\d+m|\d+h \d+m)$/)
})
test('rowNext highlight is set on exactly one row (the next prayer)', () => {
const p = page()
p.update()
const nextRows = p.rows.filter((r) => r.key === p.nextKey)
assert.equal(nextRows.length, 1, 'exactly one row matches nextKey')
})
test('update is idempotent-ish: two calls keep rows length 6 and consistent next', () => {
const p = page()
p.update()
p.update()
assert.equal(p.rows.length, 6)
assert.ok(p.nextKey in LABELS)
})
console.log(process.exitCode ? 'FAILED' : 'ALL PASSED (' + passed + ')')