Repository navigation
Expand file tree
/
Copy pathtracking.empty-fallback.test.ts
More file actions
96 lines (80 loc) · 3.52 KB
/
Copy pathtracking.empty-fallback.test.ts
File metadata and controls
96 lines (80 loc) · 3.52 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
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { trackUser } from './tracking';
describe('trackUser Edge Cases & Empty Fallback Tests', () => {
let originalWindow: typeof globalThis.window;
let originalNavigator: typeof globalThis.navigator;
let originalFetch: typeof globalThis.fetch;
let originalBlob: typeof globalThis.Blob;
beforeEach(() => {
originalWindow = globalThis.window;
originalNavigator = globalThis.navigator;
originalFetch = globalThis.fetch;
originalBlob = globalThis.Blob;
globalThis.window = {} as unknown as typeof globalThis.window;
globalThis.navigator = {
sendBeacon: vi.fn().mockReturnValue(true),
} as unknown as typeof globalThis.navigator;
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true });
// Ensure Blob is polyfilled securely to prevent hydration or runtime errors in CI
if (typeof globalThis.Blob === 'undefined') {
globalThis.Blob = class Blob {
constructor(
public parts: string[],
public options: Record<string, string>
) {}
} as unknown as typeof globalThis.Blob;
}
vi.spyOn(console, 'error').mockImplementation(() => undefined);
});
afterEach(() => {
globalThis.window = originalWindow;
globalThis.navigator = originalNavigator;
globalThis.fetch = originalFetch;
globalThis.Blob = originalBlob;
vi.restoreAllMocks();
});
it('1. correctly short-circuits and ignores empty string usernames without throwing', () => {
trackUser('');
expect(globalThis.navigator.sendBeacon).not.toHaveBeenCalled();
expect(globalThis.fetch).not.toHaveBeenCalled();
});
it('2. safely acts as a non-breaking no-op when executing in a headless/server environment (missing window/navigator)', () => {
// Simulate empty server environment without triggering TypeScript readonly errors
const globals = globalThis as unknown as Record<string, unknown>;
delete globals.window;
delete globals.navigator;
expect(() => trackUser('octocat')).not.toThrow();
});
it('3. seamlessly falls back to the fetch API when navigator.sendBeacon is unavailable or undefined', () => {
const nav = globalThis.navigator as unknown as Record<string, unknown>;
delete nav.sendBeacon;
trackUser('octocat');
expect(globalThis.fetch).toHaveBeenCalledWith(
'/api/track-user',
expect.objectContaining({
method: 'POST',
keepalive: true,
})
);
});
it('4. gracefully tolerates beacon queuing failures by automatically deploying the fetch fallback', () => {
// sendBeacon returns false when the queue is full or payload is too large
globalThis.navigator = {
sendBeacon: vi.fn().mockReturnValue(false),
} as unknown as typeof globalThis.navigator;
trackUser('octocat');
expect(globalThis.navigator.sendBeacon).toHaveBeenCalled();
expect(globalThis.fetch).toHaveBeenCalled(); // Triggered fallback
});
it('5. guarantees no unhandled promise rejections if the fallback fetch network request completely fails', async () => {
globalThis.navigator = {
sendBeacon: vi.fn().mockReturnValue(false),
} as unknown as typeof globalThis.navigator;
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network offline'));
// Should not throw or cause unhandled rejections
expect(() => trackUser('octocat')).not.toThrow();
// Wait a tick to ensure async catch block executes cleanly
await new Promise(process.nextTick);
expect(console.error).toHaveBeenCalled();
});
});