const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const scheduleCode = fs.readFileSync(path.join(__dirname, '../artwork-schedule.js'), 'utf8'); const artworkCode = fs.readFileSync(path.join(__dirname, '../artwork.js'), 'utf8'); function page({ time = Date.parse('2026-10-07T09:00:00+09:00'), reduced = true, width = 390, height = 480 } = {}) { let now = time; let nextTimer = 0; let balance = 0; let points = []; const timers = new Map(); function node() { const events = {}; const captures = new Set(); return { events, options: [], value: '', dataset: {}, hidden: false, style: { setProperty() {}, removeProperty() {} }, append(child) { this.options.push(child); }, addEventListener(name, fn) { (events[name] ??= []).push(fn); }, emit(name, event = {}) { for (const fn of events[name] ?? []) fn(event); }, focus() {}, setPointerCapture(id) { captures.add(id); }, hasPointerCapture(id) { return captures.has(id); }, releasePointerCapture(id) { captures.delete(id); this.emit('lostpointercapture', { pointerId: id }); } }; } const field = Object.assign(node(), { classList: { add() {} }, getBoundingClientRect: () => ({ width, height, left: 0, top: 250 }) }); const picker = node(); const trigger = node(); const controls = Object.assign(node(), { hidden: true, open: true }); const close = node(); const previous = node(); const next = node(); const random = node(); const nodes = { '.artwork': field, '#artwork-picker': picker, '#developer-trigger': trigger, '.artwork-controls': controls, '#artwork-close': close, '#artwork-previous': previous, '#artwork-next': next, '#artwork-random': random }; const finite = (...values) => values.filter(value => typeof value === 'number') .forEach(value => assert(Number.isFinite(value))); const gradient = { addColorStop(value, color) { assert(value >= 0 && value <= 1); assert(!/NaN|undefined/.test(color)); } }; const context = new Proxy({ clearRect() { points = []; }, save() { balance++; }, restore() { balance--; assert(balance >= 0); }, arc(...args) { finite(...args); assert(args[2] > 0); points.push(args); }, ellipse(...args) { finite(...args); assert(args[2] > 0 && args[3] > 0); }, measureText: () => ({ width: 500 }), createRadialGradient(...args) { finite(...args); return gradient; }, createLinearGradient(...args) { finite(...args); return gradient; } }, { get: (object, key) => object[key] ?? finite }); const document = Object.assign(node(), { hidden: false, documentElement: node(), querySelector(selector) { assert(nodes[selector], selector); return nodes[selector]; }, createElement: tag => tag === 'canvas' ? { getContext: () => context } : node() }); const window = node(); const sandbox = { document, window, Date: { now: () => now }, Math, devicePixelRatio: 2, innerWidth: width, innerHeight: height + 400, performance: { now: () => 0 }, getComputedStyle: () => ({ getPropertyValue: () => '#879098' }), matchMedia: query => ({ matches: query.includes('reduce') && reduced, addEventListener() {} }), requestAnimationFrame: () => 1, setTimeout(fn, delay) { const id = ++nextTimer; timers.set(id, { fn, at: now + delay }); return id; }, clearTimeout(id) { timers.delete(id); }, ResizeObserver: class { constructor(fn) { this.fn = fn; } observe() { this.fn(); } }, IntersectionObserver: class { observe() {} } }; vm.runInNewContext(scheduleCode, sandbox); vm.runInNewContext(artworkCode, sandbox); return { field, picker, trigger, controls, close, previous, next, random, document, window, schedule: sandbox.NNCArtworkSchedule, now: () => now, points: () => JSON.stringify(points), choose(index) { picker.value = String(index); picker.emit('change'); assert.equal(balance, 0); }, advance(duration) { const end = now + duration; while (true) { const entry = [...timers.entries()].filter(([, timer]) => timer.at <= end) .sort((a, b) => a[1].at - b[1].at)[0]; if (!entry) break; now = entry[1].at; timers.delete(entry[0]); entry[1].fn(); } now = end; } }; } function pointer(pointerType = 'mouse', extras = {}) { return { isPrimary: true, button: 0, pointerId: 1, pointerType, clientX: 340, clientY: 900, preventDefault() {}, ...extras }; } test('markup is hidden before scripts run, with a close button and correct script order', () => { const html = fs.readFileSync(path.join(__dirname, '../index.html'), 'utf8'); assert.match(html, /