-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathcatcher.test.ts
More file actions
157 lines (123 loc) · 6.27 KB
/
catcher.test.ts
File metadata and controls
157 lines (123 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import Catcher from '../src/catcher';
import { BreadcrumbManager } from '../src/addons/breadcrumbs';
import { TEST_TOKEN, wait, createTransport, getLastPayload, createCatcher } from './catcher.helpers';
// StackParser is mocked to prevent real network calls to source files in the jsdom environment.
const mockParse = vi.hoisted(() => vi.fn().mockResolvedValue([]));
vi.mock('@hawk.so/core', async (importOriginal) => {
const actual = await importOriginal<typeof import('@hawk.so/core')>();
return { ...actual, StackParser: class { parse = mockParse; } };
});
// ─── Tests ───────────────────────────────────────────────────────────────────
describe('Catcher', () => {
beforeEach(() => {
localStorage.clear();
mockParse.mockResolvedValue([]);
(BreadcrumbManager as any).instance = null;
});
// ── Constructor variants ──────────────────────────────────────────────────
//
// The Catcher can be initialized with either a full settings object or a
// bare string token as a shorthand.
describe('constructor', () => {
const listeners: Array<[string, EventListenerOrEventListenerObject]> = [];
beforeEach(() => {
const orig = window.addEventListener.bind(window);
vi.spyOn(window, 'addEventListener').mockImplementation(
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => {
listeners.push([type, listener]);
return orig(type, listener, options as AddEventListenerOptions);
}
);
});
afterEach(() => {
vi.restoreAllMocks();
for (const [type, listener] of listeners) {
window.removeEventListener(type, listener as EventListener);
}
listeners.length = 0;
});
it('should not throw when token provided via plain string shorthand', () => {
expect(() => new Catcher(TEST_TOKEN)).not.toThrow();
});
it('should throw when integration token contains malformed JSON', () => {
// getIntegrationId() tries JSON.parse(atob(token)); malformed JSON triggers the catch path.
expect(() => new Catcher({ token: btoa('not-json') })).toThrow('Invalid integration token.');
});
it('should throw when integration token has no integrationId field', () => {
// Valid base64 JSON but missing the integrationId property — inner guard throws.
const tokenWithoutId = btoa(JSON.stringify({ secret: 'abc' }));
expect(() => new Catcher({ token: tokenWithoutId })).toThrow('Invalid integration token.');
});
});
// ── Error delivery ────────────────────────────────────────────────────────
//
// The Catcher's primary responsibility: capture errors and forward them to
// the configured transport with identifying metadata.
describe('error delivery', () => {
it('should send payload composed from Error instance', async () => {
const { sendSpy, transport } = createTransport();
createCatcher(transport).send(new Error('something broke'));
await wait();
expect(sendSpy).toHaveBeenCalledOnce();
expect(sendSpy.mock.calls[0][0].token).toBe(TEST_TOKEN);
expect(sendSpy.mock.calls[0][0].catcherType).toBe('errors/javascript');
expect(getLastPayload(sendSpy).title).toBe('something broke');
});
it('should send payload composed from string', async () => {
const { sendSpy, transport } = createTransport();
createCatcher(transport).send('unhandled rejection reason');
await wait();
expect(sendSpy).toHaveBeenCalledOnce();
expect(sendSpy.mock.calls[0][0].token).toBe(TEST_TOKEN);
expect(sendSpy.mock.calls[0][0].catcherType).toBe('errors/javascript');
expect(getLastPayload(sendSpy).title).toBe('unhandled rejection reason');
});
it('should not send payload for same Error instance twice', async () => {
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport);
const error = new Error('duplicate');
hawk.send(error);
hawk.send(error);
await wait();
expect(sendSpy).toHaveBeenCalledTimes(1);
});
it('should send payload for distinct Error instances independently', async () => {
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport);
hawk.send(new Error('first'));
hawk.send(new Error('second'));
await wait();
expect(sendSpy).toHaveBeenCalledTimes(2);
});
it('should send payload for same strings without deduplication', async () => {
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport);
hawk.send('reason');
hawk.send('reason');
await wait();
expect(sendSpy).toHaveBeenCalledTimes(2);
});
});
// ── test() convenience method ─────────────────────────────────────────────
describe('test()', () => {
it('should send a predefined test error event', async () => {
const { sendSpy, transport } = createTransport();
createCatcher(transport).test();
await wait();
expect(sendSpy).toHaveBeenCalledOnce();
expect(getLastPayload(sendSpy).title).toContain('Hawk JavaScript Catcher test message');
});
});
// ── Backtrace ─────────────────────────────────────────────────────────────
describe('backtrace', () => {
it('should omit backtrace when stack parsing throws', async () => {
mockParse.mockRejectedValueOnce(new Error('parse failed'));
const { sendSpy, transport } = createTransport();
createCatcher(transport).send(new Error('stack parse failure'));
await wait();
expect(getLastPayload(sendSpy).title).toBe('stack parse failure');
expect(sendSpy).toHaveBeenCalledOnce();
});
});
});