diff --git a/tests/browser/workflows.spec.mjs b/tests/browser/workflows.spec.mjs index 2f31eda..0ee4620 100644 --- a/tests/browser/workflows.spec.mjs +++ b/tests/browser/workflows.spec.mjs @@ -74,6 +74,24 @@ test('a page whose leave was called off reads its list again on the next input', await expect(page.locator('#resultsTitle')).toHaveText('Recent Prompts'); }); +// The gallery's startup thumbnail check runs a second after load. When that second ends while the page is leaving, +// the check must start no read: WebKit fails it with "Fetch API cannot load … due to access control checks." and +// Playwright reports a page error (seen in CI on PR #289). The page clock makes the second end after the leave began. +for (const leave of ['beforeunload', 'pagehide']) { + test(`the gallery startup check starts no read after ${leave}`, async ({ page }) => { + await page.clock.install(); + const statusReads = []; + page.on('request', req => { if (req.url().includes('/images/thumbnails/status')) statusReads.push(req.url()); }); + await page.goto('/prompt_manager/gallery'); + await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2); + await page.evaluate(type => window.dispatchEvent(new Event(type)), leave); + await page.clock.runFor(1500); + // A read made after the clock ran is listed, so the listener saw every status read. + await page.evaluate(() => fetch('/prompt_manager/images/thumbnails/status?probe=1')); + expect(statusReads.map(url => new URL(url).search)).toEqual(['?probe=1']); + }); +} + test('actual gallery review generates and persists tags for the selected root', async ({ page, request }) => { await page.goto('/prompt_manager/gallery'); await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2); diff --git a/tests/js/gallery-thumbnails.test.js b/tests/js/gallery-thumbnails.test.js index 431beea..5b085f2 100644 --- a/tests/js/gallery-thumbnails.test.js +++ b/tests/js/gallery-thumbnails.test.js @@ -73,6 +73,28 @@ test('Not Now persists the collection state and suppresses the unchanged prompt' assert.equal(f.prompts.length, 1); }); +// WebKit fails a fetch a leaving page starts ("Fetch API cannot load … due to access control checks.") and logs it +// as a page error: the delayed startup check starts no read once the page has begun to leave (as admin.js, #290). +test('the startup check starts no read once the page is leaving', async () => { + const f = fixture({ success: true, total_images: 100, pending: 30, collection_id: 'a' }); + f.manager.pageLifetime = new AbortController(); + f.manager.pageLifetime.abort(); + await f.check(); + assert.equal(f.requests.length, 0); + assert.equal(f.prompts.length, 0); +}); + +test('the startup check passes the page lifetime to its read', async () => { + const f = fixture({ success: true, total_images: 100, pending: 30, collection_id: 'a' }); + const signals = []; + f.manager.pageLifetime = new AbortController(); + const get = f.manager.api.get; + f.manager.api.get = async (url, options) => { signals.push(options?.signal); return get(url); }; + await f.check(); + assert.deepEqual(signals, [f.manager.pageLifetime.signal]); + assert.equal(f.prompts.length, 1); +}); + test('disabled checks do not request collection status', async () => { const f = fixture({}, { checkThumbnailsAtStartup: false }); await f.check(); diff --git a/web/js/gallery.js b/web/js/gallery.js index 7f6b2d6..ee9f24b 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -17,6 +17,14 @@ this.dupResults = null; this.api = ApiClient.createApiClient(); + // The page's lifetime ends when it starts to leave: WebKit fails a fetch a leaving page starts + // ("Fetch API cannot load … due to access control checks.") and logs it as a page error. + this.pageLifetime = new AbortController(); + window.addEventListener("beforeunload", () => this.pageLifetime.abort()); + window.addEventListener("pagehide", () => this.pageLifetime.abort()); + window.addEventListener("pageshow", (e) => { + if (e.persisted && this.pageLifetime.signal.aborted) this.pageLifetime = new AbortController(); + }); // Only the newest page request may update the grid this.imagesRequest = ApiClient.latestOnly(); @@ -2534,8 +2542,11 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; return; } + // A leaving page starts no read, and one it aborts is not reported. + const signal = this.pageLifetime?.signal; + if (signal?.aborted) return; try { - const data = await this.api.get('/prompt_manager/images/thumbnails/status'); + const data = await this.api.get('/prompt_manager/images/thumbnails/status', { signal }); const total = data.total_images; const pending = data.pending; if (!data.success || !Number.isSafeInteger(total) ||