import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; import vm from 'node:vm'; const root = path.resolve(import.meta.dirname, '..'); class ClassList { constructor() { this.values = new Set(); } add(...names) { names.forEach(name => this.values.add(name)); } remove(...names) { names.forEach(name => this.values.delete(name)); } contains(name) { return this.values.has(name); } } class FakeElement extends EventTarget { constructor(id = '') { super(); this.id = id; this.hidden = false; this.value = 0; this.textContent = ''; this.src = ''; this.async = true; this.classList = new ClassList(); this.attributes = new Map(); this.focused = false; } focus() { this.focused = true; } setAttribute(name, value) { this.attributes.set(name, String(value)); } closest() { return null; } } function pointerEvent() { const event = new Event('pointerup', { cancelable: true }); Object.defineProperties(event, { isPrimary: { value: true }, pointerType: { value: 'touch' }, button: { value: 0 } }); return event; } test('bootstrap remains visible after loading and opens the app only after deliberate activation', async () => { const sourcePath = path.join(root, 'public/js/bootstrap-loader.js'); const source = await fs.readFile(sourcePath, 'utf8'); const ids = [ 'bootstrap-screen', 'bootstrap-progress', 'bootstrap-error', 'bootstrap-retry', 'bootstrap-enter', 'bootstrap-ready', 'storage-cleanup-status', 'app-shell' ]; const elements = new Map(ids.map(id => [id, new FakeElement(id)])); const steps = new Map([ 'jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location' ].map(name => [name, new FakeElement(name)])); const navigations = []; function jquery() { return { button() { return this; } }; } const context = { Wegwichtel: { PrivacyStorage: { async cleanupExpired(callback) { callback(0, 1); callback(1, 1); return { removed: 1, total: 1 }; } }, App: { async initialize({ markStep }) { markStep('server', 'done'); markStep('routes', 'done'); markStep('location', 'done'); }, navigate(...args) { navigations.push(args); } } }, document: { getElementById: id => elements.get(id), querySelector(selector) { const match = selector.match(/^\[data-step="(.+)"\]$/); return match ? steps.get(match[1]) : null; }, querySelectorAll(selector) { if (selector === '#bootstrap-steps .done') { return [...steps.values()].filter(step => step.classList.contains('done')); } return []; }, createElement: () => new FakeElement('script'), head: { appendChild(script) { if (script.src.includes('/jquery-4.0.0.min.js')) { jquery.fn = { jquery: '4.0.0' }; context.jQuery = jquery; } if (script.src.includes('/jquery-ui-1.14.2.min.js')) { jquery.ui = { version: '1.14.2' }; } queueMicrotask(() => script.onload()); } } }, Event, EventTarget, Promise, console, encodeURIComponent, queueMicrotask, location: { reload() {} } }; context.globalThis = context; vm.runInNewContext(source, context, { filename: sourcePath }); await new Promise(resolve => setTimeout(resolve, 20)); assert.equal(elements.get('bootstrap-screen').hidden, false); assert.equal(elements.get('bootstrap-screen').attributes.get('aria-busy'), 'false'); assert.equal(elements.get('bootstrap-enter').hidden, false); assert.equal(elements.get('storage-cleanup-status').textContent, 'Entferne alte Streckendaten aus dem Speicher (1 / 1)'); assert.equal(navigations.length, 0); elements.get('bootstrap-screen').dispatchEvent(pointerEvent()); assert.equal(elements.get('bootstrap-screen').hidden, true); assert.equal(elements.get('app-shell').hidden, false); assert.equal(navigations[0][0], '#routes-page'); assert.equal(navigations[0][1].replace, true); });