Files
archy/scripts/test-monitoring-chart.cjs
T

108 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
// Bounded actual-SFC source test; canvas calls are recorded, not real graphics.
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 = createRequire(path.join(path.resolve(process.argv[2] || 'neode-ui/node_modules'), '.chart-test.cjs'))
const { JSDOM } = deps('jsdom')
const dom = new JSDOM('<div id="app"></div>', { url: 'https://chart.invalid/' })
for (const key of ['window', 'document', 'HTMLElement', 'SVGElement', 'Element', 'Node']) global[key] = key === 'window' ? dom.window : dom.window[key]
const calls = []
const setAttribute = dom.window.HTMLCanvasElement.prototype.setAttribute
dom.window.HTMLCanvasElement.prototype.setAttribute = function (name, value) {
if (name === 'width' || name === 'height') calls.push(['dimension', name, value])
return setAttribute.call(this, name, value)
}
for (const name of ['width', 'height']) {
const descriptor = Object.getOwnPropertyDescriptor(dom.window.HTMLCanvasElement.prototype, name)
Object.defineProperty(dom.window.HTMLCanvasElement.prototype, name, {
...descriptor,
set(value) { calls.push(['dimension', name, value]); descriptor.set.call(this, value) },
})
}
const context = Object.fromEntries(['scale', 'clearRect', 'fillText', 'beginPath', 'moveTo', 'lineTo', 'stroke', 'closePath', 'fill'].map(name => [name, (...args) => calls.push([name, ...args])]))
dom.window.HTMLCanvasElement.prototype.getContext = () => context
const { parse, compileScript } = deps('@vue/compiler-sfc')
const ts = deps('typescript')
const { createApp, h, reactive, nextTick } = deps('vue')
const filename = path.resolve(__dirname, '../neode-ui/src/components/LineChart.vue')
const input = fs.readFileSync(filename, 'utf8')
const parsed = parse(input, { filename })
assert.deepEqual(parsed.errors, [])
const compiled = compileScript(parsed.descriptor, { id: 'chart-test', inlineTemplate: true })
const output = ts.transpileModule(compiled.content, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, reportDiagnostics: true })
assert.deepEqual(output.diagnostics, [])
const loaded = { exports: {} }
new Function('require', 'module', 'exports', output.outputText)(deps, loaded, loaded.exports)
let app
let passed = 0
let failed = 0
async function check(name, run) {
calls.length = 0
try { await run(); passed++; console.log(`PASS ${name}`) }
catch (error) { failed++; console.error(`FAIL ${name}`, error.message) }
finally { if (app) { app.unmount(); app = null } }
}
function mount(initial) {
const props = reactive(initial)
app = createApp({ render: () => h(loaded.exports.default, props) })
app.mount(document.getElementById('app'))
return props
}
const dataset = data => ({ label: 'Sample', data, color: '#fb923c' })
async function main() {
await check('empty first series does not hide a later populated series', async () => {
mount({ datasets: [dataset([]), dataset([20, 40])] })
assert.ok(!calls.some(call => call[0] === 'fillText' && call[1] === 'No data yet'))
assert.ok(calls.some(call => call[0] === 'fill'))
})
await check('all empty series show explicit no-data instead of inventing zero', async () => {
mount({ datasets: [dataset([]), dataset([])] })
assert.ok(calls.some(call => call[0] === 'fillText' && call[1] === 'No data yet'))
assert.ok(!calls.some(call => call[0] === 'fill'))
})
await check('zero values remain real data', async () => {
mount({ datasets: [dataset([0, 0])] })
assert.ok(!calls.some(call => call[1] === 'No data yet'))
assert.ok(calls.some(call => call[0] === 'fill'))
})
await check('changing yMax redraws axes and points without a data update', async () => {
const props = mount({ datasets: [dataset([20, 40])], yMax: 100 })
calls.length = 0
props.yMax = 200
await nextTick()
assert.ok(calls.some(call => call[0] === 'clearRect'))
assert.ok(calls.some(call => call[0] === 'fillText' && call[1] === '200'))
})
await check('changing showGrid redraws and removes grid labels', async () => {
const props = mount({ datasets: [dataset([20, 40])], showGrid: true })
calls.length = 0
props.showGrid = false
await nextTick()
assert.ok(calls.some(call => call[0] === 'clearRect'))
assert.ok(!calls.some(call => call[0] === 'fillText'))
})
for (const dpr of [1, 2]) {
for (const dimension of ['width', 'height']) {
await check(`${dimension} update preserves final drawing at DPR ${dpr}`, async () => {
Object.defineProperty(window, 'devicePixelRatio', { value: dpr, configurable: true })
const props = mount({ datasets: [dataset([20, 40])], width: 400, height: 180 })
calls.length = 0
props[dimension] = dimension === 'width' ? 500 : 240
await nextTick()
const canvas = document.querySelector('canvas')
assert.equal(canvas.width, props.width * dpr)
assert.equal(canvas.height, props.height * dpr)
const lastDraw = calls.findLastIndex(call => call[0] === 'fill')
assert.ok(lastDraw >= 0)
assert.ok(!calls.slice(lastDraw + 1).some(call => call[0] === 'dimension'), 'dimension reset cleared the drawing')
})
}
}
console.log(JSON.stringify({ passed, failed, source_sha256: createHash('sha256').update(input).digest('hex'), scope: 'actual Vue SFC/jsdom with recorded canvas calls; not browser paint, live metrics or UAT' }))
if (failed) process.exitCode = 1
}
main().catch(error => { console.error(error); process.exitCode = 1 }).finally(() => dom.window.close())