From 89572e03c6e53bf7a77dea81f03fadb24befffe3 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Fri, 30 Oct 2026 16:52:37 -0700 Subject: [PATCH] test(images): wait for the delete and return jobs, not 5 s (#554) Closes #553 --- tests/browser/job-end.mjs | 13 ++++++++++++ .../browser/workspace-images-delete.spec.mjs | 21 ++++++++++++++++--- .../browser/workspace-images-removed.spec.mjs | 9 ++++---- 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 tests/browser/job-end.mjs diff --git a/tests/browser/job-end.mjs b/tests/browser/job-end.mjs new file mode 100644 index 00000000..9bd5d3a4 --- /dev/null +++ b/tests/browser/job-end.mjs @@ -0,0 +1,13 @@ +// A job's end toast follows the job, which has no time limit: under load it outlasts the 5 s expect default (#533, as +// #520). Do `action`, whose POST to `url` answers with the job it started, then wait for that job's event stream to end. +export const APPLY_URL = '/prompt_manager/maintenance/apply'; +export const RETURN_URL = '/prompt_manager/images/removed/return'; + +export async function startAndAwaitJob(page, action, url = APPLY_URL) { + const applied = page.waitForResponse(response => response.url().endsWith(url)); + await action(); + const { job } = await (await applied).json(); + const events = await page.request.get(`/prompt_manager/jobs/${job.id}/events`); + if (!events.ok()) throw new Error(`Job ${job.id} events answered ${events.status()}`); + return job; +} diff --git a/tests/browser/workspace-images-delete.spec.mjs b/tests/browser/workspace-images-delete.spec.mjs index 7051166b..c16247fa 100644 --- a/tests/browser/workspace-images-delete.spec.mjs +++ b/tests/browser/workspace-images-delete.spec.mjs @@ -1,6 +1,7 @@ import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs'; import { test, expect } from './fixtures.mjs'; import { seedConceptAt, openProduction } from './concept-inventory.mjs'; +import { startAndAwaitJob } from './job-end.mjs'; // #480 (D159): Images › Delete…: the review asks every time (nothing preselected), "Remove from the library only" // hides the file and keeps it on disk, "Also move the files to the trash" moves it (the server's fake trash here). @@ -62,7 +63,7 @@ for (const mode of ['library', 'trash']) { const { dialog, primary } = await deleteChecked(page, mode); await expect(dialog.locator('dt', { hasText: 'Media on disk' })).toBeVisible(); await expect(primary).toHaveText(mode === 'trash' ? 'Move 1 image to the trash' : 'Remove 1 image from the library'); - await primary.click(); + await startAndAwaitJob(page, () => primary.click()); await expect(toast(page)).toContainText(mode === 'trash' ? 'Moved 1 image to the trash.' : 'Removed 1 image from the library.'); await expect(page.locator(`${CARDS}[data-image-key="${key}"]`)).toHaveCount(0); await expect(page.locator(CARDS)).toHaveCount(before - 1); @@ -81,11 +82,25 @@ test('a finished delete re-reads the sidebar Images count', async ({ page }) => const before = Number((await badge.textContent()).replace(/,/g, '')); await check(page, 0); const { primary } = await deleteChecked(page, 'library'); - await primary.click(); + await startAndAwaitJob(page, () => primary.click()); await expect(toast(page)).toContainText('Removed 1 image from the library.'); await expect(badge).toHaveText((before - 1).toLocaleString('en-US')); }); +// #533: the toast says "started" for as long as the job's end has not reached the page, then says how it ended. +test('a slow job keeps its started toast until the end arrives', async ({ page }) => { + await openImages(page); + await check(page, 0); + let release; + const held = new Promise(resolve => { release = resolve; }); + await page.route('**/prompt_manager/jobs/*/events', async route => { await held; await route.continue(); }); + const { primary } = await deleteChecked(page, 'library'); + await primary.click(); + await expect(toast(page)).toHaveText('Delete images (remove from the library) started.'); + release(); + await expect(toast(page)).toContainText('Removed 1 image from the library.'); +}); + test('Cancel changes nothing', async ({ page }) => { const before = await openImages(page); await check(page, 0); @@ -175,7 +190,7 @@ test('a chosen mode starts exactly one delete, with that mode, however often it const applies = []; page.on('request', request => { if (request.url().includes('/maintenance/apply')) applies.push(request.postDataJSON()); }); const { primary } = await deleteChecked(page, 'library'); - await primary.dblclick(); + await startAndAwaitJob(page, () => primary.dblclick()); await expect(toast(page)).toContainText('Removed 1 image from the library.'); await expect(page.locator(CARDS)).toHaveCount(before - 1); expect(applies.length).toBe(1); diff --git a/tests/browser/workspace-images-removed.spec.mjs b/tests/browser/workspace-images-removed.spec.mjs index c1ed410b..73a31aaa 100644 --- a/tests/browser/workspace-images-removed.spec.mjs +++ b/tests/browser/workspace-images-removed.spec.mjs @@ -1,6 +1,7 @@ import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs'; import { test, expect } from './fixtures.mjs'; import { seedConceptAt, openProduction } from './concept-inventory.mjs'; +import { RETURN_URL, startAndAwaitJob } from './job-end.mjs'; // #498 (D162): Images › Show removed images (N) lists the files "Remove from the library only" hid; Return to library // (bar or inspector) clears their marks and scans them again. Nothing is deleted or moved. @@ -46,7 +47,7 @@ async function removeFromLibrary(page, count) { await bar(page).getByRole('button', { name: 'Delete…', exact: true }).click(); const dialog = page.getByRole('dialog', { name: /^Delete \d+ images?$/ }); await dialog.getByRole('radio', { name: /^Remove from the library only/ }).check(); - await dialog.locator('.dialog-actions button.danger').click(); + await startAndAwaitJob(page, () => dialog.locator('.dialog-actions button.danger').click()); await expect(toast(page)).toContainText(`Removed ${count} image${count === 1 ? '' : 's'} from the library.`); for (const key of keys) await expect(page.locator(`${CARDS}[data-image-key="${key}"]`)).toHaveCount(0); return keys; @@ -78,7 +79,7 @@ test('Show removed images lists the removed files; Return to library brings them await check(page, 1); const actions = bar(page).locator('button'); await expect(actions).toHaveText(['Return to library', 'Clear']); - await bar(page).getByRole('button', { name: 'Return to library' }).click(); + await startAndAwaitJob(page, () => bar(page).getByRole('button', { name: 'Return to library' }).click(), RETURN_URL); await expect(toast(page)).toContainText('Returned 2 images to the library'); await expect(toast(page)).toContainText('Tags, categories, collections and favorites do not come back.'); // The view emptied: it turns off and Images lists every file again. @@ -98,7 +99,7 @@ test('Return to library from the inspector', async ({ page }) => { await expect(panel.getByRole('button', { name: 'Return to library' })).toBeVisible(); await expect(panel.getByRole('button', { name: 'Delete…' })).toHaveCount(0); await expect(panel.locator('button[data-collections]')).toHaveCount(0); - await panel.getByRole('button', { name: 'Return to library' }).click(); + await startAndAwaitJob(page, () => panel.getByRole('button', { name: 'Return to library' }).click(), RETURN_URL); await expect(toast(page)).toContainText('Returned 1 image to the library'); await expect(toggle(page)).toHaveCount(0); await expect(page.locator(`${CARDS}[data-image-key="${key}"]`)).toHaveCount(1); @@ -112,7 +113,7 @@ test('the sidebar Images count follows a remove and a return', async ({ page }) await expect(imagesBadge(page)).toHaveText((start - 1).toLocaleString('en-US')); await toggle(page).click(); await check(page, 0); - await bar(page).getByRole('button', { name: 'Return to library' }).click(); + await startAndAwaitJob(page, () => bar(page).getByRole('button', { name: 'Return to library' }).click(), RETURN_URL); await expect(toast(page)).toContainText('Returned 1 image to the library'); await expect(imagesBadge(page)).toHaveText(start.toLocaleString('en-US')); });