import { afterEach, describe, expect, it, vi } from 'vitest'; import { collectTelegramDiag, insideTelegram, loadTelegramSDK, telegramSdkOutcome, routeExternalLinkInTelegram, telegramLaunch, telegramOpenExternalLink, } from './telegram'; function stubWebApp(initData: string, startParam?: string) { vi.stubGlobal('window', { Telegram: { WebApp: { initData, initDataUnsafe: startParam ? { start_param: startParam } : {}, themeParams: { bg_color: '#101418' }, ready: () => {}, expand: () => {}, }, }, }); } describe('telegram launch detection', () => { afterEach(() => vi.unstubAllGlobals()); it('is not inside Telegram without a window', () => { expect(insideTelegram()).toBe(false); }); it('is inside Telegram only with non-empty initData', () => { stubWebApp(''); expect(insideTelegram()).toBe(false); stubWebApp('query_id=abc'); expect(insideTelegram()).toBe(true); }); it('telegramLaunch returns initData, start param and theme', () => { stubWebApp('query_id=abc', 'g123'); const launch = telegramLaunch(); expect(launch.initData).toBe('query_id=abc'); expect(launch.startParam).toBe('g123'); expect(launch.theme?.bg_color).toBe('#101418'); }); }); describe('telegramOpenExternalLink', () => { afterEach(() => vi.unstubAllGlobals()); it('routes an external URL through the SDK openLink inside Telegram', () => { const openLink = vi.fn(); vi.stubGlobal('window', { Telegram: { WebApp: { openLink } } }); expect(telegramOpenExternalLink('https://gramota.ru/poisk?query=кот')).toBe(true); expect(openLink).toHaveBeenCalledWith('https://gramota.ru/poisk?query=кот'); }); it('returns false without the SDK so the caller falls back to the anchor', () => { expect(telegramOpenExternalLink('https://x.io')).toBe(false); }); }); describe('routeExternalLinkInTelegram', () => { afterEach(() => vi.unstubAllGlobals()); function stubInside(openLink = vi.fn(), origin = 'https://app.example') { vi.stubGlobal('window', { Telegram: { WebApp: { initData: 'query_id=abc', openLink } } }); vi.stubGlobal('location', { href: origin + '/', origin }); return openLink; } it('routes an external http(s) _blank link through openLink inside Telegram', () => { const openLink = stubInside(); expect(routeExternalLinkInTelegram({ href: 'https://gramota.ru/poisk?query=кот', target: '_blank' })).toBe(true); expect(openLink).toHaveBeenCalledWith('https://gramota.ru/poisk?query=кот'); }); it('leaves a t.me link to the native handler (openTelegramLink owns those)', () => { const openLink = stubInside(); expect(routeExternalLinkInTelegram({ href: 'https://t.me/some_bot', target: '_blank' })).toBe(false); expect(openLink).not.toHaveBeenCalled(); }); it('leaves same-origin and non-_blank links to in-app navigation', () => { const openLink = stubInside(vi.fn(), 'https://app.example'); expect(routeExternalLinkInTelegram({ href: 'https://app.example/lobby', target: '_blank' })).toBe(false); expect(routeExternalLinkInTelegram({ href: 'https://gramota.ru', target: '' })).toBe(false); expect(openLink).not.toHaveBeenCalled(); }); it('does nothing outside Telegram (the anchor opens natively)', () => { expect(routeExternalLinkInTelegram({ href: 'https://x.io', target: '_blank' })).toBe(false); }); }); describe('collectTelegramDiag', () => { afterEach(() => vi.unstubAllGlobals()); it('reports a missing SDK outside Telegram', () => { const d = collectTelegramDiag(); expect(d.hasSDK).toBe(false); expect(d.hasWebApp).toBe(false); expect(d.initDataLen).toBe(0); expect(d.fieldsPresent).toEqual([]); expect(d.fieldsMissing).toEqual(['user', 'auth_date', 'hash', 'signature']); }); it('reads field NAMES (never values) from a non-empty initData', () => { stubWebApp('query_id=abc&user=%7B%7D&auth_date=1&hash=deadbeef'); const d = collectTelegramDiag(); expect(d.hasSDK).toBe(true); expect(d.hasWebApp).toBe(true); expect(d.initDataLen).toBeGreaterThan(0); expect(d.fieldsPresent).toEqual(['query_id', 'user', 'auth_date', 'hash']); expect(d.fieldsMissing).toEqual(['signature']); }); it('recovers field names from the URL fragment when the SDK left initData empty', () => { // Telegram passed launch data in the fragment, but WebApp.initData is empty (the Android // failure this screen diagnoses): the names come from the raw fragment instead, and // hashHadTgData flags that the data did arrive in the URL. vi.stubGlobal('window', { Telegram: { WebApp: { initData: '', platform: 'android' } } }); vi.stubGlobal('location', { hash: '#tgWebAppData=user%3D%257B%257D%26auth_date%3D1%26hash%3Ddeadbeef&tgWebAppVersion=7.0', pathname: '/telegram/', }); const d = collectTelegramDiag(); expect(d.hasSDK).toBe(true); expect(d.platform).toBe('android'); expect(d.initDataLen).toBe(0); expect(d.hashHadTgData).toBe(true); expect(d.fieldsPresent).toEqual(['user', 'auth_date', 'hash']); expect(d.fieldsMissing).toEqual(['signature']); }); }); describe('loadTelegramSDK', () => { afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); }); it('resolves true and records "present" when the SDK is already there', async () => { vi.stubGlobal('window', { Telegram: { WebApp: { initData: '' } } }); vi.stubGlobal('document', { createElement: vi.fn(), head: { appendChild: vi.fn() } }); await expect(loadTelegramSDK(10000)).resolves.toBe(true); expect(telegramSdkOutcome()).toBe('present'); }); it('records "loaded" when the script defines the WebApp', async () => { const script: Record = {}; vi.stubGlobal('window', {}); vi.stubGlobal('document', { createElement: () => script, head: { appendChild: () => { (window as unknown as { Telegram: unknown }).Telegram = { WebApp: { initData: '' } }; (script.onload as () => void)(); }, }, }); await expect(loadTelegramSDK(10000)).resolves.toBe(true); expect(telegramSdkOutcome()).toBe('loaded'); }); it('records "no-webapp" when the script loads but defines nothing', async () => { const script: Record = {}; vi.stubGlobal('window', {}); vi.stubGlobal('document', { createElement: () => script, head: { appendChild: () => (script.onload as () => void)() }, }); await expect(loadTelegramSDK(10000)).resolves.toBe(false); expect(telegramSdkOutcome()).toBe('no-webapp'); }); it('records "error" when the script fails to load (telegram.org unreachable)', async () => { const script: Record = {}; vi.stubGlobal('window', {}); vi.stubGlobal('document', { createElement: () => script, head: { appendChild: () => (script.onerror as () => void)() }, }); await expect(loadTelegramSDK(10000)).resolves.toBe(false); expect(telegramSdkOutcome()).toBe('error'); }); it('records "timeout" when the script neither loads nor fails in time', async () => { vi.useFakeTimers(); vi.stubGlobal('window', {}); vi.stubGlobal('document', { createElement: () => ({}), head: { appendChild: vi.fn() } }); const p = loadTelegramSDK(10000); await vi.advanceTimersByTimeAsync(10000); await expect(p).resolves.toBe(false); expect(telegramSdkOutcome()).toBe('timeout'); }); });