import type { RawTelemetryEvent } from '@datadog/browser-core' import { display, isIE, noop, resetTelemetry, startFakeTelemetry } from '@datadog/browser-core' import { MockWorker } from '../../../test/utils' import type { createDeflateWorker } from './deflateWorker' import { startDeflateWorker, resetDeflateWorkerState } from './startDeflateWorker' describe('startDeflateWorker', () => { let deflateWorker: MockWorker let createDeflateWorkerSpy: jasmine.Spy let callbackSpy: jasmine.Spy beforeEach(() => { deflateWorker = new MockWorker() callbackSpy = jasmine.createSpy('callbackSpy') createDeflateWorkerSpy = jasmine.createSpy('createDeflateWorkerSpy').and.callFake(() => deflateWorker) }) afterEach(() => { resetDeflateWorkerState() }) it('creates a deflate worker and call callback when initialized', () => { startDeflateWorker(callbackSpy, createDeflateWorkerSpy) expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) deflateWorker.processAllMessages() expect(callbackSpy).toHaveBeenCalledOnceWith(deflateWorker) }) it('uses the previously created worker', () => { startDeflateWorker(noop, createDeflateWorkerSpy) deflateWorker.processAllMessages() startDeflateWorker(callbackSpy, createDeflateWorkerSpy) expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) deflateWorker.processAllMessages() expect(callbackSpy).toHaveBeenCalledOnceWith(deflateWorker) }) describe('loading state', () => { it('does not create multiple workers when called multiple times while the worker is loading', () => { startDeflateWorker(noop, createDeflateWorkerSpy) startDeflateWorker(noop, createDeflateWorkerSpy) expect(createDeflateWorkerSpy).toHaveBeenCalledTimes(1) }) it('calls all registered callbacks when the worker is initialized', () => { const callbackSpy1 = jasmine.createSpy() const callbackSpy2 = jasmine.createSpy() startDeflateWorker(callbackSpy1, createDeflateWorkerSpy) startDeflateWorker(callbackSpy2, createDeflateWorkerSpy) deflateWorker.processAllMessages() expect(callbackSpy1).toHaveBeenCalledOnceWith(deflateWorker) expect(callbackSpy2).toHaveBeenCalledOnceWith(deflateWorker) }) }) describe('worker CSP error', () => { let telemetryEvents: RawTelemetryEvent[] // mimic Chrome behavior let CSP_ERROR: DOMException let displaySpy: jasmine.Spy beforeEach(() => { if (isIE()) { pending('IE does not support CSP blocking worker creation') } displaySpy = spyOn(display, 'error') telemetryEvents = startFakeTelemetry() CSP_ERROR = new DOMException( "Failed to construct 'Worker': Access to the script at 'blob:https://example.org/9aadbb61-effe-41ee-aa76-fc607053d642' is denied by the document's Content Security Policy." ) }) afterEach(() => { resetTelemetry() }) it('displays CSP instructions when the worker creation throws a CSP error', () => { startDeflateWorker(noop, () => { throw CSP_ERROR }) expect(displaySpy).toHaveBeenCalledWith( 'Please make sure CSP is correctly configured https://docs.datadoghq.com/real_user_monitoring/faq/content_security_policy' ) }) it('does not report CSP errors to telemetry', () => { startDeflateWorker(noop, () => { throw CSP_ERROR }) expect(telemetryEvents).toEqual([]) }) it('displays ErrorEvent as CSP error', () => { startDeflateWorker(noop, createDeflateWorkerSpy) deflateWorker.dispatchErrorEvent() expect(displaySpy).toHaveBeenCalledWith( 'Please make sure CSP is correctly configured https://docs.datadoghq.com/real_user_monitoring/faq/content_security_policy' ) }) it('calls the callback without argument in case of an error occurs during loading', () => { startDeflateWorker(callbackSpy, createDeflateWorkerSpy) deflateWorker.dispatchErrorEvent() expect(callbackSpy).toHaveBeenCalledOnceWith() }) it('calls the callback without argument in case of an error occurred in a previous loading', () => { startDeflateWorker(noop, createDeflateWorkerSpy) deflateWorker.dispatchErrorEvent() startDeflateWorker(callbackSpy, createDeflateWorkerSpy) expect(callbackSpy).toHaveBeenCalledOnceWith() }) }) describe('worker unknown error', () => { let telemetryEvents: RawTelemetryEvent[] const UNKNOWN_ERROR = new Error('boom') let displaySpy: jasmine.Spy beforeEach(() => { displaySpy = spyOn(display, 'error') telemetryEvents = startFakeTelemetry() }) afterEach(() => { resetTelemetry() }) it('displays an error message when the worker creation throws an unknown error', () => { startDeflateWorker(noop, () => { throw UNKNOWN_ERROR }) expect(displaySpy).toHaveBeenCalledOnceWith( 'Session Replay recording failed to start: an error occurred while creating the Worker:', UNKNOWN_ERROR ) }) it('reports unknown errors to telemetry', () => { startDeflateWorker(noop, () => { throw UNKNOWN_ERROR }) expect(telemetryEvents).toEqual([ { status: 'error' as any, message: 'boom', error: { kind: 'Error', stack: jasmine.any(String) } }, ]) }) it('does not display error messages as CSP error', () => { startDeflateWorker(noop, createDeflateWorkerSpy) deflateWorker.dispatchErrorMessage('foo') expect(displaySpy).not.toHaveBeenCalledWith( 'Please make sure CSP is correctly configured https://docs.datadoghq.com/real_user_monitoring/faq/content_security_policy' ) }) it('reports errors occurring after loading to telemetry', () => { startDeflateWorker(noop, createDeflateWorkerSpy) deflateWorker.processAllMessages() deflateWorker.dispatchErrorMessage('boom') expect(telemetryEvents).toEqual([ { status: 'error' as any, message: 'Uncaught "boom"', error: { stack: jasmine.any(String) } }, ]) }) }) })