diff --git a/packages/core/src/browser/addEventListener.spec.ts b/packages/core/src/browser/addEventListener.spec.ts index 75529a84ec..9c822dcf04 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 { isIE } from '../tools/utils/browserDetection' +import type { Configuration } from '../domain/configuration' +import { createNewEvent, mockZoneJs, registerCleanupTask } from '../../test' import type { MockZoneJs } from '../../test' -import { createNewEvent, mockZoneJs } from '../../test' import { noop } from '../tools/utils/functionUtils' import { addEventListener, DOM_EVENT } from './addEventListener' @@ -35,6 +36,62 @@ describe('addEventListener', () => { }) }) + it('Use the EventTarget.prototype.addEventListener when the eventTarget is an instance of EventTarget', () => { + if (isIE()) { + pending('EventTarget not supported in IE') + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalAddEventListener = EventTarget.prototype.addEventListener + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalRemoveEventListener = EventTarget.prototype.removeEventListener + + EventTarget.prototype.addEventListener = jasmine.createSpy() + EventTarget.prototype.removeEventListener = jasmine.createSpy() + + registerCleanupTask(() => { + EventTarget.prototype.addEventListener = originalAddEventListener + EventTarget.prototype.removeEventListener = originalRemoveEventListener + }) + + 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() + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(EventTarget.prototype.addEventListener).toHaveBeenCalled() + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(EventTarget.prototype.removeEventListener).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/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/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/mockReportingObserver.ts b/packages/core/test/emulate/mockReportingObserver.ts index 1f034f4e62..a278ece395 100644 --- a/packages/core/test/emulate/mockReportingObserver.ts +++ b/packages/core/test/emulate/mockReportingObserver.ts @@ -50,8 +50,16 @@ export function mockReportingObserver() { export type MockCspEventListener = ReturnType export function mockCspEventListener() { - spyOn(document, 'addEventListener').and.callFake((_type: string, listener: EventListener) => { - listeners.push(listener) + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalAddEventListener = EventTarget.prototype.addEventListener + EventTarget.prototype.addEventListener = jasmine + .createSpy() + .and.callFake((_type: string, listener: EventListener) => { + listeners.push(listener) + }) + + registerCleanupTask(() => { + EventTarget.prototype.addEventListener = originalAddEventListener }) const listeners: EventListener[] = [] 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/core/test/index.ts b/packages/core/test/index.ts index 33562e4622..9da1721c13 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 './emulate/mockRequestIdleCallback' export * from './typeUtils' export * from './coreConfiguration' diff --git a/packages/rum-core/src/domain/error/trackReportError.spec.ts b/packages/rum-core/src/domain/error/trackReportError.spec.ts index 095897b501..843a8ac8a8 100644 --- a/packages/rum-core/src/domain/error/trackReportError.spec.ts +++ b/packages/rum-core/src/domain/error/trackReportError.spec.ts @@ -23,6 +23,9 @@ describe('trackReportError', () => { let configuration: RumConfiguration beforeEach(() => { + if (!window.ReportingObserver) { + pending('ReportingObserver not supported') + } configuration = mockRumConfiguration() errorObservable = new Observable() notifyLog = jasmine.createSpy('notifyLog') 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 } })) } }