test(broadcast-client): cover deep links and client
This commit is contained in:
parent
5fe891f472
commit
b8ce14c259
@ -40,8 +40,10 @@ export class DeepLinkRouter {
|
||||
|
||||
// Handle app-specific URLs: myapp://screen/params
|
||||
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
||||
const pathParts = urlObj.pathname.split('/').filter(Boolean);
|
||||
const screen = pathParts[0] || 'home';
|
||||
const routeParts = [urlObj.host, ...urlObj.pathname.split('/').filter(Boolean)].filter(
|
||||
Boolean
|
||||
);
|
||||
const screen = routeParts[0] || 'home';
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
// Parse query params
|
||||
|
||||
219
packages/broadcast-client/src/index.test.ts
Normal file
219
packages/broadcast-client/src/index.test.ts
Normal file
@ -0,0 +1,219 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user