Skip to content

Commit a2850a3

Browse files
committed
fix(browser): Only patch available window.history properties
1 parent 9cae1a0 commit a2850a3

File tree

4 files changed

+101
-4
lines changed

4 files changed

+101
-4
lines changed

packages/browser-utils/src/instrument/history.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export function addHistoryInstrumentationHandler(handler: (data: HandlerDataHist
1818
maybeInstrument(type, instrumentHistory);
1919
}
2020

21-
function instrumentHistory(): void {
21+
/**
22+
* Exported just for testing
23+
*/
24+
export function instrumentHistory(): void {
2225
// The `popstate` event may also be triggered on `pushState`, but it may not always reliably be emitted by the browser
2326
// Which is why we also monkey-patch methods below, in addition to this
2427
WINDOW.addEventListener('popstate', () => {
@@ -61,6 +64,10 @@ function instrumentHistory(): void {
6164
};
6265
}
6366

64-
fill(WINDOW.history, 'pushState', historyReplacementFunction);
65-
fill(WINDOW.history, 'replaceState', historyReplacementFunction);
67+
if (typeof WINDOW.history.pushState === 'function') {
68+
fill(WINDOW.history, 'pushState', historyReplacementFunction);
69+
}
70+
if (typeof WINDOW.history.replaceState === 'function') {
71+
fill(WINDOW.history, 'replaceState', historyReplacementFunction);
72+
}
6673
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { WINDOW } from '../../src/types';
3+
import { afterEach } from 'node:test';
4+
5+
import { instrumentHistory } from './../../src/instrument/history';
6+
7+
describe('instrumentHistory', () => {
8+
const originalHistory = WINDOW.history;
9+
WINDOW.addEventListener = vi.fn();
10+
11+
afterEach(() => {
12+
// @ts-expect-error - this is fine for testing
13+
WINDOW.history = originalHistory;
14+
});
15+
16+
it("doesn't throw if history is not available", () => {
17+
// @ts-expect-error - this is fine for testing
18+
WINDOW.history = undefined;
19+
expect(instrumentHistory).not.toThrow();
20+
expect(WINDOW.history).toBe(undefined);
21+
});
22+
23+
it('adds an event listener for popstate', () => {
24+
// adds an event listener for popstate
25+
expect(WINDOW.addEventListener).toHaveBeenCalledTimes(1);
26+
expect(WINDOW.addEventListener).toHaveBeenCalledWith('popstate', expect.any(Function));
27+
});
28+
29+
it("doesn't throw if history.pushState is not a function", () => {
30+
// @ts-expect-error - only partially adding history properties
31+
WINDOW.history = {
32+
replaceState: () => {},
33+
pushState: undefined,
34+
};
35+
36+
expect(instrumentHistory).not.toThrow();
37+
38+
expect(WINDOW.history).toEqual({
39+
replaceState: expect.any(Function), // patched function
40+
pushState: undefined, // unpatched
41+
});
42+
});
43+
44+
it("doesn't throw if history.replaceState is not a function", () => {
45+
// @ts-expect-error - only partially adding history properties
46+
WINDOW.history = {
47+
replaceState: undefined,
48+
pushState: () => {},
49+
};
50+
51+
expect(instrumentHistory).not.toThrow();
52+
53+
expect(WINDOW.history).toEqual({
54+
replaceState: undefined, // unpatched
55+
pushState: expect.any(Function), // patched function
56+
});
57+
});
58+
});

packages/core/src/utils-hoist/supports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function supportsDOMException(): boolean {
6161
* @returns Answer to the given question.
6262
*/
6363
export function supportsHistory(): boolean {
64-
return 'history' in WINDOW;
64+
return 'history' in WINDOW && !!WINDOW.history;
6565
}
6666

6767
/**
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { afterEach } from 'node:test';
2+
import { supportsHistory } from '../../src/utils-hoist/supports';
3+
import { describe, it, expect } from 'vitest';
4+
5+
describe('supportsHistory', () => {
6+
const originalHistory = globalThis.history;
7+
8+
afterEach(() => {
9+
globalThis.history = originalHistory;
10+
});
11+
12+
it('returns true if history is available', () => {
13+
// @ts-expect-error - not setting all history properties
14+
globalThis.history = {
15+
pushState: () => {},
16+
replaceState: () => {},
17+
};
18+
expect(supportsHistory()).toBe(true);
19+
});
20+
21+
it('returns false if history is not available', () => {
22+
// @ts-expect-error - deletion is okay in this case
23+
delete globalThis.history;
24+
expect(supportsHistory()).toBe(false);
25+
});
26+
27+
it('returns false if history is undefined', () => {
28+
// @ts-expect-error - undefined is okay in this case
29+
globalThis.history = undefined;
30+
expect(supportsHistory()).toBe(false);
31+
});
32+
});

0 commit comments

Comments
 (0)