-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
89ce6db
commit 4473a06
Showing
18 changed files
with
747 additions
and
16 deletions.
There are no files selected for viewing
107 changes: 107 additions & 0 deletions
107
packages/telemetry/browser-telemetry/__tests__/collectors/dom/ClickCollector.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { UiBreadcrumb } from '../../../src/api/Breadcrumb'; | ||
import { Recorder } from '../../../src/api/Recorder'; | ||
import ClickCollector from '../../../src/collectors/dom/ClickCollector'; | ||
|
||
// Mock the window object | ||
const mockAddEventListener = jest.fn(); | ||
const mockRemoveEventListener = jest.fn(); | ||
|
||
// Mock the document object | ||
const mockDocument = { | ||
body: document.createElement('div'), | ||
}; | ||
|
||
// Setup global mocks | ||
Object.defineProperty(global, 'window', { | ||
value: { | ||
addEventListener: mockAddEventListener, | ||
removeEventListener: mockRemoveEventListener, | ||
}, | ||
writable: true, | ||
}); | ||
global.document = mockDocument as any; | ||
|
||
describe('given a ClickCollector with a mock recorder', () => { | ||
let mockRecorder: Recorder; | ||
let collector: ClickCollector; | ||
let clickHandler: Function; | ||
|
||
beforeEach(() => { | ||
// Reset mocks | ||
mockAddEventListener.mockReset(); | ||
mockRemoveEventListener.mockReset(); | ||
|
||
// Capture the click handler when addEventListener is called | ||
mockAddEventListener.mockImplementation((event, handler) => { | ||
clickHandler = handler; | ||
}); | ||
// Create mock recorder | ||
mockRecorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
|
||
// Create collector | ||
collector = new ClickCollector(); | ||
}); | ||
|
||
it('adds a click event listener when created', () => { | ||
expect(mockAddEventListener).toHaveBeenCalledWith('click', expect.any(Function), true); | ||
}); | ||
|
||
it('registers recorder and uses it for click events', () => { | ||
// Register the recorder | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
// Simulate a click event | ||
const mockTarget = document.createElement('button'); | ||
mockTarget.className = 'test-button'; | ||
document.body.appendChild(mockTarget); | ||
const mockEvent = new MouseEvent('click', { | ||
bubbles: true, | ||
cancelable: true, | ||
}); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
// Call the captured click handler | ||
clickHandler(mockEvent); | ||
|
||
// Verify breadcrumb was added with correct properties | ||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<UiBreadcrumb>({ | ||
class: 'ui', | ||
type: 'click', | ||
level: 'info', | ||
timestamp: expect.any(Number), | ||
message: 'body > button.test-button', | ||
}), | ||
); | ||
}); | ||
|
||
it('stops adding breadcrumbs after unregistering', () => { | ||
// Register then unregister | ||
collector.register(mockRecorder, 'test-session'); | ||
collector.unregister(); | ||
// Simulate click | ||
const mockTarget = document.createElement('button'); | ||
const mockEvent = new MouseEvent('click', { | ||
bubbles: true, | ||
cancelable: true, | ||
}); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
clickHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('does not add a bread crumb for a null target', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockEvent = { target: null } as MouseEvent; | ||
clickHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
}); |
178 changes: 178 additions & 0 deletions
178
packages/telemetry/browser-telemetry/__tests__/collectors/dom/KeyPressCollector.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import { UiBreadcrumb } from '../../../src/api/Breadcrumb'; | ||
import { Recorder } from '../../../src/api/Recorder'; | ||
import KeypressCollector from '../../../src/collectors/dom/KeypressCollector'; | ||
|
||
// Mock the window object | ||
const mockAddEventListener = jest.fn(); | ||
const mockRemoveEventListener = jest.fn(); | ||
|
||
// Mock the document object | ||
const mockDocument = { | ||
body: document.createElement('div'), | ||
}; | ||
|
||
// Setup global mocks | ||
Object.defineProperty(global, 'window', { | ||
value: { | ||
addEventListener: mockAddEventListener, | ||
removeEventListener: mockRemoveEventListener, | ||
}, | ||
writable: true, | ||
}); | ||
global.document = mockDocument as any; | ||
|
||
describe('given a KeypressCollector with a mock recorder', () => { | ||
let mockRecorder: Recorder; | ||
let collector: KeypressCollector; | ||
let keypressHandler: Function; | ||
|
||
beforeEach(() => { | ||
// Reset mocks | ||
mockAddEventListener.mockReset(); | ||
mockRemoveEventListener.mockReset(); | ||
|
||
// Capture the keypress handler when addEventListener is called | ||
mockAddEventListener.mockImplementation((event, handler) => { | ||
keypressHandler = handler; | ||
}); | ||
|
||
// Create mock recorder | ||
mockRecorder = { | ||
addBreadcrumb: jest.fn(), | ||
captureError: jest.fn(), | ||
captureErrorEvent: jest.fn(), | ||
}; | ||
|
||
// Create collector | ||
collector = new KeypressCollector(); | ||
}); | ||
|
||
it('adds a keypress event listener when created', () => { | ||
expect(mockAddEventListener).toHaveBeenCalledWith('keypress', expect.any(Function), true); | ||
}); | ||
|
||
it('registers recorder and uses it for keypress events on input elements', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockTarget = document.createElement('input'); | ||
mockTarget.className = 'test-input'; | ||
document.body.appendChild(mockTarget); | ||
const mockEvent = new KeyboardEvent('keypress'); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
keypressHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<UiBreadcrumb>({ | ||
class: 'ui', | ||
type: 'input', | ||
level: 'info', | ||
timestamp: expect.any(Number), | ||
message: 'body > input.test-input', | ||
}), | ||
); | ||
}); | ||
|
||
it('registers recorder and uses it for keypress events on textarea elements', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockTarget = document.createElement('textarea'); | ||
mockTarget.className = 'test-textarea'; | ||
document.body.appendChild(mockTarget); | ||
const mockEvent = new KeyboardEvent('keypress'); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
keypressHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<UiBreadcrumb>({ | ||
class: 'ui', | ||
type: 'input', | ||
level: 'info', | ||
timestamp: expect.any(Number), | ||
message: 'body > textarea.test-textarea', | ||
}), | ||
); | ||
}); | ||
|
||
it('registers recorder and uses it for keypress events on contentEditable elements', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockTarget = document.createElement('p'); | ||
mockTarget.className = 'test-editable'; | ||
mockTarget.contentEditable = 'true'; | ||
// https://github.com/jsdom/jsdom/issues/1670 | ||
Object.defineProperties(mockTarget, { | ||
isContentEditable: { | ||
value: true, | ||
}, | ||
}); | ||
document.body.appendChild(mockTarget); | ||
const mockEvent = new KeyboardEvent('keypress'); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
keypressHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledWith( | ||
expect.objectContaining<UiBreadcrumb>({ | ||
class: 'ui', | ||
type: 'input', | ||
level: 'info', | ||
timestamp: expect.any(Number), | ||
message: 'body > p.test-editable', | ||
}), | ||
); | ||
}); | ||
|
||
it('does not add breadcrumb for non-input non-editable elements', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockTarget = document.createElement('div'); | ||
const mockEvent = new KeyboardEvent('keypress'); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
keypressHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('stops adding breadcrumbs after unregistering', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
collector.unregister(); | ||
|
||
const mockTarget = document.createElement('input'); | ||
const mockEvent = new KeyboardEvent('keypress'); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
keypressHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('does not add a breadcrumb for a null target', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockEvent = { target: null } as KeyboardEvent; | ||
keypressHandler(mockEvent); | ||
|
||
expect(mockRecorder.addBreadcrumb).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('deduplicates events within throttle time', () => { | ||
collector.register(mockRecorder, 'test-session'); | ||
|
||
const mockTarget = document.createElement('input'); | ||
mockTarget.className = 'test-input'; | ||
document.body.appendChild(mockTarget); | ||
const mockEvent = new KeyboardEvent('keypress'); | ||
Object.defineProperty(mockEvent, 'target', { value: mockTarget }); | ||
|
||
// First event should be recorded | ||
keypressHandler(mockEvent); | ||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledTimes(1); | ||
|
||
// Second event within throttle time should be ignored | ||
keypressHandler(mockEvent); | ||
expect(mockRecorder.addBreadcrumb).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
84 changes: 84 additions & 0 deletions
84
packages/telemetry/browser-telemetry/__tests__/collectors/dom/toSelector.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import toSelector, { elementToString, getClassName } from '../../../src/collectors/dom/toSelector'; | ||
|
||
it.each([ | ||
[{}, undefined], | ||
[{ className: '' }, undefined], | ||
[{ className: 'potato' }, '.potato'], | ||
[{ className: 'cheese potato' }, '.cheese.potato'], | ||
])('can format class names', (element: any, expected?: string) => { | ||
expect(getClassName(element)).toBe(expected); | ||
}); | ||
|
||
it.each([ | ||
[{}, ''], | ||
[{ tagName: 'DIV' }, 'div'], | ||
[{ tagName: 'P', id: 'test' }, 'p#test'], | ||
[{ tagName: 'P', className: 'bold' }, 'p.bold'], | ||
[{ tagName: 'P', className: 'bold', id: 'test' }, 'p#test.bold'], | ||
])('can format an element as a string', (element: any, expected: string) => { | ||
expect(elementToString(element)).toBe(expected); | ||
}); | ||
|
||
it.each([ | ||
[{}, ''], | ||
[undefined, ''], | ||
[null, ''], | ||
['toaster', ''], | ||
[ | ||
{ | ||
tagName: 'BODY', | ||
parentNode: { | ||
tagName: 'HTML', | ||
}, | ||
}, | ||
'body', | ||
], | ||
[ | ||
{ | ||
tagName: 'DIV', | ||
parentNode: { | ||
tagName: 'BODY', | ||
parentNode: { | ||
tagName: 'HTML', | ||
}, | ||
}, | ||
}, | ||
'body > div', | ||
], | ||
[ | ||
{ | ||
tagName: 'DIV', | ||
className: 'cheese taco', | ||
id: 'taco', | ||
parentNode: { | ||
tagName: 'BODY', | ||
parentNode: { | ||
tagName: 'HTML', | ||
}, | ||
}, | ||
}, | ||
'body > div#taco.cheese.taco', | ||
], | ||
])('can produce a CSS selector from a dom element', (element: any, expected: string) => { | ||
expect(toSelector(element)).toBe(expected); | ||
}); | ||
|
||
it('respects max depth', () => { | ||
const element = { | ||
tagName: 'DIV', | ||
className: 'cheese taco', | ||
id: 'taco', | ||
parentNode: { | ||
tagName: 'P', | ||
parentNode: { | ||
tagName: 'BODY', | ||
parentNode: { | ||
tagName: 'HTML', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
expect(toSelector(element, { maxDepth: 1 })).toBe('div#taco.cheese.taco'); | ||
expect(toSelector(element, { maxDepth: 2 })).toBe('p > div#taco.cheese.taco'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.