import assert from 'node:assert/strict'; import { test } from 'node:test'; import { JSDOM } from 'jsdom'; import { Application } from 'marionette'; import { Collection } from '@mnjs/data'; import { createLatestRequest } from './dist/latest-request.js'; import { createResultsFeature } from './dist/results-feature.js'; await test('documented refresh uses one active session and replaces only current data', async t => { const dom = new JSDOM('
'); globalThis.window = dom.window; globalThis.document = dom.window.document; async function fixture(beforeStop) { const items = new Collection([{ id: 1, name: 'First' }]); const requests = []; const feature = await createResultsFeature({ el: document.querySelector('main'), items, beforeStop, loadItems(query, { signal }) { const request = { query, signal, ...Promise.withResolvers() }; requests.push(request); return request.promise; } }); return { ...feature, items, requests, async destroy() { requests.forEach(request => request.resolve([])); await feature.application.destroy(); items.destroy(); } }; } try { await t.test('refresh retains layout, editor, collection, model, and surviving row identities', async() => { const f = await fixture(); try { const layout = f.application.getView(); const row = document.querySelector('li'); const editor = document.querySelector('textarea'); const model = f.items.get(1); editor.value = 'Unsaved draft'; const slow = f.refresh('slow'); const fast = f.refresh('fast'); assert.equal(f.requests[0].signal.aborted, true); assert.equal(document.querySelector('li'), row); f.requests[1].resolve([{ id: 1, name: 'Updated' }, { id: 2, name: 'Second' }]); assert.equal(await fast, true); f.requests[0].resolve([{ id: 3, name: 'Stale' }]); assert.equal(await slow, false); assert.equal(f.application.getView(), layout); assert.equal(f.items.get(1), model); assert.equal(document.querySelector('li'), row); assert.equal(row.textContent, 'Updated'); assert.equal(row.querySelector('b'), null); assert.equal(document.querySelector('textarea'), editor); assert.equal(editor.value, 'Unsaved draft'); assert.equal(f.application.isRunning(), true); const secondRow = document.querySelectorAll('li')[1]; const reordered = f.refresh('reordered'); f.requests[2].resolve([{ id: 2, name: 'Second' }, { id: 1, name: 'Moved' }]); assert.equal(await reordered, true); assert.deepEqual([...document.querySelectorAll('li')], [secondRow, row]); const removed = f.refresh('removed'); f.requests[3].resolve([{ id: 1, name: 'Survivor' }]); assert.equal(await removed, true); assert.equal(secondRow.isConnected, false); assert.equal(document.querySelector('li'), row); assert.equal(f.items.get(1), model); } finally { await f.destroy(); } }); await t.test('old errors and finally cannot overwrite data or release a newer cancellation handle', async() => { const f = await fixture(); try { const old = f.refresh('old'); const middle = f.refresh('middle'); f.requests[0].reject(new Error('Obsolete failure')); assert.equal(await old, false); const newest = f.refresh('newest'); assert.equal(f.requests[1].signal.aborted, true); f.requests[1].resolve([{ id: 2, name: 'Middle' }]); f.requests[2].resolve([{ id: 1, name: 'Newest' }]); assert.equal(await middle, false); assert.equal(await newest, true); assert.equal(document.querySelector('li').textContent, 'Newest'); } finally { await f.destroy(); } }); await t.test('current load failures reject, preserve displayed data, and permit retry', async() => { const f = await fixture(); try { const failed = f.refresh('failed'); const error = new Error('Try again'); f.requests[0].reject(error); await assert.rejects(failed, failure => failure === error); assert.equal(document.querySelector('li').textContent, 'First'); const retry = f.refresh('retry'); f.requests[1].resolve([{ id: 1, name: 'Recovered' }]); assert.equal(await retry, true); assert.equal(document.querySelector('li').textContent, 'Recovered'); } finally { await f.destroy(); } }); await t.test('feature cancellation retains display and permits a new refresh', async() => { const f = await fixture(); try { const row = document.querySelector('li'); const canceled = f.refresh('canceled'); f.cancel(); assert.equal(f.requests[0].signal.aborted, true); f.requests[0].resolve([{ id: 1, name: 'Canceled' }]); assert.equal(await canceled, false); assert.equal(document.querySelector('li'), row); assert.equal(row.textContent, 'First'); const retry = f.refresh('retry'); f.requests[1].resolve([{ id: 1, name: 'Retried' }]); assert.equal(await retry, true); assert.equal(row.textContent, 'Retried'); } finally { await f.destroy(); } }); await t.test('independent controllers do not cancel each other', async() => { const loads = []; const commits = []; const makeController = () => createLatestRequest({ load(input, { signal }) { const request = { signal, ...Promise.withResolvers() }; loads.push(request); return request.promise; }, commit: value => commits.push(value) }); const list = makeController(); const detail = makeController(); try { const listRequest = list.run('list'); const detailRequest = detail.run('detail'); list.dispose(); assert.equal(loads[0].signal.aborted, true); assert.equal(loads[1].signal.aborted, false); loads[0].resolve('list'); loads[1].resolve('detail'); assert.equal(await listRequest, false); assert.equal(await detailRequest, true); assert.deepEqual(commits, ['detail']); } finally { list.dispose(); detail.dispose(); loads.forEach(load => load.resolve()); } }); await t.test('stop permission keeps requests active until successful deactivation', async() => { const permission = Promise.withResolvers(); let hold = true; const f = await fixture(() => hold ? permission.promise : undefined); try { const loading = f.refresh('during-permission'); const stopping = f.application.stop().then(value => ({ value }), error => ({ error })); f.requests[0].resolve([{ id: 1, name: 'Still active' }]); assert.equal(await loading, true); assert.equal(document.querySelector('li').textContent, 'Still active'); const denied = new Error('Keep editing'); permission.reject(denied); assert.equal((await stopping).error, denied); assert.equal(f.application.isRunning(), true); const pending = f.refresh('before-stop'); hold = false; assert.equal(await f.application.stop(), true); assert.equal(f.requests[1].signal.aborted, true); assert.equal(await f.refresh('stopped'), false); assert.equal(f.requests.length, 2); assert.equal(await f.application.start(), true); const resumed = f.refresh('resumed'); f.requests[2].resolve([{ id: 1, name: 'New session' }]); assert.equal(await resumed, true); f.requests[1].resolve([{ id: 1, name: 'Obsolete session' }]); assert.equal(await pending, false); assert.equal(f.items.get(1).get('name'), 'New session'); } finally { hold = false; permission.resolve(); await f.destroy(); } }); await t.test('start superseding pending stop releases the previous request controller', async() => { const permission = Promise.withResolvers(); const f = await fixture(() => permission.promise); try { const old = f.refresh('old-session'); const stopping = f.application.stop(); const starting = f.application.start(); assert.equal(await stopping, false); permission.resolve(); assert.equal(await starting, true); assert.equal(f.requests[0].signal.aborted, true); const current = f.refresh('current-session'); f.requests[1].resolve([{ id: 1, name: 'Current session' }]); assert.equal(await current, true); f.requests[0].resolve([{ id: 1, name: 'Obsolete session' }]); assert.equal(await old, false); assert.equal(document.querySelector('li').textContent, 'Current session'); } finally { permission.resolve(); await f.destroy(); } }); await t.test('owner stop and direct destroy cancel work without disposing borrowed data', async() => { const f = await fixture(); const owner = new Application(); owner.addChildApp('results', f.application); try { const pending = f.refresh('owned'); await owner.stop(); assert.equal(f.requests[0].signal.aborted, true); f.requests[0].resolve([{ id: 1, name: 'Too late' }]); assert.equal(await pending, false); assert.equal(document.querySelector('main').children.length, 0); await f.application.start(); const destroyed = f.refresh('destroyed'); await f.application.destroy(); assert.equal(f.requests[1].signal.aborted, true); f.requests[1].reject(new Error('Late failure')); assert.equal(await destroyed, false); assert.equal(await f.refresh('after-destroy'), false); assert.equal(f.items.get(1).get('name'), 'First'); f.items.get(1).set({ name: 'Borrower remains live' }); assert.equal(f.items.get(1).get('name'), 'Borrower remains live'); } finally { await owner.destroy(); await f.destroy(); } }); await t.test('cancellation releases external listeners before a non-cooperative loader settles', async() => { const external = new AbortController(); const listeners = new Set(); const signal = external.signal; const add = signal.addEventListener.bind(signal); const remove = signal.removeEventListener.bind(signal); signal.addEventListener = (name, callback, options) => { listeners.add(callback); add(name, callback, options); }; signal.removeEventListener = (name, callback) => { listeners.delete(callback); remove(name, callback); }; const loads = []; const commits = []; const requests = createLatestRequest({ load(input, context) { const load = { ...context, ...Promise.withResolvers() }; loads.push(load); return load.promise; }, commit: value => commits.push(value) }); try { const first = requests.run('first', { signal }); assert.equal(listeners.size, 1); const aborted = new AbortController(); aborted.abort(); assert.equal(await requests.run('ignored', { signal: aborted.signal }), false); assert.equal(loads[0].signal.aborted, false); const second = requests.run('second'); assert.equal(listeners.size, 0); assert.equal(loads[0].signal.aborted, true); requests.cancel(); loads[0].resolve('old'); loads[1].resolve('canceled'); assert.equal(await first, false); assert.equal(await second, false); assert.deepEqual(commits, []); const third = requests.run('third', { signal }); external.abort(); assert.equal(listeners.size, 0); loads[2].resolve('externally canceled'); assert.equal(await third, false); requests.dispose(); requests.dispose(); assert.equal(await requests.run('disposed'), false); assert.equal(loads.length, 3); } finally { requests.dispose(); loads.forEach(load => load.resolve()); } }); await t.test('current synchronous commit failures reject without a recovery contract', async() => { const error = new Error('Commit failed'); const requests = createLatestRequest({ load: async() => 'value', commit() { throw error; } }); try { await assert.rejects(requests.run('input'), failure => failure === error); } finally { requests.dispose(); } }); } finally { dom.window.close(); delete globalThis.window; delete globalThis.document; } });