From 310ea8a4afbe3e21cc09b1c2899d4e675ec1eb4b Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Thu, 3 Sep 2026 16:55:13 +0200 Subject: [PATCH 1/6] Add e2e test helpers --- tests/functional/inc/commands.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/functional/inc/commands.ts b/tests/functional/inc/commands.ts index 8611207..3205f71 100644 --- a/tests/functional/inc/commands.ts +++ b/tests/functional/inc/commands.ts @@ -16,6 +16,20 @@ export function sleep(timeout = 0): Promise { return new Promise((resolve) => setTimeout(() => resolve(undefined), timeout)); } +function documentOrigin(page: Page) { + return page.evaluate(() => Math.floor(window.performance.timeOrigin)); +} + +export async function expectNoPageReload( + page: Page, + action: (page: Page) => Promise | unknown +) { + const before = await documentOrigin(page); + await action(page); + await sleep(500); + expect(await documentOrigin(page)).toEqual(before); +} + export async function clickOnLink(page: Page, url: string, options?: Parameters[1]) { await page.click(`a[href="${url}"]`, options); await expectToBeAt(page, url); From 6200899ebb7fbcd21b38d843081ec691e7cda6ae Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Thu, 3 Sep 2026 16:58:30 +0200 Subject: [PATCH 2/6] Inject page load hook args during preload --- src/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 76ea559..99d1b1b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -181,10 +181,15 @@ export default class SwupPreloadPlugin extends Plugin { /** * Before core page load: return existing preload promise if available. */ - protected onPageLoad: HookDefaultHandler<'page:load'> = (visit, args, defaultHandler) => { + protected onPageLoad: HookDefaultHandler<'page:load'> = async (visit, args, defaultHandler) => { const { url } = visit.to; - if (url && this.preloadPromises.has(url)) { - return this.preloadPromises.get(url) as Promise; + const preloadPromise = url ? this.preloadPromises.get(url) : undefined; + // A preload can fail and end up with no page to reuse + const page = (await preloadPromise?.catch(() => undefined)) ?? null; + if (page) { + args.page = page; + args.cache = false; + return page; } return defaultHandler!(visit, args); }; From 0dd7e55c5f617261723a5e09149651248da9820d Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Thu, 3 Sep 2026 16:58:48 +0200 Subject: [PATCH 3/6] Add tests around inflight page args --- tests/functional/preload-plugin.spec.ts | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/functional/preload-plugin.spec.ts b/tests/functional/preload-plugin.spec.ts index d750634..bb20c76 100644 --- a/tests/functional/preload-plugin.spec.ts +++ b/tests/functional/preload-plugin.spec.ts @@ -5,6 +5,7 @@ import { clickOnLink, waitForSwup, navigateWithSwup, + expectNoPageReload, expectSwupToHaveCacheEntry, expectSwupNotToHaveCacheEntry, expectSwupToHaveCacheEntries, @@ -282,3 +283,87 @@ test.describe('ignores external origins', () => { expect(urls).toEqual(['/page-1.html', '/page-2.html']); }); }); + +test.describe('page:load args', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/page-1.html'); + await waitForSwup(page); + await page.evaluate(() => { + window._swup.hooks.on('page:load', (visit, args) => { + window.data = { page: args.page, cache: args.cache }; + }); + }); + }); + + test('provides page data when reusing an in-flight preload', async ({ page }) => { + let releasePreload!: () => void; + let markRequestStarted!: () => void; + const requestStarted = new Promise((resolve) => (markRequestStarted = resolve)); + const release = new Promise((resolve) => (releasePreload = resolve)); + + await page.route('**/page-2.html', async (route) => { + markRequestStarted(); + await release; + await route.continue(); + }); + + await page.evaluate(() => { + window._swup.preload!('/page-2.html'); + }); + await requestStarted; + await clickOnLink(page, '/page-2.html'); + await sleep(500); + releasePreload(); + + await expect(page.locator('h1')).toHaveText('Page 2', { timeout: 5000 }); + + const pageArgs = await page.evaluate(() => window.data); + expect(pageArgs.page).toBeTruthy(); + expect(pageArgs.page.url).toContain('/page-2.html'); + expect(pageArgs.page.html).toContain('Page 2'); + expect(pageArgs.cache).toBe(false); + }); + + test('recovers when a reused in-flight preload fails', async ({ page }) => { + let requestCount = 0; + let releasePreload!: () => void; + let markRequestStarted!: () => void; + const requestStarted = new Promise((resolve) => (markRequestStarted = resolve)); + const release = new Promise((resolve) => (releasePreload = resolve)); + + await page.route('**/page-2.html', async (route) => { + requestCount++; + if (requestCount === 1) { + markRequestStarted(); + await release; + await route.abort(); + } else { + await route.continue(); + } + }); + + await expectNoPageReload(page, async () => { + await page.evaluate(() => { + window._swup.preload!('/page-2.html'); + }); + await requestStarted; + await clickOnLink(page, '/page-2.html'); + await sleep(500); + releasePreload(); + + await expect(page.locator('h1')).toHaveText('Page 2', { timeout: 5000 }); + }); + + const pageArgs = await page.evaluate(() => window.data); + expect(pageArgs?.page).toBeTruthy(); + }); + + test('provides page data when no preload is in flight', async ({ page }) => { + await navigateWithSwup(page, '/page-2.html'); + await expect(page.locator('h1')).toHaveText('Page 2'); + + const pageArgs = await page.evaluate(() => window.data); + expect(pageArgs?.page).toBeTruthy(); + expect(pageArgs.page.url).toContain('/page-2.html'); + }); +}); From 56e8f4323af001fe45bf7012e43d4cb0c2742194 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Thu, 3 Sep 2026 16:59:32 +0200 Subject: [PATCH 4/6] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ce55ad..c101254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [3.2.12] - 2026-09-03 + +- Set `args.page` when a navigation reuses an in-flight preload +- Fall back to a fresh fetch when an in-flight preload fails instead of aborting the visit + ## [3.2.11] - 2024-11-20 - Return cached data or pending promise for `swup.preload()` if already preloaded or preloading @@ -129,6 +134,7 @@ - Initial release +[3.2.12]: https://github.com/swup/preload-plugin/releases/tag/3.2.12 [3.2.11]: https://github.com/swup/preload-plugin/releases/tag/3.2.11 [3.2.10]: https://github.com/swup/preload-plugin/releases/tag/3.2.10 [3.2.9]: https://github.com/swup/preload-plugin/releases/tag/3.2.9 From 82be637d018527facfc3704965bd11f5927b8514 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 4 Sep 2026 11:02:57 +0200 Subject: [PATCH 5/6] Format for clarity --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 99d1b1b..a3af67c 100755 --- a/src/index.ts +++ b/src/index.ts @@ -183,7 +183,9 @@ export default class SwupPreloadPlugin extends Plugin { */ protected onPageLoad: HookDefaultHandler<'page:load'> = async (visit, args, defaultHandler) => { const { url } = visit.to; + const preloadPromise = url ? this.preloadPromises.get(url) : undefined; + // A preload can fail and end up with no page to reuse const page = (await preloadPromise?.catch(() => undefined)) ?? null; if (page) { @@ -191,6 +193,7 @@ export default class SwupPreloadPlugin extends Plugin { args.cache = false; return page; } + return defaultHandler!(visit, args); }; From a7fcfce776787b70293aebfff06f58d772a620b1 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 4 Sep 2026 11:03:11 +0200 Subject: [PATCH 6/6] Simplify load method --- src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index a3af67c..f677375 100755 --- a/src/index.ts +++ b/src/index.ts @@ -188,13 +188,13 @@ export default class SwupPreloadPlugin extends Plugin { // A preload can fail and end up with no page to reuse const page = (await preloadPromise?.catch(() => undefined)) ?? null; - if (page) { - args.page = page; - args.cache = false; - return page; + if (!page) { + return defaultHandler!(visit, args); } - return defaultHandler!(visit, args); + args.page = page; + args.cache = false; + return page; }; /**