import path from 'node:path' import { expect, test, vi, beforeEach } from 'vitest' import type { Protocol } from 'devtools-protocol' import type { CDPSession } from 'puppeteer-core/lib/esm/puppeteer/api/CDPSession.js' import type { Page } from 'puppeteer-core/lib/esm/puppeteer/api/Page.js' import type { HTTPRequest } from 'puppeteer-core/lib/esm/puppeteer/api/HTTPRequest.js' import TraceGatherer from '../../src/gatherer/trace.js' import { FRAME_LOAD_START_TIMEOUT, CLICK_TRANSITION } from '../../src/constants.js' import type { GathererDriver } from '../../src/types.js' import TRACELOG from '../__fixtures__/tracelog.json' with { type: 'json' } vi.mock('lighthouse/lighthouse-core/fraggle-rock/gather/session') vi.mock('lighthouse/lighthouse-core/gather/driver/wait-for-condition') vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) let traceGatherer: TraceGatherer const pageMock = { on: vi.fn(), evaluateOnNewDocument: vi.fn() } const sessionMock = { on: vi.fn(), emit: vi.fn(), send: vi.fn(), off: vi.fn(), _connection: { _transport: { _ws: { addEventListener: vi.fn() } } } } const frame = { id: '123', loaderId: 'foobar123', url: 'http://foobar.com' } const driver = { beginTrace: vi.fn(), endTrace: vi.fn().mockReturnValue(Promise.resolve(TRACELOG)) } as unknown as GathererDriver vi.useFakeTimers() vi.spyOn(global, 'setTimeout') beforeEach(() => { pageMock.on.mockClear() pageMock.evaluateOnNewDocument.mockClear() sessionMock.on.mockClear() traceGatherer = new TraceGatherer( sessionMock as unknown as CDPSession, pageMock as unknown as Page, driver ) traceGatherer.emit = vi.fn() ;(traceGatherer['_driver'] as any).beginTrace.mockClear() vi.mocked(driver.beginTrace).mockClear() vi.mocked(driver.endTrace).mockClear() }) test('should register eventlisteners for network monitor', () => { const statusMonitor = { dispatch: vi.fn() } traceGatherer['_networkStatusMonitor'] = statusMonitor as any for (const fn of Object.values(traceGatherer['_networkListeners'])) { fn({ some: 'params' }) } expect(statusMonitor.dispatch.mock.calls) .toMatchSnapshot() }) test('onFrameLoadFail', () => { traceGatherer.onFrameLoadFail({ frame: () => undefined } as unknown as HTTPRequest) expect(traceGatherer['_failingFrameLoadIds']).toEqual([]) traceGatherer.onFrameLoadFail({ frame: () => ({ url: () => '123' }) } as unknown as HTTPRequest) expect(traceGatherer['_failingFrameLoadIds']).toEqual(['123']) }) test('getter isTracing', () => { expect(traceGatherer.isTracing).toBe(false) traceGatherer['_traceStart'] = Date.now() expect(traceGatherer.isTracing).toBe(true) }) test('finishTracing', async () => { traceGatherer['_pageLoadDetected'] = true traceGatherer['_networkStatusMonitor'] = {} as any traceGatherer['_traceStart'] = 123 traceGatherer['_frameId'] = 'true' traceGatherer['_loaderId'] = 'true' traceGatherer['_pageUrl'] = 'http://some.url' traceGatherer.finishTracing() expect(traceGatherer['_pageLoadDetected']).toBe(false) expect(traceGatherer['_networkStatusMonitor']).toBe(undefined) expect(traceGatherer['_traceStart']).toBe(undefined) expect(traceGatherer['_frameId']).toBe(undefined) expect(traceGatherer['_loaderId']).toBe(undefined) expect(traceGatherer['_pageUrl']).toBe(undefined) expect(sessionMock.off).toHaveBeenCalledTimes(7) expect(traceGatherer.emit).toBeCalledWith('tracingFinished') }) test('onFrameNavigated', () => { expect(traceGatherer['_frameId']).toBe(undefined) expect(traceGatherer['_loaderId']).toBe(undefined) expect(traceGatherer['_pageUrl']).toBe(undefined) traceGatherer['_traceStart'] = Date.now() traceGatherer.onFrameNavigated({ frame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer.emit).toBeCalledWith('tracingStarted', '123') expect(traceGatherer['_frameId']).toBe('123') expect(traceGatherer['_loaderId']).toBe('foobar123') expect(traceGatherer['_pageUrl']).toBe('http://foobar.com') }) test('onFrameNavigated: should not start if frame was detected', () => { traceGatherer['_frameId'] = '123' traceGatherer.onFrameNavigated({ frame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer.emit).toHaveBeenCalledTimes(0) }) test('onFrameNavigated: should not start if a subframe was detected', () => { const subFrame = Object.assign({}, frame, { parentId: 122 }) traceGatherer.onFrameNavigated({ frame: subFrame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer.emit).toHaveBeenCalledTimes(0) }) test('onFrameNavigated: should not start if url is not supported', () => { const subFrame = Object.assign({}, frame, { url: 'data:,foobar' }) traceGatherer.onFrameNavigated({ frame: subFrame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer.emit).toHaveBeenCalledTimes(0) }) test('onFrameNavigated: should not start if tracing is not started', () => { traceGatherer['_traceStart'] = undefined traceGatherer.finishTracing = vi.fn() traceGatherer['_failingFrameLoadIds'].push('123') traceGatherer.onFrameNavigated({ frame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer.emit).toHaveBeenCalledTimes(0) expect(traceGatherer.finishTracing).toHaveBeenCalledTimes(0) }) test('onFrameNavigated: should cancel trace if page load failed', () => { traceGatherer['_traceStart'] = Date.now() traceGatherer.finishTracing = vi.fn() traceGatherer['_failingFrameLoadIds'].push('http://foobar.com') traceGatherer.onFrameNavigated({ frame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer.emit).toHaveBeenCalledWith('tracingError', expect.any(Error)) expect(traceGatherer.finishTracing).toHaveBeenCalledTimes(1) }) test('onFrameNavigated: can detect page load', () => { traceGatherer['_clickTraceTimeout'] = 123 as unknown as NodeJS.Timeout traceGatherer['_traceStart'] = Date.now() expect(traceGatherer['_pageLoadDetected']).toBe(false) traceGatherer.onFrameNavigated({ frame } as unknown as Protocol.Page.FrameNavigatedEvent) expect(traceGatherer['_pageLoadDetected']).toBe(true) }) test('startTracing', async () => { await traceGatherer.startTracing('http://foobar.com') expect(sessionMock.on).toHaveBeenCalledTimes(7) expect(pageMock.evaluateOnNewDocument).toHaveBeenCalledTimes(1) expect((traceGatherer['_driver'] as any).beginTrace).toHaveBeenCalledTimes(1) }) test('startTracing: registers timeout for click events', async () => { traceGatherer.finishTracing = vi.fn() await traceGatherer.startTracing(CLICK_TRANSITION) vi.advanceTimersByTime(FRAME_LOAD_START_TIMEOUT + 10) expect((traceGatherer['_driver'] as any).beginTrace).toHaveBeenCalledTimes(1) expect(traceGatherer.finishTracing).toHaveBeenCalledTimes(1) }) test('completeTracing', async () => { traceGatherer.finishTracing = vi.fn() await traceGatherer.completeTracing() expect(traceGatherer.finishTracing).toHaveBeenCalledTimes(1) expect(traceGatherer.emit).toBeCalledWith('tracingComplete', expect.any(Object)) }) test('completeTracing: in failure case', async () => { traceGatherer.finishTracing = vi.fn() ;(traceGatherer['_driver'] as any).endTrace.mockReturnValue(Promise.reject(new Error('boom'))) await traceGatherer.completeTracing() expect(traceGatherer.finishTracing).toHaveBeenCalledTimes(1) expect(traceGatherer.emit).toBeCalledWith('tracingError', expect.any(Error)) }) test('onLoadEventFired', async () => { await traceGatherer.onLoadEventFired() traceGatherer['_traceStart'] = Date.now() traceGatherer.completeTracing = vi.fn().mockReturnValue(Promise.resolve('yeahh')) vi.advanceTimersByTime(15000) await traceGatherer.onLoadEventFired() expect(traceGatherer.completeTracing).toHaveBeenCalledTimes(1) }) /** * ToDo(Christian): unskip test */ test.skip('onLoadEventFired: using min trace time', async () => { traceGatherer['_traceStart'] = Date.now() - 9700 traceGatherer.completeTracing = vi.fn().mockReturnValue(Promise.resolve('yeahh')) const doneCb = vi.fn() traceGatherer.waitForMaxTimeout = vi.fn().mockReturnValue(new Promise((resolve) => { vi.advanceTimersByTime(150) resolve(doneCb) })) await traceGatherer.onLoadEventFired() expect(doneCb).toBeCalledTimes(1) }) test('waitForMaxTimeout', async () => { traceGatherer.completeTracing = vi.fn() const done = traceGatherer.waitForMaxTimeout(200) vi.advanceTimersByTime(200) ;(await done)() expect(traceGatherer.completeTracing).toHaveBeenCalledTimes(1) })