diff --git a/TESTING.md b/TESTING.md index f43da6d4..648ef497 100644 --- a/TESTING.md +++ b/TESTING.md @@ -86,6 +86,18 @@ timer that "usually" covers a frame is a race on a loaded runner. Library virtualization specs settle with `libraryObservation.settled()` (two consecutive frames with no Library frame queued). To look for such races, repeat a spec under load (`--workers 8 --repeat-each 10`, and in the image with `--cpus=1` or `2`). +CPU limits do not reproduce disk-bound timeouts (backup, restore). For those, +throttle writes in the image: with rootful Docker on a host with cgroup v2, add +`--device-write-iops /dev/:20` to the `docker run`, where `` is the +whole disk (not a partition) holding Docker's data root, because the test server +keeps its database in the container's `/tmp`. On a plain partition, +`lsblk -no pkname "$(df --output=source "$(docker info -f '{{.DockerRootDir}}')" | tail -1)"` +prints it; on LVM or LUKS, `lsblk -s` walks down to the disk. A spec that waits +on a timer then fails at its 5 s expect. A spec that waits on the response can +still fail at the 30 s test timeout when several workers share the throttled +disk; that means the disk is too slow for the test budget, not that the spec is +wrong. + On unsupported Linux distributions, browser system dependencies may need manual installation. For a quick browser run, use `test:workflows -- --project chromium`. diff --git a/tests/browser/workflows.spec.mjs b/tests/browser/workflows.spec.mjs index a3007d54..6ae6d971 100644 --- a/tests/browser/workflows.spec.mjs +++ b/tests/browser/workflows.spec.mjs @@ -257,6 +257,18 @@ async function chooseRestore(page, file) { await page.locator('#restoreFileInput').setInputFiles(file); } +// Confirms the chosen restore and waits for the server's answer, not for a timer: a restore is disk-bound and can take +// longer than an expect timeout on a slow runner (#359). The modal closes only after that answer. +async function confirmRestoreAndWait(page) { + const restored = page.waitForResponse(response => response.url().endsWith('/prompt_manager/restore')); + await page.locator('#confirmRestore').click(); + const response = await restored; + const body = await response.text(); + expect(response.status(), body).toBe(200); + expect(JSON.parse(body)).toMatchObject({ success: true, prompt_count: 2, backup_created: expect.any(String) }); + await expect(page.locator('#restoreModal')).toBeHidden(); +} + function confirmNextDialog(page, accept, message) { page.once('dialog', async dialog => { expect(dialog.type()).toBe('confirm'); @@ -287,12 +299,7 @@ test('downloaded backup restores deleted prompts, tags, and image links through expect(await promptSnapshot(request)).not.toEqual(before); await chooseRestore(page, backup); confirmNextDialog(page, true, backup.name); - const restored = page.waitForResponse(response => response.url().endsWith('/prompt_manager/restore')); - await page.locator('#confirmRestore').click(); - const response = await restored; - expect(response.ok()).toBe(true); - expect(await response.json()).toMatchObject({ success: true, prompt_count: 2, backup_created: expect.any(String) }); - await expect(page.locator('#restoreModal')).toBeHidden(); + await confirmRestoreAndWait(page); await expect(page.locator('.prompt-text')).toHaveCount(2); await page.reload(); await expect(page.locator('.prompt-text').filter({ hasText: 'Cat portrait' })).toBeVisible(); @@ -335,8 +342,7 @@ test('restore validation and cancellation preserve live data and allow a valid r expect(await promptSnapshot(request)).toEqual(before); await page.locator('#restoreFileInput').setInputFiles(backup); confirmNextDialog(page, true, backup.name); - await page.locator('#confirmRestore').click(); - await expect(page.locator('#restoreModal')).toBeHidden(); + await confirmRestoreAndWait(page); expect(await promptSnapshot(request)).toEqual(before); }); @@ -356,8 +362,7 @@ test('backup and restore recover from interrupted network requests without losin await expect(page.locator('#restoreModal')).toBeVisible(); expect(await promptSnapshot(request)).toEqual(before); confirmNextDialog(page, true, backup.name); - await page.locator('#confirmRestore').click(); - await expect(page.locator('#restoreModal')).toBeHidden(); + await confirmRestoreAndWait(page); expect(await promptSnapshot(request)).toEqual(before); });