220 lines
6.1 KiB
TypeScript
220 lines
6.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
DeepLinkRouter,
|
|
DeepLinkScreens,
|
|
createBroadcastClient,
|
|
createBroadcastDeepLink,
|
|
createUseBroadcast,
|
|
} from './index.js';
|
|
|
|
const fetchMock = vi.fn();
|
|
|
|
function jsonResponse(body: unknown, status = 200) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
json: () => Promise.resolve(body),
|
|
text: () => Promise.resolve(JSON.stringify(body)),
|
|
};
|
|
}
|
|
|
|
describe('createBroadcastClient', () => {
|
|
beforeEach(() => {
|
|
fetchMock.mockReset();
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('lists messages with default segment and auth headers', async () => {
|
|
fetchMock.mockResolvedValueOnce(jsonResponse({ messages: [{ id: 'm1' }] }));
|
|
|
|
const client = createBroadcastClient({
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAuthToken: () => 'token-123',
|
|
platform: 'web',
|
|
appVersion: '1.2.3',
|
|
osVersion: 'macOS 15',
|
|
});
|
|
|
|
const result = await client.listMessages();
|
|
|
|
expect(result).toEqual({ messages: [{ id: 'm1' }] });
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/broadcasts',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer token-123',
|
|
'x-product-id': 'testapp',
|
|
'x-platform': 'web',
|
|
'x-app-version': '1.2.3',
|
|
'x-os-version': 'macOS 15',
|
|
'x-user-segments': 'free',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('supports async auth token resolution and optional headers', async () => {
|
|
fetchMock.mockResolvedValueOnce(jsonResponse({ messages: [] }));
|
|
|
|
const client = createBroadcastClient({
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAuthToken: async () => 'async-token',
|
|
platform: 'ios',
|
|
appVersion: '2.0.0',
|
|
osVersion: '18.0',
|
|
countryCode: 'US',
|
|
regionCode: 'CA',
|
|
userSegments: ['pro', 'beta'],
|
|
});
|
|
|
|
await client.listMessages();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/broadcasts',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer async-token',
|
|
'x-country-code': 'US',
|
|
'x-region-code': 'CA',
|
|
'x-user-segments': 'pro,beta',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('throws a descriptive error when the API fails', async () => {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
text: () => Promise.resolve('boom'),
|
|
});
|
|
|
|
const client = createBroadcastClient({
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAuthToken: () => 'token-123',
|
|
platform: 'web',
|
|
appVersion: '1.2.3',
|
|
osVersion: 'macOS 15',
|
|
});
|
|
|
|
await expect(client.markRead('message-1')).rejects.toThrow('Broadcast API error: 500 boom');
|
|
});
|
|
|
|
it('polls messages and returns a cleanup function', async () => {
|
|
vi.useFakeTimers();
|
|
fetchMock.mockResolvedValue(jsonResponse({ messages: [] }));
|
|
|
|
const client = createBroadcastClient({
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAuthToken: () => 'token-123',
|
|
platform: 'web',
|
|
appVersion: '1.2.3',
|
|
osVersion: 'macOS 15',
|
|
});
|
|
|
|
const stopPolling = client.pollMessages(1000);
|
|
await vi.advanceTimersByTimeAsync(3000);
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
|
|
stopPolling();
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('returns the same client from createUseBroadcast', () => {
|
|
const client = createBroadcastClient({
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAuthToken: () => 'token-123',
|
|
platform: 'web',
|
|
appVersion: '1.2.3',
|
|
osVersion: 'macOS 15',
|
|
});
|
|
|
|
const useBroadcast = createUseBroadcast(client);
|
|
|
|
expect(useBroadcast()).toEqual({ client });
|
|
});
|
|
});
|
|
|
|
describe('DeepLinkRouter', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('parses custom app deep links using the host as the screen', () => {
|
|
const router = new DeepLinkRouter();
|
|
|
|
expect(router.parseDeepLink('myapp://broadcast?broadcastId=b1&variant=test')).toEqual({
|
|
screen: 'broadcast',
|
|
params: {
|
|
broadcastId: 'b1',
|
|
variant: 'test',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('parses nested deep links from web URLs', () => {
|
|
const router = new DeepLinkRouter();
|
|
|
|
expect(
|
|
router.parseDeepLink('https://app.bytelyst.dev/open?dl=myapp%3A%2F%2Fsurvey%3Fid%3Ds1')
|
|
).toEqual({
|
|
screen: 'survey',
|
|
params: { id: 's1' },
|
|
});
|
|
});
|
|
|
|
it('dispatches to registered handlers and falls back when needed', () => {
|
|
const router = new DeepLinkRouter();
|
|
const primaryHandler = vi.fn();
|
|
const fallbackHandler = vi.fn();
|
|
|
|
router.register(DeepLinkScreens.BROADCAST_DETAIL, primaryHandler);
|
|
router.setFallback(fallbackHandler);
|
|
|
|
expect(router.handle({ screen: DeepLinkScreens.BROADCAST_DETAIL, params: { id: 'b1' } })).toBe(
|
|
true
|
|
);
|
|
expect(primaryHandler).toHaveBeenCalledWith({
|
|
screen: DeepLinkScreens.BROADCAST_DETAIL,
|
|
params: { id: 'b1' },
|
|
});
|
|
|
|
expect(router.handle({ screen: 'unknown' })).toBe(true);
|
|
expect(fallbackHandler).toHaveBeenCalledWith({ screen: 'unknown' });
|
|
});
|
|
|
|
it('returns false and warns when processing an invalid URL without a fallback', () => {
|
|
const router = new DeepLinkRouter();
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
expect(router.process('not-a-url')).toBe(false);
|
|
expect(warnSpy).toHaveBeenCalledWith('[DeepLink] Failed to parse: not-a-url');
|
|
});
|
|
});
|
|
|
|
describe('createBroadcastDeepLink', () => {
|
|
it('builds deep links with params and broadcastId', () => {
|
|
expect(
|
|
createBroadcastDeepLink(
|
|
'https://app.bytelyst.dev',
|
|
DeepLinkScreens.ANNOUNCEMENTS,
|
|
{ tab: 'latest' },
|
|
'b42'
|
|
)
|
|
).toBe('https://app.bytelyst.dev/announcements?tab=latest&broadcastId=b42');
|
|
});
|
|
});
|