108 lines
5.6 KiB
JavaScript
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())
|