310 lines
22 KiB
JavaScript
310 lines
22 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { join } from 'node:path';
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { filesFixture } from './helpers/w15-files.mjs';
|
|
import { setup as conversationFixture, T, S } from './helpers/w15-conversations.mjs';
|
|
import { setup as requestFixture, state, REQUEST } from './helpers/w15-requests.mjs';
|
|
|
|
test('Files presents reviewed delivery and every names-only draft state at both widths', async t => {
|
|
const { f, b, errors } = await filesFixture(t);
|
|
const shots = process.env.MOSAIC_UX_SHOTS;
|
|
const state = { version: 1, revision: 1, phase: 'complete', request: 'request-1', delivery: 'delivery-1', delivery_evidence: { id: 'delivery-1', request: 'request-1', artifact: { path: 'output/timer.html' } } };
|
|
const base = { available: true, entries: [], truncated: false, contentAvailable: false, observedAt: '2026-11-05T00:00:00.000Z' };
|
|
const scenarios = [
|
|
['draft-list', { ...base, entries: [{ name: 'src/timer.js' }, { name: 'README.md' }] }, /Names only.*src\/timer.js.*README.md/s],
|
|
['draft-empty', base, /No draft files found/],
|
|
['draft-unavailable', { ...base, available: false }, /Draft workspace is unavailable/],
|
|
['draft-truncated', { ...base, entries: [{ name: 'src/timer.js' }], truncated: true }, /src\/timer.js.*truncated/s],
|
|
['draft-error', null, /Could not read draft workspace/],
|
|
];
|
|
for (const width of [1440, 390]) {
|
|
await b.viewport(width, 1000);
|
|
await b.navigate(f.base + '/#/');
|
|
await b.evaluate(`(async () => {
|
|
const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = '/files.css'; document.head.append(css);
|
|
await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); });
|
|
window.fixtureState = ${JSON.stringify(state)};
|
|
const main = document.querySelector('#main'); main.replaceChildren(); const mount = document.createElement('div'); mount.id = 'slice-fixture'; main.append(mount);
|
|
})()`);
|
|
for (const [name, workspace, expected] of scenarios) {
|
|
await b.evaluate(`(async () => { window.fixtureWorkspace = ${JSON.stringify(workspace)}; await MosaicFiles.mount({container: document.querySelector('#slice-fixture'), requestIds:['request-1']}); })()`);
|
|
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
|
|
assert.match(text, /Reviewed delivery.*timer.html.*Open as text.*Download.*Product workspace, draft/s);
|
|
assert.match(text, expected);
|
|
assert.equal(await b.evaluate("document.querySelector('#slice-fixture a').getAttribute('href')"), '/api/requests/request-1/artifact/delivery-1/text');
|
|
assert.equal(await b.evaluate("document.querySelector('#slice-fixture a').rel"), 'noopener noreferrer');
|
|
assert.equal(await b.evaluate('document.documentElement.scrollWidth <= innerWidth'), true);
|
|
await b.evaluate("document.querySelector('#slice-fixture a[download]').focus()");
|
|
await b.key('Tab');
|
|
assert.equal(await b.evaluate("document.activeElement.getAttribute('aria-label')"), 'Product workspace, draft');
|
|
await b.key('Tab', 'Tab', 8);
|
|
assert.equal(await b.evaluate("document.activeElement.hasAttribute('download')"), true);
|
|
await b.call('Input.dispatchMouseEvent', { type: 'mousePressed', x: 2, y: 2, button: 'left', clickCount: 1 });
|
|
await b.call('Input.dispatchMouseEvent', { type: 'mouseReleased', x: 2, y: 2, button: 'left', clickCount: 1 });
|
|
if (shots) {
|
|
await b.screenshot(join(shots, `files-${name}-${width}.png`));
|
|
const { cssContentSize } = await b.call('Page.getLayoutMetrics');
|
|
const { data } = await b.call('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true, clip: { x: 0, y: 0, width: cssContentSize.width, height: cssContentSize.height, scale: 1 } });
|
|
await writeFile(join(shots, `files-${name}-${width}-full.png`), Buffer.from(data, 'base64'));
|
|
}
|
|
}
|
|
}
|
|
await b.evaluate(`(async () => { window.fixtureState.request = 'foreign'; await MosaicFiles.mount({container: document.querySelector('#slice-fixture'), requestIds:['request-1']}); })()`);
|
|
assert.equal(await b.evaluate("document.querySelectorAll('#slice-fixture a').length"), 0);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
async function polishFixture(t) {
|
|
const fixture = await filesFixture(t);
|
|
const { f, b } = fixture;
|
|
await b.navigate(f.base + '/#/');
|
|
await b.evaluate(`(async () => {
|
|
await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); });
|
|
const main = document.querySelector('#main');
|
|
const section = document.createElement('section'); section.id = 'files-block';
|
|
const heading = document.createElement('h3'); heading.textContent = 'Files';
|
|
const mount = document.createElement('div'); mount.id = 'slice-fixture'; section.append(heading, mount); main.append(section);
|
|
window.fixtureState = { version: 1, revision: 1, phase: 'complete', request: 'request-1', delivery: 'delivery-1', delivery_evidence: { id: 'delivery-1', request: 'request-1', artifact: { path: 'output/old.html' } } };
|
|
window.fixtureWorkspace = { available: true, entries: [{ name: 'old-draft.txt' }], truncated: false, contentAvailable: false, observedAt: '2026-11-05T00:00:00.000Z' };
|
|
window.fixtureFetch = window.fetch;
|
|
window.replaceFiles = () => { const old = document.querySelector('#slice-fixture'); const next = document.createElement('div'); next.id = old.id; old.replaceWith(next); return next; };
|
|
})()`);
|
|
return fixture;
|
|
}
|
|
|
|
test('Files caller heading is the only Files heading for data and unavailable states', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
|
|
const count = "[...document.querySelectorAll('#files-block h1, #files-block h2, #files-block h3, #files-block h4, #files-block h5, #files-block h6')].filter(h => h.textContent === 'Files').length";
|
|
assert.equal(await b.evaluate(count), 1, 'data state has one Files heading');
|
|
await b.evaluate(`(async () => {
|
|
window.fetch = (url, init) => String(url) === '/api/config' ? Promise.resolve(new Response('{}', {status: 503})) : window.fixtureFetch(url, init);
|
|
await MosaicFiles.mount({container: window.replaceFiles(), requestIds:['request-1']});
|
|
})()`);
|
|
assert.match(await b.evaluate("document.querySelector('#files-block').textContent"), /Files are unavailable\./);
|
|
assert.equal(await b.evaluate(count), 1, 'unavailable state has one Files heading');
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('Files remount retains previous data while refreshing, then replaces it', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
|
|
await b.evaluate(`(() => {
|
|
window.fixtureState.delivery_evidence.artifact.path = 'output/new.html';
|
|
window.fixtureWorkspace.entries = [{name:'new-draft.txt'}];
|
|
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
|
|
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1', 'request-1']});
|
|
})()`);
|
|
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
|
|
assert.match(text, /old\.html/);
|
|
assert.match(text, /old-draft\.txt/);
|
|
assert.doesNotMatch(text, /Reading files…/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
const updated = await b.evaluate("document.querySelector('#slice-fixture').textContent");
|
|
assert.match(updated, /new\.html/);
|
|
assert.match(updated, /new-draft\.txt/);
|
|
assert.doesNotMatch(updated, /old\.html|old-draft\.txt/);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('Files new keys load initially and sign-out prevents cached content', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
|
|
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'new-thread'});
|
|
})()`);
|
|
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /Reading files…/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
await b.evaluate(`(() => { window.pendingBeforeSignout = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'new-thread'}); document.dispatchEvent(new CustomEvent('mosaic-signed-out')); })()`);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingBeforeSignout; })()');
|
|
await b.evaluate(`(() => { window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'new-thread'}); })()`);
|
|
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
|
|
test('Files cache rejects older settlements from detached containers for the same key', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate(`(() => {
|
|
let first = true;
|
|
window.fetch = async (url, init) => {
|
|
if (String(url) === '/api/config' && first) { first = false; await new Promise(resolve => { window.releaseOld = resolve; }); }
|
|
return window.fixtureFetch(url, init);
|
|
};
|
|
window.oldMount = MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'thread-1'});
|
|
})()`);
|
|
await b.evaluate(`(async () => {
|
|
window.fixtureState.delivery_evidence.artifact.path = 'output/newest.html';
|
|
await MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'thread-1'});
|
|
window.fixtureState.delivery_evidence.artifact.path = 'output/stale.html';
|
|
window.releaseOld(); await window.oldMount;
|
|
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
|
|
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'thread-1'});
|
|
})()`);
|
|
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
|
|
assert.match(text, /newest\.html/);
|
|
assert.doesNotMatch(text, /stale\.html|Reading files…/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('Files caller key preserves content as request IDs change', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'thread-1'})");
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
|
|
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1','request-2'], key:'thread-1'});
|
|
})()`);
|
|
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
|
|
test('Files caches the latest completed mount even after its container is detached', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
|
|
window.pendingFiles = MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'detached-thread'});
|
|
window.replaceFiles();
|
|
})()`);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
await b.evaluate("window.pendingFiles = MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:'detached-thread'}); true");
|
|
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html.*old-draft\.txt/s);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('Files ignores invalid caller keys and never restores cache under signed-out body', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1'], key:{}})");
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if (String(url) === '/api/config') await new Promise(resolve => { window.releaseFiles = resolve; }); return window.fixtureFetch(url, init); };
|
|
window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1'], key:'x'.repeat(321)});
|
|
})()`);
|
|
assert.match(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; document.body.classList.add("signed-out"); window.pendingFiles = MosaicFiles.mount({container:window.replaceFiles(), requestIds:["request-1"]}); })()');
|
|
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
|
|
await b.evaluate('(async () => { window.releaseFiles(); await window.pendingFiles; })()');
|
|
assert.doesNotMatch(await b.evaluate("document.querySelector('#slice-fixture').textContent"), /old\.html|old-draft\.txt/);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('project conversations link to Files and legacy chat mounts one Files heading', async t => {
|
|
const { b, render, errors } = await conversationFixture(t);
|
|
await b.evaluate(`(async () => { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); }); })()`);
|
|
await b.evaluate("window.__realFiles = window.MosaicFiles; window.MosaicFiles = {mount(o) { window.__filesKey = o.key; return window.__realFiles.mount(o); }}; true");
|
|
await render(`/projects/threads/${T}/sessions/${S}`);
|
|
assert.equal(await b.evaluate(`!!document.querySelector('.project-tabs a[href$="/files"]') && !document.querySelector('[data-chat-files]')`), true);
|
|
await b.evaluate(`history.replaceState(null,'',${JSON.stringify('#/chat/threads/'+T+'/sessions/'+S)})`);
|
|
await render(`/chat/threads/${T}/sessions/${S}`);
|
|
assert.equal(await b.evaluate('window.__filesKey'), `thread:${T}`);
|
|
await b.evaluate(`(async () => { const end = Date.now() + 5000; while (!document.querySelector('[data-chat-files]')?.textContent.includes('Files are unavailable.')) { if (Date.now() > end) throw Error('Files did not settle'); await new Promise(r => setTimeout(r, 20)); } })()`);
|
|
assert.equal(await b.evaluate("[...document.querySelectorAll('.chat-files h1, .chat-files h2, .chat-files h3, .chat-files h4, .chat-files h5, .chat-files h6')].filter(h => h.textContent === 'Files').length"), 1);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('real request wrapper and Files module render one heading', async t => {
|
|
const { b, navigate, consoleErrors } = await requestFixture(t, {states:{[REQUEST]:state({})}});
|
|
await navigate(1440);
|
|
await b.evaluate(`(async () => { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = '/files.js'; s.onload = resolve; s.onerror = reject; document.head.append(s); }); document.querySelector('[data-request-reload]').click(); })()`);
|
|
await b.evaluate(`(async () => { const end = Date.now() + 5000; while (!document.querySelector('[data-request-files]')?.textContent.includes('Product workspace, draft')) { if (Date.now() > end) throw Error('Files did not settle'); await new Promise(r => setTimeout(r, 20)); } })()`);
|
|
assert.equal(await b.evaluate("[...document.querySelectorAll('.request-files h1, .request-files h2, .request-files h3, .request-files h4, .request-files h5, .request-files h6')].filter(h => h.textContent === 'Files').length"), 1);
|
|
assert.deepEqual(consoleErrors, []);
|
|
});
|
|
|
|
|
|
test('Files keeps settled read errors visible on refresh', async t => {
|
|
const { b, errors } = await polishFixture(t);
|
|
await b.evaluate("MosaicFiles.mount({container:document.querySelector('#slice-fixture'), requestIds:['request-1']})");
|
|
await b.evaluate(`(async () => {
|
|
window.fetch = (url, init) => String(url).startsWith('/api/requests/') ? Promise.resolve(new Response('{}', {status:503})) : window.fixtureFetch(url, init);
|
|
await MosaicFiles.mount({container:window.replaceFiles(), requestIds:['request-1']});
|
|
})()`);
|
|
const text = await b.evaluate("document.querySelector('#slice-fixture').textContent");
|
|
assert.match(text, /Reviewed files are unavailable\./);
|
|
assert.match(text, /Could not read draft workspace\. Try again later\./);
|
|
assert.doesNotMatch(text, /old\.html|old-draft\.txt/);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
// Review r1 F1/F2: per-key ordering across live containers (from the reviewer probes).
|
|
async function orderFixture(t) {
|
|
const fixture = await filesFixture(t);
|
|
await fixture.b.navigate(fixture.f.base + '/#/');
|
|
await fixture.b.evaluate(`(() => {
|
|
const section = document.createElement('section');
|
|
section.innerHTML = '<h3>Files</h3><div id="review-a"></div><div id="review-b"></div><div id="review-c"></div>';
|
|
document.querySelector('#main').append(section);
|
|
window.reviewState = name => ({ version: 1, revision: 1, phase: 'complete', request: 'request-1', delivery: 'delivery-1', delivery_evidence: {id:'delivery-1', request:'request-1', artifact:{path:'output/' + name}} });
|
|
window.fixtureState = window.reviewState('seed.html');
|
|
window.fixtureWorkspace = {available:true, entries:[{name:'seed-draft.txt'}], truncated:false};
|
|
window.reviewFetch = window.fetch;
|
|
window.reviewMount = (name, options = {}) => MosaicFiles.mount({container:document.querySelector('#review-' + name), requestIds:['request-1'], ...options});
|
|
})()`);
|
|
return fixture;
|
|
}
|
|
|
|
test('Files two live same-key mounts reject a late older on-screen settlement', async t => {
|
|
const { b, errors } = await orderFixture(t);
|
|
await b.evaluate("reviewMount('a')");
|
|
await b.evaluate(`(() => {
|
|
window.fixtureState = reviewState('stale.html');
|
|
window.fixtureWorkspace.entries = [{name:'stale-draft.txt'}];
|
|
let ready;
|
|
window.reviewOldSnapshot = new Promise(resolve => ready = resolve);
|
|
let first = true;
|
|
window.fetch = async (url, init) => {
|
|
if (String(url) === '/api/requests/request-1' && first) {
|
|
first = false;
|
|
const capturedResponse = await window.reviewFetch(url, init);
|
|
await new Promise(resolve => { window.reviewReleaseOld = resolve; ready(); });
|
|
return capturedResponse;
|
|
}
|
|
return window.reviewFetch(url, init);
|
|
};
|
|
window.reviewOld = reviewMount('a');
|
|
})()`);
|
|
await b.evaluate('window.reviewOldSnapshot');
|
|
await b.evaluate(`(async () => {
|
|
window.fixtureState = reviewState('newest.html');
|
|
window.fixtureWorkspace.entries = [{name:'newest-draft.txt'}];
|
|
await reviewMount('b');
|
|
window.reviewReleaseOld(); await window.reviewOld;
|
|
window.fetch = async (url, init) => { if(String(url) === '/api/config') await new Promise(resolve => window.reviewReleaseNext = resolve); return window.reviewFetch(url, init); };
|
|
window.reviewNext = reviewMount('c');
|
|
})()`);
|
|
const actual = await b.evaluate(`Object.fromEntries(['a','b','c'].map(id => [id, { connected:document.querySelector('#review-' + id).isConnected, text:document.querySelector('#review-' + id).textContent }]))`);
|
|
await b.evaluate('(async () => { reviewReleaseNext(); await reviewNext; })()');
|
|
assert.deepEqual(errors, []);
|
|
assert.match(actual.b.text, /newest\.html/);
|
|
assert.match(actual.c.text, /newest\.html/, 'the shared cache correctly retains the newer mount');
|
|
assert.doesNotMatch(actual.a.text, /stale\.html|stale-draft\.txt/, 'the superseded live mount must not paint an older result');
|
|
});
|
|
|
|
test('Files a new container for the same key keeps a completed result while a later read is pending', async t => {
|
|
const { b, errors } = await orderFixture(t);
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if(String(url) === '/api/config') await new Promise(resolve => window.reviewReleaseFirst = resolve); return window.reviewFetch(url, init); };
|
|
window.reviewFirst = reviewMount('a');
|
|
})()`);
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if(String(url) === '/api/config') await new Promise(resolve => window.reviewReleaseSecond = resolve); return window.reviewFetch(url, init); };
|
|
window.reviewSecond = reviewMount('b');
|
|
})()`);
|
|
await b.evaluate('(async () => { reviewReleaseFirst(); await reviewFirst; })()');
|
|
await b.evaluate(`(() => {
|
|
window.fetch = async (url, init) => { if(String(url) === '/api/config') await new Promise(resolve => window.reviewReleaseThird = resolve); return window.reviewFetch(url, init); };
|
|
window.reviewThird = reviewMount('c');
|
|
})()`);
|
|
const actual = await b.evaluate(`({completed:document.querySelector('#review-a').textContent, newMount:document.querySelector('#review-c').textContent})`);
|
|
await b.evaluate('(async () => { reviewReleaseSecond(); await reviewSecond; reviewReleaseThird(); await reviewThird; })()');
|
|
assert.deepEqual(errors, []);
|
|
assert.match(actual.completed, /seed\.html/);
|
|
assert.match(actual.newMount, /seed\.html/, 'a completed result exists for this request key');
|
|
});
|