import assert from 'node:assert/strict'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { JSDOM } from 'jsdom'; const fixtureDir = dirname(fileURLToPath(import.meta.url)); const marker = ''; const markdown = await readFile(resolve(fixtureDir, '../../../docs/routing.md'), 'utf8'); assert.equal(markdown.split(marker).length - 1, 1); const code = markdown.slice(markdown.indexOf(marker) + marker.length) .match(/^\s*```javascript\n([\s\S]*?)\n```/); assert.ok(code, 'the routing example must be a JavaScript module'); await mkdir(resolve(fixtureDir, 'dist'), { recursive: true }); const examplePath = resolve(fixtureDir, 'dist/page-navigation.mjs'); await writeFile(examplePath, code[1]); const bootstrapMarker = ''; const bootstrapMarkdown = await readFile(resolve(fixtureDir, '../../../docs/marionette.application.md'), 'utf8'); assert.equal(bootstrapMarkdown.split(bootstrapMarker).length - 1, 1); const bootstrapCode = bootstrapMarkdown.slice(bootstrapMarkdown.indexOf(bootstrapMarker) + bootstrapMarker.length) .match(/^\s*```javascript\n([\s\S]*?)\n```/); assert.ok(bootstrapCode, 'the bootstrap example must be a JavaScript module'); const bootstrapPath = resolve(fixtureDir, 'dist/application-bootstrap.mjs'); await writeFile(bootstrapPath, bootstrapCode[1]); const dom = new JSDOM('
'); globalThis.window = dom.window; globalThis.document = dom.window.document; const requests = new Map(); const page = title => ({ title, body: `Body of ${title}` }); let application; let bootstrap; try { const { createPageNavigation } = await import(pathToFileURL(examplePath)); const feature = await createPageNavigation({ el: document.querySelector('#page'), loadPage(id, { signal }) { // Intentionally ignores abort: the controller must reject stale commits itself. const request = { signal, ...Promise.withResolvers() }; requests.set(id, request); return request.promise; } }); application = feature.application; const { navigate } = feature; assert.equal(application.isRunning(), true); const first = navigate('first'); requests.get('first').resolve(page('First')); assert.equal(await first, true); const firstView = application.getView(); assert.equal(document.querySelector('h1').textContent, 'First'); const slow = navigate('slow'); const fast = navigate('fast'); assert.equal(requests.get('slow').signal.aborted, true); assert.equal(application.getView(), firstView, 'loading retains the previous page'); requests.get('fast').resolve(page('Fast')); assert.equal(await fast, true); assert.equal(firstView.isDestroyed(), true, 'Region replacement destroys the previous page'); const fastView = application.getView(); requests.get('slow').resolve(page('Stale')); assert.equal(await slow, false); assert.equal(application.getView(), fastView, 'a late response cannot replace the current page'); assert.equal(document.querySelector('h1').textContent, 'Fast'); const old = navigate('old'); const middle = navigate('middle'); requests.get('old').reject(new Error('Old load failed')); assert.equal(await old, false, 'a stale rejection is cancellation'); const newest = navigate('newest'); assert.equal(requests.get('middle').signal.aborted, true, 'the stale finally block must not discard the current cancellation handle'); requests.get('middle').resolve(page('Middle')); requests.get('newest').resolve(page('Newest')); assert.equal(await middle, false); assert.equal(await newest, true); const failed = navigate('failed'); const loadError = new Error('Current load failed'); requests.get('failed').reject(loadError); await assert.rejects(failed, error => error === loadError); assert.equal(document.querySelector('h1').textContent, 'Newest', 'a failed load retains the previous page'); const pendingStop = navigate('pending-stop'); const displayed = application.getView(); assert.equal(await application.stop(), true); assert.equal(requests.get('pending-stop').signal.aborted, true); assert.equal(displayed.isDestroyed(), true); requests.get('pending-stop').resolve(page('Stopped')); assert.equal(await pendingStop, false); assert.equal(document.querySelector('#page').children.length, 0); assert.equal(await navigate('while-stopped'), false); assert.equal(requests.has('while-stopped'), false, 'stopped features do not load'); assert.equal(await application.start(), true); const resumed = navigate('resumed'); requests.get('resumed').resolve(page('Literal title')); assert.equal(await resumed, true, 'a stopped feature can resume navigation'); assert.equal(document.querySelector('h1').textContent, 'Literal title'); assert.equal(document.querySelector('h1 b'), null, 'page data is text, not markup'); const pendingDestroy = navigate('pending-destroy'); const resumedView = application.getView(); assert.equal(await application.destroy(), true); assert.equal(requests.get('pending-destroy').signal.aborted, true); requests.get('pending-destroy').reject(new Error('Late failure after destroy')); assert.equal(await pendingDestroy, false); assert.equal(resumedView.isDestroyed(), true); assert.equal(document.querySelector('#page').children.length, 0); assert.equal(await navigate('after-destroy'), false); assert.equal(requests.has('after-destroy'), false); const { createSessionApplication } = await import(pathToFileURL(bootstrapPath)); const sessions = []; bootstrap = createSessionApplication({ el: document.querySelector('#page'), loadSession({ signal }) { const request = { signal, ...Promise.withResolvers() }; sessions.push(request); return request.promise; } }); const canceledStart = bootstrap.start(); assert.equal(bootstrap.isRunning(), false, 'the feature waits for startup data'); assert.equal(await bootstrap.stop(), true); assert.equal(await canceledStart, false); assert.equal(sessions[0].signal.aborted, true); const currentStart = bootstrap.start(); sessions[1].resolve({ name: 'Current session' }); assert.equal(await currentStart, true); sessions[0].resolve({ name: 'Stale session' }); await Promise.resolve(); assert.equal(bootstrap.session.name, 'Current session', 'canceled startup cannot commit state'); assert.equal(document.querySelector('h1').textContent, 'Current session'); assert.equal(await bootstrap.stop(), true); const failedStart = bootstrap.start(); sessions[2].reject(new Error('Session unavailable')); await assert.rejects(failedStart, /Session unavailable/); assert.equal(bootstrap.isRunning(), false); assert.equal(document.querySelector('#page').children.length, 0); console.log('Routing and bootstrap examples passed: replacement, stale results/errors, readiness, failure, stop/restart, destruction.'); } finally { if (bootstrap && !bootstrap.isDestroyed()) { await bootstrap.destroy(); } if (application && !application.isDestroyed()) { await application.destroy(); } dom.window.close(); delete globalThis.window; delete globalThis.document; }