Skip to content

Commit ca81fbe

Browse files
committed
Add unit tests for oob_resource_service changes to include web vitals.
1 parent 35e3879 commit ca81fbe

File tree

3 files changed

+275
-51
lines changed

3 files changed

+275
-51
lines changed

packages/performance/src/services/api_service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
import { ERROR_FACTORY, ErrorCode } from '../utils/errors';
1919
import { isIndexedDBAvailable, areCookiesEnabled } from '@firebase/util';
2020
import { consoleLogger } from '../utils/console_logger';
21+
import {
22+
CLSMetricWithAttribution,
23+
INPMetricWithAttribution,
24+
LCPMetricWithAttribution,
25+
onCLS as vitalsOnCLS,
26+
onINP as vitalsOnINP,
27+
onLCP as vitalsOnLCP
28+
} from 'web-vitals/attribution';
2129

2230
declare global {
2331
interface Window {
@@ -47,6 +55,9 @@ export class Api {
4755
private readonly PerformanceObserver: typeof PerformanceObserver;
4856
private readonly windowLocation: Location;
4957
readonly onFirstInputDelay?: (fn: (fid: number) => void) => void;
58+
readonly onLCP: (fn: (metric: LCPMetricWithAttribution) => void) => void;
59+
readonly onINP: (fn: (metric: INPMetricWithAttribution) => void) => void;
60+
readonly onCLS: (fn: (metric: CLSMetricWithAttribution) => void) => void;
5061
readonly localStorage?: Storage;
5162
readonly document: Document;
5263
readonly navigator: Navigator;
@@ -68,6 +79,9 @@ export class Api {
6879
if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) {
6980
this.onFirstInputDelay = window.perfMetrics.onFirstInputDelay;
7081
}
82+
this.onLCP = vitalsOnLCP;
83+
this.onINP = vitalsOnINP;
84+
this.onCLS = vitalsOnCLS;
7185
}
7286

7387
getUrl(): string {

packages/performance/src/services/oob_resources_service.test.ts

Lines changed: 215 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import {
1919
spy,
2020
stub,
21+
restore as sinonRestore,
2122
SinonSpy,
2223
SinonStub,
2324
useFakeTimers,
@@ -26,12 +27,21 @@ import {
2627
import { expect } from 'chai';
2728
import { Api, setupApi, EntryType } from './api_service';
2829
import * as iidService from './iid_service';
29-
import { setupOobResources } from './oob_resources_service';
30+
import { setupOobResources, resetForUnitTests } from './oob_resources_service';
3031
import { Trace } from '../resources/trace';
3132
import '../../test/setup';
3233
import { PerformanceController } from '../controllers/perf';
3334
import { FirebaseApp } from '@firebase/app';
3435
import { FirebaseInstallations } from '@firebase/installations-types';
36+
import { WebVitalMetrics } from '../resources/web_vitals';
37+
import {
38+
CLSAttribution,
39+
CLSMetricWithAttribution,
40+
INPAttribution,
41+
INPMetricWithAttribution,
42+
LCPAttribution,
43+
LCPMetricWithAttribution
44+
} from 'web-vitals/attribution';
3545

3646
describe('Firebase Performance > oob_resources_service', () => {
3747
const MOCK_ID = 'idasdfsffe';
@@ -82,23 +92,36 @@ describe('Firebase Performance > oob_resources_service', () => {
8292

8393
let getIidStub: SinonStub<[], string | undefined>;
8494
let apiGetInstanceSpy: SinonSpy<[], Api>;
95+
let eventListenerSpy: SinonSpy<
96+
[
97+
type: string,
98+
listener: EventListenerOrEventListenerObject,
99+
options?: boolean | AddEventListenerOptions | undefined
100+
],
101+
void
102+
>;
85103
let getEntriesByTypeStub: SinonStub<[EntryType], PerformanceEntry[]>;
86104
let setupObserverStub: SinonStub<
87105
[EntryType, (entry: PerformanceEntry) => void],
88106
void
89107
>;
90-
let createOobTraceStub: SinonStub<
108+
let createOobTraceStub: SinonSpy<
91109
[
92110
PerformanceController,
93111
PerformanceNavigationTiming[],
94112
PerformanceEntry[],
113+
WebVitalMetrics,
95114
(number | undefined)?
96115
],
97116
void
98117
>;
99118
let clock: SinonFakeTimers;
119+
let lcpSpy: SinonSpy<[(m: LCPMetricWithAttribution) => void], void>;
120+
let inpSpy: SinonSpy<[(m: INPMetricWithAttribution) => void], void>;
121+
let clsSpy: SinonSpy<[(m: CLSMetricWithAttribution) => void], void>;
100122

101-
setupApi(self);
123+
const mockWindow = { ...self };
124+
setupApi(mockWindow);
102125

103126
const fakeFirebaseConfig = {
104127
apiKey: 'api-key',
@@ -120,9 +143,22 @@ describe('Firebase Performance > oob_resources_service', () => {
120143
fakeInstallations
121144
);
122145

146+
function callEventListener(name: string): void {
147+
for (let i = eventListenerSpy.callCount; i > 0; i--) {
148+
const [eventName, eventFn] = eventListenerSpy.getCall(i - 1).args;
149+
if (eventName === name) {
150+
if (typeof eventFn === 'function') {
151+
eventFn(new CustomEvent(name));
152+
}
153+
}
154+
}
155+
}
156+
123157
beforeEach(() => {
158+
resetForUnitTests();
124159
getIidStub = stub(iidService, 'getIid');
125-
apiGetInstanceSpy = spy(Api, 'getInstance');
160+
eventListenerSpy = spy(mockWindow.document, 'addEventListener');
161+
126162
clock = useFakeTimers();
127163
getEntriesByTypeStub = stub(Api.prototype, 'getEntriesByType').callsFake(
128164
entry => {
@@ -133,14 +169,24 @@ describe('Firebase Performance > oob_resources_service', () => {
133169
}
134170
);
135171
setupObserverStub = stub(Api.prototype, 'setupObserver');
136-
createOobTraceStub = stub(Trace, 'createOobTrace');
172+
createOobTraceStub = spy(Trace, 'createOobTrace');
173+
const api = Api.getInstance();
174+
lcpSpy = spy(api, 'onLCP');
175+
inpSpy = spy(api, 'onINP');
176+
clsSpy = spy(api, 'onCLS');
177+
apiGetInstanceSpy = spy(Api, 'getInstance');
137178
});
138179

139180
afterEach(() => {
140181
clock.restore();
182+
sinonRestore();
183+
const api = Api.getInstance();
184+
//@ts-ignore Assignment to read-only property.
185+
api.onFirstInputDelay = undefined;
141186
});
142187

143-
describe('setupOobResources', () => {
188+
// eslint-disable-next-line no-restricted-properties
189+
describe.only('setupOobResources', () => {
144190
it('does not start if there is no iid', () => {
145191
getIidStub.returns(undefined);
146192
setupOobResources(performanceController);
@@ -158,18 +204,49 @@ describe('Firebase Performance > oob_resources_service', () => {
158204
expect(setupObserverStub).to.be.calledWith('resource');
159205
});
160206

161-
it('sets up page load trace collection', () => {
207+
it('does not create page load trace before hidden', () => {
162208
getIidStub.returns(MOCK_ID);
163209
setupOobResources(performanceController);
164210
clock.tick(1);
165211

166212
expect(apiGetInstanceSpy).to.be.called;
213+
expect(createOobTraceStub).not.to.be.called;
214+
});
215+
216+
it('creates page load trace after hidden', () => {
217+
getIidStub.returns(MOCK_ID);
218+
setupOobResources(performanceController);
219+
clock.tick(1);
220+
221+
stub(mockWindow.document, 'visibilityState').value('hidden');
222+
callEventListener('visibilitychange');
223+
167224
expect(getEntriesByTypeStub).to.be.calledWith('navigation');
168225
expect(getEntriesByTypeStub).to.be.calledWith('paint');
169226
expect(createOobTraceStub).to.be.calledWithExactly(
170227
performanceController,
171228
[NAVIGATION_PERFORMANCE_ENTRY],
172-
[PAINT_PERFORMANCE_ENTRY]
229+
[PAINT_PERFORMANCE_ENTRY],
230+
{},
231+
undefined
232+
);
233+
});
234+
235+
it('creates page load trace after pagehide', () => {
236+
getIidStub.returns(MOCK_ID);
237+
setupOobResources(performanceController);
238+
clock.tick(1);
239+
240+
callEventListener('pagehide');
241+
242+
expect(getEntriesByTypeStub).to.be.calledWith('navigation');
243+
expect(getEntriesByTypeStub).to.be.calledWith('paint');
244+
expect(createOobTraceStub).to.be.calledWithExactly(
245+
performanceController,
246+
[NAVIGATION_PERFORMANCE_ENTRY],
247+
[PAINT_PERFORMANCE_ENTRY],
248+
{},
249+
undefined
173250
);
174251
});
175252

@@ -181,13 +258,19 @@ describe('Firebase Performance > oob_resources_service', () => {
181258
setupOobResources(performanceController);
182259
clock.tick(1);
183260

261+
// Force the page load event to be sent
262+
stub(mockWindow.document, 'visibilityState').value('hidden');
263+
callEventListener('visibilitychange');
264+
184265
expect(api.onFirstInputDelay).to.be.called;
185266
expect(createOobTraceStub).not.to.be.called;
186267
clock.tick(5000);
187268
expect(createOobTraceStub).to.be.calledWithExactly(
188269
performanceController,
189270
[NAVIGATION_PERFORMANCE_ENTRY],
190-
[PAINT_PERFORMANCE_ENTRY]
271+
[PAINT_PERFORMANCE_ENTRY],
272+
{},
273+
undefined
191274
);
192275
});
193276

@@ -206,10 +289,15 @@ describe('Firebase Performance > oob_resources_service', () => {
206289
clock.tick(1);
207290
firstInputDelayCallback(FIRST_INPUT_DELAY);
208291

292+
// Force the page load event to be sent
293+
stub(mockWindow.document, 'visibilityState').value('hidden');
294+
callEventListener('visibilitychange');
295+
209296
expect(createOobTraceStub).to.be.calledWithExactly(
210297
performanceController,
211298
[NAVIGATION_PERFORMANCE_ENTRY],
212299
[PAINT_PERFORMANCE_ENTRY],
300+
{},
213301
FIRST_INPUT_DELAY
214302
);
215303
});
@@ -223,5 +311,123 @@ describe('Firebase Performance > oob_resources_service', () => {
223311
expect(getEntriesByTypeStub).to.be.calledWith('measure');
224312
expect(setupObserverStub).to.be.calledWith('measure');
225313
});
314+
315+
it('sends LCP metrics with attribution', () => {
316+
getIidStub.returns(MOCK_ID);
317+
setupOobResources(performanceController);
318+
clock.tick(1);
319+
320+
lcpSpy.getCall(-1).args[0]({
321+
value: 12.34,
322+
attribution: {
323+
element: 'some-element'
324+
} as LCPAttribution
325+
} as LCPMetricWithAttribution);
326+
327+
// Force the page load event to be sent
328+
stub(mockWindow.document, 'visibilityState').value('hidden');
329+
callEventListener('visibilitychange');
330+
331+
expect(createOobTraceStub).to.be.calledWithExactly(
332+
performanceController,
333+
[NAVIGATION_PERFORMANCE_ENTRY],
334+
[PAINT_PERFORMANCE_ENTRY],
335+
{
336+
lcp: { value: 12.34, elementAttribution: 'some-element' }
337+
},
338+
undefined
339+
);
340+
});
341+
342+
it('sends INP metrics with attribution', () => {
343+
getIidStub.returns(MOCK_ID);
344+
setupOobResources(performanceController);
345+
clock.tick(1);
346+
347+
inpSpy.getCall(-1).args[0]({
348+
value: 0.198,
349+
attribution: {
350+
interactionTarget: 'another-element'
351+
} as INPAttribution
352+
} as INPMetricWithAttribution);
353+
354+
// Force the page load event to be sent
355+
stub(mockWindow.document, 'visibilityState').value('hidden');
356+
callEventListener('visibilitychange');
357+
358+
expect(createOobTraceStub).to.be.calledWithExactly(
359+
performanceController,
360+
[NAVIGATION_PERFORMANCE_ENTRY],
361+
[PAINT_PERFORMANCE_ENTRY],
362+
{
363+
inp: { value: 0.198, elementAttribution: 'another-element' }
364+
},
365+
undefined
366+
);
367+
});
368+
369+
it('sends CLS metrics with attribution', () => {
370+
getIidStub.returns(MOCK_ID);
371+
setupOobResources(performanceController);
372+
clock.tick(1);
373+
374+
clsSpy.getCall(-1).args[0]({
375+
value: 0.3,
376+
// eslint-disable-next-line
377+
attribution: {
378+
largestShiftTarget: 'large-shift-element'
379+
} as CLSAttribution
380+
} as CLSMetricWithAttribution);
381+
382+
// Force the page load event to be sent
383+
stub(mockWindow.document, 'visibilityState').value('hidden');
384+
callEventListener('visibilitychange');
385+
386+
expect(createOobTraceStub).to.be.calledWithExactly(
387+
performanceController,
388+
[NAVIGATION_PERFORMANCE_ENTRY],
389+
[PAINT_PERFORMANCE_ENTRY],
390+
{
391+
cls: { value: 0.3, elementAttribution: 'large-shift-element' }
392+
},
393+
undefined
394+
);
395+
});
396+
397+
it('sends all core web vitals metrics', () => {
398+
getIidStub.returns(MOCK_ID);
399+
setupOobResources(performanceController);
400+
clock.tick(1);
401+
402+
lcpSpy.getCall(-1).args[0]({
403+
value: 5.91,
404+
attribution: { element: 'an-element' } as LCPAttribution
405+
} as LCPMetricWithAttribution);
406+
inpSpy.getCall(-1).args[0]({
407+
value: 0.1
408+
} as INPMetricWithAttribution);
409+
clsSpy.getCall(-1).args[0]({
410+
value: 0.3,
411+
attribution: {
412+
largestShiftTarget: 'large-shift-element'
413+
} as CLSAttribution
414+
} as CLSMetricWithAttribution);
415+
416+
// Force the page load event to be sent
417+
stub(mockWindow.document, 'visibilityState').value('hidden');
418+
callEventListener('visibilitychange');
419+
420+
expect(createOobTraceStub).to.be.calledWithExactly(
421+
performanceController,
422+
[NAVIGATION_PERFORMANCE_ENTRY],
423+
[PAINT_PERFORMANCE_ENTRY],
424+
{
425+
lcp: { value: 5.91, elementAttribution: 'an-element' },
426+
inp: { value: 0.1, elementAttribution: undefined },
427+
cls: { value: 0.3, elementAttribution: 'large-shift-element' }
428+
},
429+
undefined
430+
);
431+
});
226432
});
227433
});

0 commit comments

Comments
 (0)