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'); }); });