Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(routing): fix history router based on history length #5004

Merged
merged 12 commits into from
Feb 7, 2022
111 changes: 111 additions & 0 deletions src/lib/__tests__/routing-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { createSearchClient } from '../../../test/mock/createSearchClient';
import { wait } from '../../../test/utils/wait';
import historyRouter from '../routers/history';
import instantsearch from '../..';
import { connectSearchBox } from '../../connectors';

const writeDelay = 10;
const writeWait = 1.5 * writeDelay;

describe('routing', () => {
beforeEach(() => {
window.history.replaceState({}, '', 'http://localhost/');
jest.clearAllMocks();
});

test('SPA (Single Page App) use case: URL should not be cleaned', async () => {
const pushState = jest.spyOn(window.history, 'pushState');

const search = instantsearch({
indexName: 'indexName',
searchClient: createSearchClient(),
routing: {
router: historyRouter({
writeDelay,
}),
},
});

search.addWidgets([connectSearchBox(() => {})({})]);

search.start();

// Check URL has been initialized
await wait(writeWait);
expect(window.location.search).toEqual('');
expect(pushState).toHaveBeenCalledTimes(0);

// Trigger an update by refining
search.renderState.indexName!.searchBox!.refine('Apple');

// Check URL has been updated
await wait(writeWait);
expect(window.location.search).toEqual(
`?${encodeURI('indexName[query]=Apple')}`
);
expect(pushState).toHaveBeenCalledTimes(1);

// Trigger a dispose
search.dispose();

// Navigate to a new page (like a router would do)
window.history.pushState({}, '', '/about');

// Check URL has been updated to new page
await wait(writeWait);
expect(window.location.search).toEqual('');
expect(window.location.pathname).toEqual('/about');
expect(pushState).toHaveBeenCalledTimes(2);

// Go back to previous page
window.history.back();

// Check URL has been updated to previous page
await wait(writeWait);
expect(window.location.search).toEqual(
`?${encodeURI('indexName[query]=Apple')}`
);
expect(window.location.pathname).toEqual('/');
});

test('Modal use case: dispose is manually called', async () => {
const pushState = jest.spyOn(window.history, 'pushState');

const search = instantsearch({
indexName: 'indexName',
searchClient: createSearchClient(),
routing: {
router: historyRouter({
writeDelay,
}),
},
});

search.addWidgets([connectSearchBox(() => {})({})]);

search.start();

// Check URL has been initialized
await wait(writeWait);
expect(window.location.search).toEqual('');
expect(pushState).toHaveBeenCalledTimes(0);

// Trigger an update by refining
search.renderState.indexName!.searchBox!.refine('Apple');

// Check URL has been updated
await wait(writeWait);
expect(window.location.search).toEqual(
`?${encodeURI('indexName[query]=Apple')}`
);
expect(pushState).toHaveBeenCalledTimes(1);

// Trigger a dispose (modal closed, for example)
search.dispose();

// Check URL has been cleaned
await wait(writeWait);
expect(window.location.search).toEqual('');
expect(pushState).toHaveBeenCalledTimes(2);
});
});
18 changes: 16 additions & 2 deletions src/lib/routers/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ class BrowserHistory<TRouteState> implements Router<TRouteState> {
*/
private shouldPushState: boolean = true;

/**
* Indicates the window.history.length before the last call to
* window.history.pushState (called in `write`).
* It allows to determine if a `pushState` has been triggered elsewhere,
* and thus to prevent the `write` method from calling `pushState`.
*/
private lastHistoryLength: number = 0;
FabienMotte marked this conversation as resolved.
Show resolved Hide resolved

/**
* Initializes a new storage provider that syncs the search state to the URL
* using web APIs (`window.location.pushState` and `onpopstate` event).
Expand All @@ -96,9 +104,11 @@ class BrowserHistory<TRouteState> implements Router<TRouteState> {
this.parseURL = parseURL;
this.getLocation = getLocation;

safelyRunOnBrowser(() => {
safelyRunOnBrowser(({ window }) => {
const title = this.windowTitle && this.windowTitle(this.read());
setWindowTitle(title);

this.lastHistoryLength = window.history.length;
});
}

Expand All @@ -123,8 +133,12 @@ class BrowserHistory<TRouteState> implements Router<TRouteState> {

this.writeTimer = setTimeout(() => {
setWindowTitle(title);
if (this.shouldPushState) {
if (
this.shouldPushState &&
this.lastHistoryLength === window.history.length
FabienMotte marked this conversation as resolved.
Show resolved Hide resolved
) {
window.history.pushState(routeState, title || '', url);
this.lastHistoryLength = window.history.length;
}
this.shouldPushState = true;
this.writeTimer = undefined;
Expand Down