Skip to content

Commit 9746f23

Browse files
committed
main 🧊 add use performance observer
1 parent adb0873 commit 9746f23

15 files changed

Lines changed: 1044 additions & 666 deletions

File tree

‎package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
"prepare": "husky"
3636
},
3737
"devDependencies": {
38-
"@siberiacancode/eslint": "2.16.5",
38+
"@siberiacancode/eslint": "2.16.11",
3939
"@siberiacancode/prettier": "^1.6.1",
40-
"@types/node": "^25.3.5",
40+
"@types/node": "^25.5.0",
4141
"husky": "^9.1.7",
42-
"lint-staged": "^16.3.2",
42+
"lint-staged": "^16.4.0",
4343
"prettier-plugin-tailwindcss": "^0.7.2",
4444
"typescript": "^5.9.3"
4545
}

‎packages/core/package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@
7171
"@types/react": "^19.2.14",
7272
"@types/react-dom": "^19.2.3",
7373
"@types/web-bluetooth": "^0.0.21",
74-
"@vitejs/plugin-react": "^5.1.4",
75-
"core-js": "^3.48.0",
74+
"@vitejs/plugin-react": "^6.0.1",
75+
"core-js": "^3.49.0",
7676
"react": "^19.2.4",
7777
"react-dom": "^19.2.4",
7878
"shx": "^0.4.0",
7979
"tailwindcss": "4.2.1",
80-
"vite": "^7.3.1",
80+
"vite": "^8.0.0",
8181
"vite-plugin-dts": "^4.5.4",
82-
"vitest": "^4.0.18"
82+
"vitest": "^4.1.0"
8383
},
8484
"lint-staged": {
8585
"*.{js,ts,tsx}": [

‎packages/core/src/bundle/hooks/usePerformanceObserver/usePerformanceObserver.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useEffect, useRef, useState } from 'react';
1717
export const usePerformanceObserver = (options, callback) => {
1818
const supported = typeof window !== 'undefined' && !!window.PerformanceObserver;
1919
const [entries, setEntries] = useState([]);
20-
const observerRef = useRef(null);
20+
const observerRef = useRef(undefined);
2121
const internalCallback = useRef(callback);
2222
internalCallback.current = callback;
2323
const start = () => {
@@ -32,7 +32,7 @@ export const usePerformanceObserver = (options, callback) => {
3232
const stop = () => {
3333
if (!supported) return;
3434
observerRef.current?.disconnect();
35-
observerRef.current = null;
35+
observerRef.current = undefined;
3636
};
3737
useEffect(() => {
3838
if (!supported) return;
@@ -41,5 +41,5 @@ export const usePerformanceObserver = (options, callback) => {
4141
stop();
4242
};
4343
}, []);
44-
return { supported, entries, start, stop };
44+
return { supported, entries, start, stop, observer: observerRef.current };
4545
};

‎packages/core/src/hooks/useBluetooth/useBluetooth.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ beforeEach(() => {
3535
configurable: true
3636
});
3737

38-
vi.clearAllMocks();
3938
trigger.clear();
4039
});
4140

‎packages/core/src/hooks/useCssVar/useCssVar.test.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const element = document.getElementById('target') as HTMLDivElement;
2525
beforeEach(() => {
2626
element.style.removeProperty('--test-color');
2727
element.className = '';
28-
vi.clearAllMocks();
2928
});
3029

3130
const trigger = createTrigger<Node, MutationCallback>();

‎packages/core/src/hooks/useDevicePixelRatio/useDevicePixelRatio.test.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ beforeEach(() => {
4242
trigger.clear();
4343
});
4444

45-
afterEach(() => {
46-
mockMediaQueryListAddEventListener.mockClear();
47-
mockMediaQueryListRemoveEventListener.mockClear();
48-
});
49-
5045
it('Should use device pixel ratio', () => {
5146
const { result } = renderHook(useDevicePixelRatio);
5247

‎packages/core/src/hooks/useDoubleClick/useDoubleClick.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ targets.forEach((target) => {
2929

3030
afterEach(() => {
3131
vi.useRealTimers();
32-
vi.clearAllMocks();
3332
});
3433

3534
it('Should use double click', () => {

‎packages/core/src/hooks/useLongPress/useLongPress.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ beforeEach(vi.useFakeTimers);
2727

2828
afterEach(() => {
2929
vi.useRealTimers();
30-
vi.clearAllMocks();
3130
});
3231

3332
targets.forEach((target) => {
Lines changed: 55 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { act, renderHook } from '@testing-library/react';
22
import { vi } from 'vitest';
33

4-
import { renderHookServer } from '@/tests';
4+
import { createTrigger, renderHookServer } from '@/tests';
55

66
import { usePerformanceObserver } from './usePerformanceObserver';
77

8-
const mockObserve = vi.fn();
9-
const mockDisconnect = vi.fn();
8+
const PERFORMANCE_OBSERVER_KEY = 'performance-observer';
9+
const trigger = createTrigger<string, PerformanceObserverCallback>();
1010

11-
let observerCallback: PerformanceObserverCallback;
11+
const mockPerformanceObserverObserve = vi.fn();
12+
const mockPerformanceObserverDisconnect = vi.fn();
1213

1314
const createMockPerformanceEntry = (
1415
name = 'test-entry',
@@ -25,17 +26,26 @@ const createMockPerformanceEntry = (
2526

2627
class MockPerformanceObserver {
2728
constructor(callback: PerformanceObserverCallback) {
28-
observerCallback = callback;
29+
this.callback = callback;
2930
}
3031

31-
observe = (options?: PerformanceObserverInit) => mockObserve(options);
32-
disconnect = () => mockDisconnect();
32+
callback: PerformanceObserverCallback;
33+
34+
observe = (options?: PerformanceObserverInit) => {
35+
trigger.add(PERFORMANCE_OBSERVER_KEY, this.callback);
36+
mockPerformanceObserverObserve(options);
37+
};
38+
39+
disconnect = () => {
40+
trigger.delete(PERFORMANCE_OBSERVER_KEY);
41+
mockPerformanceObserverDisconnect();
42+
};
3343
}
3444

3545
globalThis.PerformanceObserver = MockPerformanceObserver as any;
3646

37-
afterEach(() => {
38-
vi.clearAllMocks();
47+
beforeEach(() => {
48+
trigger.clear();
3949
});
4050

4151
it('Should use performance observer', () => {
@@ -45,6 +55,7 @@ it('Should use performance observer', () => {
4555
expect(result.current.entries).toEqual([]);
4656
expect(result.current.start).toBeTypeOf('function');
4757
expect(result.current.stop).toBeTypeOf('function');
58+
expect(result.current.observer).toBeUndefined();
4859
});
4960

5061
it('Should use performance observer on server side', () => {
@@ -54,113 +65,77 @@ it('Should use performance observer on server side', () => {
5465
expect(result.current.entries).toEqual([]);
5566
expect(result.current.start).toBeTypeOf('function');
5667
expect(result.current.stop).toBeTypeOf('function');
68+
expect(result.current.observer).toBeUndefined();
5769
});
5870

59-
it('Should start observing when immediate is true', () => {
71+
it('Should start observing when immediate', () => {
6072
renderHook(() => usePerformanceObserver({ entryTypes: ['measure'], immediate: true }));
6173

62-
expect(mockObserve).toHaveBeenCalledOnce();
63-
expect(mockObserve).toHaveBeenCalledWith(expect.objectContaining({ entryTypes: ['measure'] }));
64-
});
65-
66-
it('Should not start observing when immediate is not set', () => {
67-
renderHook(() => usePerformanceObserver({ entryTypes: ['measure'] }));
68-
69-
expect(mockObserve).not.toHaveBeenCalled();
74+
expect(mockPerformanceObserverObserve).toHaveBeenCalledOnce();
75+
expect(mockPerformanceObserverObserve).toHaveBeenCalledWith(
76+
expect.objectContaining({ entryTypes: ['measure'] })
77+
);
7078
});
7179

72-
it('Should start, stop and restart observer manually', () => {
80+
it('Should restart observer manually', () => {
7381
const { result } = renderHook(() => usePerformanceObserver({ entryTypes: ['measure'] }));
7482

75-
expect(mockObserve).not.toHaveBeenCalled();
76-
77-
act(() => result.current.start());
83+
const entry = createMockPerformanceEntry();
84+
const entryList = {
85+
getEntries: () => [entry]
86+
} as PerformanceObserverEntryList;
7887

79-
expect(mockObserve).toHaveBeenCalledOnce();
88+
act(() => {
89+
result.current.start();
90+
trigger.callback(PERFORMANCE_OBSERVER_KEY, entryList, result.current.observer!);
91+
});
8092

81-
act(() => result.current.stop());
93+
expect(result.current.entries).toEqual([entry]);
8294

83-
expect(mockDisconnect).toHaveBeenCalledOnce();
95+
expect(mockPerformanceObserverObserve).toHaveBeenCalledOnce();
8496

85-
act(() => result.current.start());
97+
act(result.current.stop);
8698

87-
expect(mockObserve).toHaveBeenCalledTimes(2);
99+
expect(mockPerformanceObserverDisconnect).toHaveBeenCalledOnce();
88100
});
89101

90102
it('Should call callback when performance entries are observed', () => {
91103
const callback = vi.fn();
92104

93-
renderHook(() => usePerformanceObserver({ entryTypes: ['measure'], immediate: true }, callback));
94-
95-
const entry = createMockPerformanceEntry();
96-
const entryList = { getEntries: () => [entry] } as PerformanceObserverEntryList;
97-
98-
act(() => observerCallback(entryList, {} as PerformanceObserver));
99-
100-
expect(callback).toHaveBeenCalledOnce();
101-
expect(callback).toHaveBeenCalledWith(entryList, expect.any(Object));
102-
});
103-
104-
it('Should update entries state when performance entries are observed', () => {
105105
const { result } = renderHook(() =>
106-
usePerformanceObserver({ entryTypes: ['measure'], immediate: true })
106+
usePerformanceObserver({ entryTypes: ['measure'] }, callback)
107107
);
108108

109-
expect(result.current.entries).toEqual([]);
109+
const entry = createMockPerformanceEntry();
110+
const entryList = {
111+
getEntries: () => [entry]
112+
} as PerformanceObserverEntryList;
110113

111-
const entry = createMockPerformanceEntry('paint', 'measure', 0, 50);
112-
const entryList = { getEntries: () => [entry] } as PerformanceObserverEntryList;
114+
act(() => {
115+
result.current.start();
116+
trigger.callback(PERFORMANCE_OBSERVER_KEY, entryList, result.current.observer);
117+
});
113118

114-
act(() => observerCallback(entryList, {} as PerformanceObserver));
119+
expect(result.current.observer).toBeTruthy();
115120

116121
expect(result.current.entries).toEqual([entry]);
117-
});
118-
119-
it('Should disconnect observer on unmount', () => {
120-
const { unmount } = renderHook(() =>
121-
usePerformanceObserver({ entryTypes: ['measure'], immediate: true })
122-
);
123-
124-
unmount();
125-
126-
expect(mockDisconnect).toHaveBeenCalledOnce();
122+
expect(callback).toHaveBeenCalledOnce();
127123
});
128124

129125
it('Should not disconnect observer on unmount when not started', () => {
130126
const { unmount } = renderHook(() => usePerformanceObserver({ entryTypes: ['measure'] }));
131127

132128
unmount();
133129

134-
expect(mockDisconnect).not.toHaveBeenCalled();
135-
});
136-
137-
it('Should work without callback', () => {
138-
const { result } = renderHook(() =>
139-
usePerformanceObserver({ entryTypes: ['measure'], immediate: true })
140-
);
141-
142-
const entry = createMockPerformanceEntry();
143-
const entryList = { getEntries: () => [entry] } as PerformanceObserverEntryList;
144-
145-
act(() => observerCallback(entryList, {} as PerformanceObserver));
146-
147-
expect(result.current.entries).toEqual([entry]);
130+
expect(mockPerformanceObserverDisconnect).not.toHaveBeenCalled();
148131
});
149132

150-
it('Should update entries state with multiple entries', () => {
151-
const { result } = renderHook(() =>
133+
it('Should disconnect observer on unmount', () => {
134+
const { unmount } = renderHook(() =>
152135
usePerformanceObserver({ entryTypes: ['measure'], immediate: true })
153136
);
154137

155-
const entries = [
156-
createMockPerformanceEntry('first-paint', 'paint', 0, 10),
157-
createMockPerformanceEntry('api-call', 'measure', 100, 200),
158-
createMockPerformanceEntry('lcp', 'largest-contentful-paint', 50, 150)
159-
];
160-
const entryList = { getEntries: () => entries } as PerformanceObserverEntryList;
161-
162-
act(() => observerCallback(entryList, {} as PerformanceObserver));
138+
unmount();
163139

164-
expect(result.current.entries).toEqual(entries);
165-
expect(result.current.entries).toHaveLength(3);
140+
expect(mockPerformanceObserverDisconnect).toHaveBeenCalledOnce();
166141
});

‎packages/core/src/hooks/usePerformanceObserver/usePerformanceObserver.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const usePerformanceObserver = (
2828
const supported = typeof window !== 'undefined' && !!window.PerformanceObserver;
2929
const [entries, setEntries] = useState<PerformanceEntry[]>([]);
3030

31-
const observerRef = useRef<PerformanceObserver | null>(null);
31+
const observerRef = useRef<PerformanceObserver>(undefined);
3232
const internalCallback = useRef<PerformanceObserverCallback | null>(callback);
3333
internalCallback.current = callback;
3434

@@ -45,7 +45,7 @@ export const usePerformanceObserver = (
4545
const stop = () => {
4646
if (!supported) return;
4747
observerRef.current?.disconnect();
48-
observerRef.current = null;
48+
observerRef.current = undefined;
4949
};
5050

5151
useEffect(() => {
@@ -57,5 +57,5 @@ export const usePerformanceObserver = (
5757
};
5858
}, []);
5959

60-
return { supported, entries, start, stop };
60+
return { supported, entries, start, stop, observer: observerRef.current };
6161
};

0 commit comments

Comments
 (0)