Skip to content

Commit 47f95e5

Browse files
authored
fix(core): Ensure fill only patches functions (#15632)
1 parent 9cae1a0 commit 47f95e5

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

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

+4-1
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', () => {
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/object.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { truncate } from './string';
1010
/**
1111
* Replace a method in an object with a wrapped version of itself.
1212
*
13+
* If the method on the passed object is not a function, the wrapper will not be applied.
14+
*
1315
* @param source An object that contains a method to be wrapped.
1416
* @param name The name of the method to be wrapped.
1517
* @param replacementFactory A higher-order function that takes the original version of the given method and returns a
@@ -23,7 +25,13 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
2325
return;
2426
}
2527

26-
const original = source[name] as () => any;
28+
// explicitly casting to unknown because we don't know the type of the method initially at all
29+
const original = source[name] as unknown;
30+
31+
if (typeof original !== 'function') {
32+
return;
33+
}
34+
2735
const wrapped = replacementFactory(original) as WrappedFunction;
2836

2937
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work

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

+1-1
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
/**

packages/core/test/utils-hoist/object.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ describe('fill()', () => {
5454
expect(source.prop()).toEqual(41);
5555
});
5656

57+
test.each([42, null, undefined, {}])("does't throw if the property is not a function but %s", (propValue: any) => {
58+
const source = {
59+
foo: propValue,
60+
};
61+
const name = 'foo';
62+
const replacement = vi.fn().mockImplementationOnce(cb => cb);
63+
64+
fill(source, name, replacement);
65+
66+
expect(source.foo).toBe(propValue);
67+
expect(replacement).not.toBeCalled();
68+
});
69+
5770
test('can do anything inside replacement function', () => {
5871
const source = {
5972
foo: (): number => 42,
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)