Skip to content
Open
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
25 changes: 23 additions & 2 deletions packages/void/src/pages/navigation-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ function withoutActionKey(fetchUrl: string, pageUrl: string): string {
: fetchUrl;
}

/** URL to commit after a page mutation (strip action keys, then prefer the server page URL). */
function mutationCommitUrl(fetchUrl: string, pageUrl?: string): string {
if (!pageUrl) {
return withoutActionKey(fetchUrl, '');
}
const withoutKey = withoutActionKey(fetchUrl, pageUrl);
if (withoutKey !== fetchUrl) {
return withoutKey;
}
const fetchPathEnd = fetchUrl.indexOf('?');
const fetchPath = fetchPathEnd === -1 ? fetchUrl : fetchUrl.slice(0, fetchPathEnd);
const pagePathEnd = pageUrl.indexOf('?');
const pagePath = pagePathEnd === -1 ? pageUrl : pageUrl.slice(0, pagePathEnd);
return fetchPath === pagePath ? pageUrl : fetchUrl;
}

export type PageComponentModule = {
default: unknown;
};
Expand Down Expand Up @@ -835,7 +851,7 @@ export async function createPageNavigationEngine(

if (method !== 'GET') {
deferredRegistry.preserveValuesFrom(_committedDeferredRegistry, pageData);
commitTarget.fetchUrl = withoutActionKey(fetchUrl, pageData.url);
commitTarget.fetchUrl = mutationCommitUrl(fetchUrl, pageData.url);
}

const [component, layouts] = await Promise.race([
Expand Down Expand Up @@ -923,7 +939,12 @@ export async function createPageNavigationEngine(
// preserveState only controls component state; back/forward traversal already moved history.
if (!options._popstate) {
const historyUrl = commitTarget.fetchUrl + commitTarget.hash;
scrollRestoration.writeHistory(historyUrl, !!options.replace);
const resource = getResource();
const currentDocumentUrl = window.location.pathname + window.location.search;
const replaceHistory =
!!options.replace ||
(resource.method !== 'GET' && historyUrl === currentDocumentUrl + commitTarget.hash);
scrollRestoration.writeHistory(historyUrl, replaceHistory);
}
scrollRestoration.commit();

Expand Down
44 changes: 42 additions & 2 deletions packages/void/test/unit/pages-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,11 @@ describe('createVoidRouter', () => {
['/books?delete', '/books', '/books'],
['/books?page=2', '/books?page=2', '/books?page=2'],
['/books?a&b', '/books?a&b', '/books?a&b'],
])('writes POST %s with page URL %s to history as %s', async (url, pageUrl, historyUrl) => {
])('replaces history for POST %s with page URL %s as %s', async (url, pageUrl, historyUrl) => {
stubClientGlobals();
const historyLocation = new URL(historyUrl, 'http://localhost');
window.location.pathname = historyLocation.pathname;
window.location.search = historyLocation.search;
vi.stubGlobal(
'fetch',
vi.fn(async () => pageJson({ component: 'home', props: {}, url: pageUrl, errors: null })),
Expand All @@ -524,7 +527,44 @@ describe('createVoidRouter', () => {

await router.visit(url, { method: 'POST' });

expect(history.pushState).toHaveBeenCalledWith(expect.anything(), '', historyUrl);
expect(history.replaceState).toHaveBeenCalledWith(expect.anything(), '', historyUrl);
expect(history.pushState).not.toHaveBeenCalled();
});

it('pushes history when a POST changes the page URL', async () => {
stubClientGlobals();
window.location.pathname = '/books';
window.location.search = '';
vi.stubGlobal(
'fetch',
vi.fn(async () =>
pageJson({ component: 'home', props: {}, url: '/books?page=2', errors: null }),
),
);
const { router } = await createTestRouter();

await router.visit('/books?delete', { method: 'POST' });

expect(history.pushState).toHaveBeenCalledWith(expect.anything(), '', '/books?page=2');
expect(history.replaceState).not.toHaveBeenCalled();
});

it('does not grow history on repeated same-page named action submits', async () => {
stubClientGlobals();
window.location.pathname = '/db';
window.location.search = '';
vi.stubGlobal(
'fetch',
vi.fn(async () => pageJson({ component: 'home', props: {}, url: '/db', errors: null })),
);
const { router } = await createTestRouter();

await router.visit('/db?create', { method: 'POST' });
await router.visit('/db?create', { method: 'POST' });

expect(history.pushState).not.toHaveBeenCalled();
expect(history.replaceState).toHaveBeenCalledTimes(2);
expect(history.replaceState).toHaveBeenLastCalledWith(expect.anything(), '', '/db');
});

it('runs navigation listeners for same-page hash visits', async () => {
Expand Down