Skip to content

Commit b7bdd54

Browse files
test(tracking-empty-fallback): verify Edge Cases & Empty/Missing Inputs Verification
1 parent 7393ddf commit b7bdd54

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { trackUser } from './tracking';
3+
4+
describe('trackUser Edge Cases & Empty Fallback Tests', () => {
5+
let originalWindow: typeof globalThis.window;
6+
let originalNavigator: typeof globalThis.navigator;
7+
let originalFetch: typeof globalThis.fetch;
8+
let originalBlob: typeof globalThis.Blob;
9+
10+
beforeEach(() => {
11+
originalWindow = globalThis.window;
12+
originalNavigator = globalThis.navigator;
13+
originalFetch = globalThis.fetch;
14+
originalBlob = globalThis.Blob;
15+
16+
globalThis.window = {} as unknown as typeof globalThis.window;
17+
globalThis.navigator = {
18+
sendBeacon: vi.fn().mockReturnValue(true),
19+
} as unknown as typeof globalThis.navigator;
20+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true });
21+
22+
// Ensure Blob is polyfilled securely to prevent hydration or runtime errors in CI
23+
if (typeof globalThis.Blob === 'undefined') {
24+
globalThis.Blob = class Blob {
25+
constructor(
26+
public parts: string[],
27+
public options: Record<string, string>
28+
) {}
29+
} as unknown as typeof globalThis.Blob;
30+
}
31+
32+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
33+
});
34+
35+
afterEach(() => {
36+
globalThis.window = originalWindow;
37+
globalThis.navigator = originalNavigator;
38+
globalThis.fetch = originalFetch;
39+
globalThis.Blob = originalBlob;
40+
vi.restoreAllMocks();
41+
});
42+
43+
it('1. correctly short-circuits and ignores empty string usernames without throwing', () => {
44+
trackUser('');
45+
expect(globalThis.navigator.sendBeacon).not.toHaveBeenCalled();
46+
expect(globalThis.fetch).not.toHaveBeenCalled();
47+
});
48+
49+
it('2. safely acts as a non-breaking no-op when executing in a headless/server environment (missing window/navigator)', () => {
50+
// Simulate empty server environment without triggering TypeScript readonly errors
51+
const globals = globalThis as unknown as Record<string, unknown>;
52+
delete globals.window;
53+
delete globals.navigator;
54+
55+
expect(() => trackUser('octocat')).not.toThrow();
56+
});
57+
58+
it('3. seamlessly falls back to the fetch API when navigator.sendBeacon is unavailable or undefined', () => {
59+
const nav = globalThis.navigator as unknown as Record<string, unknown>;
60+
delete nav.sendBeacon;
61+
62+
trackUser('octocat');
63+
expect(globalThis.fetch).toHaveBeenCalledWith(
64+
'/api/track-user',
65+
expect.objectContaining({
66+
method: 'POST',
67+
keepalive: true,
68+
})
69+
);
70+
});
71+
72+
it('4. gracefully tolerates beacon queuing failures by automatically deploying the fetch fallback', () => {
73+
// sendBeacon returns false when the queue is full or payload is too large
74+
globalThis.navigator = {
75+
sendBeacon: vi.fn().mockReturnValue(false),
76+
} as unknown as typeof globalThis.navigator;
77+
78+
trackUser('octocat');
79+
expect(globalThis.navigator.sendBeacon).toHaveBeenCalled();
80+
expect(globalThis.fetch).toHaveBeenCalled(); // Triggered fallback
81+
});
82+
83+
it('5. guarantees no unhandled promise rejections if the fallback fetch network request completely fails', async () => {
84+
globalThis.navigator = {
85+
sendBeacon: vi.fn().mockReturnValue(false),
86+
} as unknown as typeof globalThis.navigator;
87+
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network offline'));
88+
89+
// Should not throw or cause unhandled rejections
90+
expect(() => trackUser('octocat')).not.toThrow();
91+
92+
// Wait a tick to ensure async catch block executes cleanly
93+
await new Promise(process.nextTick);
94+
expect(console.error).toHaveBeenCalled();
95+
});
96+
});

0 commit comments

Comments
 (0)