Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,20 @@ 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<PageData>;

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) {
return defaultHandler!(visit, args);
}
return defaultHandler!(visit, args);

args.page = page;
args.cache = false;
return page;
};

/**
Expand Down
14 changes: 14 additions & 0 deletions tests/functional/inc/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ export function sleep(timeout = 0): Promise<void> {
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> | 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<Page['click']>[1]) {
await page.click(`a[href="${url}"]`, options);
await expectToBeAt(page, url);
Expand Down
85 changes: 85 additions & 0 deletions tests/functional/preload-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
clickOnLink,
waitForSwup,
navigateWithSwup,
expectNoPageReload,
expectSwupToHaveCacheEntry,
expectSwupNotToHaveCacheEntry,
expectSwupToHaveCacheEntries,
Expand Down Expand Up @@ -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<void>((resolve) => (markRequestStarted = resolve));
const release = new Promise<void>((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<void>((resolve) => (markRequestStarted = resolve));
const release = new Promise<void>((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');
});
});
Loading