diff --git a/tests/browser/workspace-activity.spec.mjs b/tests/browser/workspace-activity.spec.mjs index a8adb14..f61eac5 100644 --- a/tests/browser/workspace-activity.spec.mjs +++ b/tests/browser/workspace-activity.spec.mjs @@ -643,3 +643,22 @@ test('an unknown or restarted job view says so', async ({ page, request }) => { await expect(fileRows(page)).toHaveCount(1); await expect(pill(page)).toBeHidden(); }); + +// Review M3: a finished job the server no longer lists is gone from this tab too, and its open view says so. +test('a finished job the server pruned leaves its open view honest', async ({ page, request }) => { + await page.setViewportSize({ width: 1600, height: 1100 }); + const job = await startJob(request); + await step(request, job.id, { processed: 1, records: [{ group: 'added', path: 'kept/row.png', detail: 'Added' }] }); + await finish(request, job.id); + await openJob(page, job.id); + await expect(fileRows(page)).toHaveCount(1); + await filter(page).focus(); + await expect(await request.post('/__test__/jobs/restart')).toBeOK(); + // The next periodic list read (15 s) no longer has the finished job. + test.slow(); + await expect(main(page).locator('section.surface p.empty-state')).toHaveText('This job is not in this session’s Activity. Jobs are kept until ComfyUI restarts.', { timeout: 20000 }); + await expect(progressSection(page)).toHaveCount(0); + await expect(heading(page).locator('h1')).toHaveText('Activity'); + await expect(heading(page).locator('h1')).toBeFocused(); + await expect(page).toHaveTitle('Activity · Prompt Manager'); +}); diff --git a/tests/js/workspace-toast.test.js b/tests/js/workspace-toast.test.js new file mode 100644 index 0000000..907498f --- /dev/null +++ b/tests/js/workspace-toast.test.js @@ -0,0 +1,96 @@ +// The shell's shared toast: plain messages clear after 6.5 s; a message with a link stays 20 s (D57), and any toast +// stays while the pointer or keyboard focus rests on it. +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const source = import('../../web/js/workspace/toast.mjs'); + +class FakeToast extends EventTarget { + constructor() { super(); this.held = false; this.nodes = []; this.text = ''; } + get textContent() { return this.text; } + set textContent(value) { this.text = value; this.nodes = []; } + replaceChildren(...nodes) { this.nodes = nodes; this.text = nodes.map(node => node.textContent).join(''); } + matches(selector) { assert.equal(selector, ':hover, :focus-within'); return this.held; } +} + +function install() { + const toast = new FakeToast(); + const previous = global.document; + global.document = { + getElementById: id => id === 'workspace-toast' ? toast : null, + createTextNode: text => ({ textContent: text }), + createElement: tag => ({ tag, href: '', textContent: '' }), + }; + return { toast, restore: () => { global.document = previous; } }; +} + +test('a plain toast clears after 6.5 s and a linked one after 20 s', async t => { + const { showWorkspaceToast, TOAST_MS, LINK_TOAST_MS } = await source; + t.mock.timers.enable({ apis: ['setTimeout'] }); + const { toast, restore } = install(); + try { + showWorkspaceToast('Scan started.'); + assert.equal(toast.textContent, 'Scan started.'); + t.mock.timers.tick(TOAST_MS); + assert.equal(toast.textContent, ''); + + showWorkspaceToast(['Another job is running: ', { text: 'Scan', href: '#/activity/x' }, '.']); + assert.equal(toast.textContent, 'Another job is running: Scan.'); + assert.deepEqual(toast.nodes[1], { tag: 'a', href: '#/activity/x', textContent: 'Scan' }); + t.mock.timers.tick(TOAST_MS); + assert.equal(toast.textContent, 'Another job is running: Scan.'); + t.mock.timers.tick(LINK_TOAST_MS - TOAST_MS); + assert.equal(toast.textContent, ''); + } finally { + restore(); + } +}); + +test('a toast under the pointer or focus waits until they leave', async t => { + const { showWorkspaceToast, TOAST_MS } = await source; + t.mock.timers.enable({ apis: ['setTimeout'] }); + const { toast, restore } = install(); + try { + showWorkspaceToast('Prompt copied.'); + toast.held = true; + t.mock.timers.tick(TOAST_MS); + assert.equal(toast.textContent, 'Prompt copied.', 'held: it stays'); + // Focus moving inside the toast keeps it; leaving it starts a fresh 6.5 s. + toast.dispatchEvent(new Event('focusout')); + t.mock.timers.tick(0); + toast.dispatchEvent(new Event('pointerleave')); + assert.equal(toast.textContent, 'Prompt copied.'); + toast.held = false; + toast.dispatchEvent(new Event('focusout')); + t.mock.timers.tick(0); + t.mock.timers.tick(TOAST_MS - 1); + assert.equal(toast.textContent, 'Prompt copied.'); + t.mock.timers.tick(1); + assert.equal(toast.textContent, ''); + + // A newer toast replaces a held one and stops waiting for the old one's pointer. + showWorkspaceToast('First.'); + toast.held = true; + t.mock.timers.tick(TOAST_MS); + showWorkspaceToast('Second.'); + toast.held = false; + toast.dispatchEvent(new Event('pointerleave')); + t.mock.timers.tick(TOAST_MS - 1); + assert.equal(toast.textContent, 'Second.'); + t.mock.timers.tick(1); + assert.equal(toast.textContent, ''); + } finally { + restore(); + } +}); + +test('a page without the toast shows nothing and does not fail', async () => { + const { showWorkspaceToast } = await source; + const previous = global.document; + global.document = { getElementById: () => null }; + try { + assert.doesNotThrow(() => showWorkspaceToast('Nothing.')); + } finally { + global.document = previous; + } +}); diff --git a/tools/vendor/frontend-coverage-baseline.json b/tools/vendor/frontend-coverage-baseline.json index cbe31b2..b4bfa2f 100644 --- a/tools/vendor/frontend-coverage-baseline.json +++ b/tools/vendor/frontend-coverage-baseline.json @@ -157,7 +157,7 @@ }, "workspace/activity-job-view.mjs": { "lines": 97, - "branches": 82, + "branches": 83, "functions": 98, "statements": 97 }, @@ -169,8 +169,8 @@ }, "workspace/activity-store.mjs": { "lines": 98, - "branches": 94, - "functions": 95, + "branches": 95, + "functions": 98, "statements": 98 }, "workspace/activity.mjs": { @@ -349,7 +349,7 @@ }, "workspace/toast.mjs": { "lines": 98, - "branches": 86, + "branches": 98, "functions": 98, "statements": 98 },