From 2e397c9ec9654266c940a9d8e33a4bf4f6031f5d Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 15 Nov 2024 11:13:47 +0100 Subject: [PATCH 1/3] Share EventTarget mock --- packages/core/test/emulate/mockEventTarget.ts | 28 +++++++ packages/core/test/emulate/mockXhr.ts | 31 +------- packages/rum/test/mockWorker.ts | 76 ++++++------------- 3 files changed, 55 insertions(+), 80 deletions(-) create mode 100644 packages/core/test/emulate/mockEventTarget.ts diff --git a/packages/core/test/emulate/mockEventTarget.ts b/packages/core/test/emulate/mockEventTarget.ts new file mode 100644 index 0000000000..591acce9cd --- /dev/null +++ b/packages/core/test/emulate/mockEventTarget.ts @@ -0,0 +1,28 @@ +export class MockEventTarget { + public listeners: { [k: string]: EventListener[] } = {} + + addEventListener(type: string, listener: EventListener, _options?: boolean | AddEventListenerOptions): void { + if (!this.listeners[type]) { + this.listeners[type] = [] + } + + this.listeners[type].push(listener) + } + + removeEventListener(type: string, listener: EventListenerOrEventListenerObject) { + if (!this.listeners[type]) { + throw new Error(`Can't remove a listener. Event "${type}" doesn't exits.`) + } + + this.listeners[type] = this.listeners[type].filter((lst) => listener !== lst) + } + + dispatchEvent(event: Event): boolean { + if (this.listeners[event.type]) { + this.listeners[event.type].forEach((listener) => { + listener.apply(this, [event]) + }) + } + return true + } +} diff --git a/packages/core/test/emulate/mockXhr.ts b/packages/core/test/emulate/mockXhr.ts index b76e0f443c..c6f51f9862 100644 --- a/packages/core/test/emulate/mockXhr.ts +++ b/packages/core/test/emulate/mockXhr.ts @@ -1,6 +1,7 @@ import { isServerError, noop } from '../../src' import { registerCleanupTask } from '../registerCleanupTask' import { createNewEvent } from './createNewEvent' +import { MockEventTarget } from './mockEventTarget' export function mockXhr() { const originalXhr = XMLHttpRequest @@ -30,35 +31,7 @@ export function withXhr({ setup(xhr as unknown as MockXhr) } -class MockEventEmitter { - public listeners: { [k: string]: Array<(event: Event) => void> } = {} - - addEventListener(name: string, callback: () => void) { - if (!this.listeners[name]) { - this.listeners[name] = [] - } - - this.listeners[name].push(callback) - } - - removeEventListener(name: string, callback: () => void) { - if (!this.listeners[name]) { - throw new Error(`Can't remove a listener. Event "${name}" doesn't exits.`) - } - - this.listeners[name] = this.listeners[name].filter((listener) => listener !== callback) - } - - dispatchEvent(evt: Event) { - if (!this.listeners[evt.type]) { - return - } - - this.listeners[evt.type].forEach((listener) => listener.apply(this, [evt])) - } -} - -export class MockXhr extends MockEventEmitter { +export class MockXhr extends MockEventTarget { public static onSend: (xhr: MockXhr) => void | undefined public response: string | undefined = undefined public responseText: string | undefined = undefined diff --git a/packages/rum/test/mockWorker.ts b/packages/rum/test/mockWorker.ts index 9c94d94b09..19236a2bd0 100644 --- a/packages/rum/test/mockWorker.ts +++ b/packages/rum/test/mockWorker.ts @@ -1,10 +1,8 @@ -import type { DeflateWorker, DeflateWorkerAction, DeflateWorkerResponse } from '@datadog/browser-core' +import type { DeflateWorker, DeflateWorkerAction } from '@datadog/browser-core' import { string2buf } from '../../worker/src/domain/deflate' -import { createNewEvent } from '../../core/test' +import { createNewEvent, MockEventTarget } from '../../core/test' -type DeflateWorkerListener = (event: { data: DeflateWorkerResponse }) => void - -export class MockWorker implements DeflateWorker { +export class MockWorker extends MockEventTarget implements DeflateWorker { public onmessage = null public onmessageerror = null public onerror = null @@ -12,23 +10,6 @@ export class MockWorker implements DeflateWorker { readonly pendingMessages: DeflateWorkerAction[] = [] private streams = new Map() - private listeners: { - message: Set - error: Set<(error: unknown) => void> - } = { message: new Set(), error: new Set() } - - addEventListener(eventName: 'message' | 'error', listener: any): void { - this.listeners[eventName].add(listener) - } - - removeEventListener(eventName: 'message' | 'error', listener: any): void { - this.listeners[eventName].delete(listener) - } - - dispatchEvent(): boolean { - // Partial implementation, feel free to implement - throw new Error('not yet implemented') - } postMessage(message: DeflateWorkerAction): void { this.pendingMessages.push(message) @@ -43,7 +24,7 @@ export class MockWorker implements DeflateWorker { } get messageListenersCount() { - return this.listeners.message.size + return this.listeners.message.length } processAllMessages(): void { @@ -61,15 +42,13 @@ export class MockWorker implements DeflateWorker { if (message) { switch (message.action) { case 'init': - this.listeners.message.forEach((listener) => - listener( - createNewEvent('message', { - data: { - type: 'initialized', - version: 'dev', - }, - }) - ) + this.dispatchEvent( + createNewEvent('message', { + data: { + type: 'initialized', + version: 'dev', + }, + }) ) break case 'write': @@ -82,20 +61,17 @@ export class MockWorker implements DeflateWorker { // In the mock worker, for simplicity, we'll just use the UTF-8 encoded string instead of deflating it. const binaryData = string2buf(message.data) stream.push(binaryData) - - this.listeners.message.forEach((listener) => - listener( - createNewEvent('message', { - data: { - type: 'wrote', - id: message.id, - streamId: message.streamId, - result: binaryData, - trailer: new Uint8Array([32]), // emulate a trailer with a single space - additionalBytesCount: binaryData.length, - }, - }) - ) + this.dispatchEvent( + createNewEvent('message', { + data: { + type: 'wrote', + id: message.id, + streamId: message.streamId, + result: binaryData, + trailer: new Uint8Array([32]), // emulate a trailer with a single space + additionalBytesCount: binaryData.length, + }, + }) ) } break @@ -107,13 +83,11 @@ export class MockWorker implements DeflateWorker { } dispatchErrorEvent() { - const error = createNewEvent('worker') - this.listeners.error.forEach((listener) => listener(error)) + const error = createNewEvent('error') + this.dispatchEvent(error) } dispatchErrorMessage(error: Error | string, streamId?: number) { - this.listeners.message.forEach((listener) => - listener(createNewEvent('message', { data: { type: 'errored', error, streamId } })) - ) + this.dispatchEvent(createNewEvent('message', { data: { type: 'errored', error, streamId } })) } } From 91bace6c08a5bb847f034c80264b03fc2a0f105d Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 15 Nov 2024 11:14:15 +0100 Subject: [PATCH 2/3] Use the window.EventTarget.prototype when possible to avoid wrong overrides --- .../core/src/browser/addEventListener.spec.ts | 53 ++++++++++++++++++- packages/core/src/browser/addEventListener.ts | 8 ++- .../test/emulate/mockReportingObserver.ts | 9 ++-- packages/core/test/index.ts | 1 + 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/packages/core/src/browser/addEventListener.spec.ts b/packages/core/src/browser/addEventListener.spec.ts index 75529a84ec..f83a4049bc 100644 --- a/packages/core/src/browser/addEventListener.spec.ts +++ b/packages/core/src/browser/addEventListener.spec.ts @@ -1,6 +1,7 @@ -import type { Configuration } from '@datadog/browser-core' -import type { MockZoneJs } from '../../test' +import { isIE } from '../tools/utils/browserDetection' +import type { Configuration } from '../domain/configuration' import { createNewEvent, mockZoneJs } from '../../test' +import type { MockZoneJs } from '../../test' import { noop } from '../tools/utils/functionUtils' import { addEventListener, DOM_EVENT } from './addEventListener' @@ -35,6 +36,54 @@ describe('addEventListener', () => { }) }) + it('Use the EventTarget.prototype.addEventListener when the eventTarget is an instance of EventTarget', () => { + if (isIE()) { + pending('EventTarget not supported in IE') + } + + const addEventListenerSpy = jasmine.createSpy() + const removeEventListenerSpy = jasmine.createSpy() + + // The stopLeakDetection function of the global after each hook will reset the EventTarget.prototype.addEventListener + EventTarget.prototype.addEventListener = addEventListenerSpy + EventTarget.prototype.removeEventListener = removeEventListenerSpy + + const htmlDivElement = document.createElement('div') + htmlDivElement.addEventListener = jasmine.createSpy() + htmlDivElement.removeEventListener = jasmine.createSpy() + + const { stop } = addEventListener({ allowUntrustedEvents: false }, htmlDivElement, DOM_EVENT.CLICK, noop) + + const event = createNewEvent(DOM_EVENT.CLICK) + htmlDivElement.dispatchEvent(event) + stop() + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(htmlDivElement.addEventListener).not.toHaveBeenCalled() + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(htmlDivElement.removeEventListener).not.toHaveBeenCalled() + + expect(addEventListenerSpy).toHaveBeenCalled() + expect(removeEventListenerSpy).toHaveBeenCalled() + }) + + it('Use the addEventListener method when the eventTarget is not an instance of EventTarget', () => { + const listener = jasmine.createSpy() + + const customEventTarget = { + addEventListener: jasmine.createSpy(), + removeEventListener: jasmine.createSpy(), + } as unknown as EventTarget + + const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', listener) + stop() + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(customEventTarget.addEventListener).toHaveBeenCalled() + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(customEventTarget.removeEventListener).toHaveBeenCalled() + }) + describe('Untrusted event', () => { beforeEach(() => { configuration = { allowUntrustedEvents: false } as Configuration diff --git a/packages/core/src/browser/addEventListener.ts b/packages/core/src/browser/addEventListener.ts index 3cec1a367e..6fb2c43ba5 100644 --- a/packages/core/src/browser/addEventListener.ts +++ b/packages/core/src/browser/addEventListener.ts @@ -127,11 +127,15 @@ export function addEventListeners add.call(eventTarget, eventName, listenerWithMonitor, options)) function stop() { - const remove = getZoneJsOriginalValue(eventTarget, 'removeEventListener') + const remove = getZoneJsOriginalValue(listenerTarget, 'removeEventListener') eventNames.forEach((eventName) => remove.call(eventTarget, eventName, listenerWithMonitor, options)) } diff --git a/packages/core/test/emulate/mockReportingObserver.ts b/packages/core/test/emulate/mockReportingObserver.ts index 1f034f4e62..0d9b6e5d1b 100644 --- a/packages/core/test/emulate/mockReportingObserver.ts +++ b/packages/core/test/emulate/mockReportingObserver.ts @@ -50,9 +50,12 @@ export function mockReportingObserver() { export type MockCspEventListener = ReturnType export function mockCspEventListener() { - spyOn(document, 'addEventListener').and.callFake((_type: string, listener: EventListener) => { - listeners.push(listener) - }) + // The stopLeakDetection function of the global after each hook will reset the EventTarget.prototype.addEventListener + EventTarget.prototype.addEventListener = jasmine + .createSpy() + .and.callFake((_type: string, listener: EventListener) => { + listeners.push(listener) + }) const listeners: EventListener[] = [] diff --git a/packages/core/test/index.ts b/packages/core/test/index.ts index 889042cb2d..b1ad2f7128 100644 --- a/packages/core/test/index.ts +++ b/packages/core/test/index.ts @@ -18,6 +18,7 @@ export * from './emulate/mockFlushController' export * from './emulate/mockExperimentalFeatures' export * from './emulate/mockFetch' export * from './emulate/mockXhr' +export * from './emulate/mockEventTarget' export * from './typeUtils' export * from './coreConfiguration' export * from './disableJasmineUncaughtExceptionTracking' From c362ac27e0620cb3126e271319f0792da99afb06 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Fri, 15 Nov 2024 15:23:36 +0100 Subject: [PATCH 3/3] Fix IE tests --- packages/core/src/domain/report/reportObservable.spec.ts | 3 +++ packages/rum-core/src/domain/error/trackReportError.spec.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/core/src/domain/report/reportObservable.spec.ts b/packages/core/src/domain/report/reportObservable.spec.ts index 8a2af76243..ae5b37c614 100644 --- a/packages/core/src/domain/report/reportObservable.spec.ts +++ b/packages/core/src/domain/report/reportObservable.spec.ts @@ -14,6 +14,9 @@ describe('report observable', () => { let configuration: Configuration beforeEach(() => { + if (!window.ReportingObserver) { + pending('ReportingObserver not supported') + } configuration = {} as Configuration reportingObserver = mockReportingObserver() cspEventListener = mockCspEventListener() diff --git a/packages/rum-core/src/domain/error/trackReportError.spec.ts b/packages/rum-core/src/domain/error/trackReportError.spec.ts index c5a477d3b3..b947b1e69d 100644 --- a/packages/rum-core/src/domain/error/trackReportError.spec.ts +++ b/packages/rum-core/src/domain/error/trackReportError.spec.ts @@ -22,6 +22,9 @@ describe('trackReportError', () => { let configuration: RumConfiguration beforeEach(() => { + if (!window.ReportingObserver) { + pending('ReportingObserver not supported') + } configuration = mockRumConfiguration() errorObservable = new Observable() notifyLog = jasmine.createSpy('notifyLog')