Files
scrabble-game/ui/src/lib/telegram.test.ts
T
Ilia Denisov b84bd1297e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
feat(telegram): clear bottom and side safe-area insets
Only the device safe-area TOP inset was mirrored, so on phones with a home
indicator the rack / bottom bar sat under it, and in landscape the notch
clipped the screen edges.

Mirror the full device safe-area inset (bottom / left / right) into new
--tg-safe-bottom / --tg-safe-left / --tg-safe-right CSS vars (0 outside
Telegram) and pad the shared Screen wrapper by them, so every screen clears the
home indicator and the landscape notch; the top inset stays owned by the header.
Replace telegramSafeAreaTop with telegramSafeAreaInset (the full inset object),
with a unit test.
2026-06-24 10:24:56 +02:00

215 lines
8.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
collectTelegramDiag,
insideTelegram,
loadTelegramSDK,
telegramSdkOutcome,
routeExternalLinkInTelegram,
telegramLaunch,
telegramOpenExternalLink,
telegramThemeParams,
telegramSafeAreaInset,
} 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');
});
it('telegramThemeParams reads the live palette (undefined outside Telegram)', () => {
expect(telegramThemeParams()).toBeUndefined();
stubWebApp('query_id=abc');
expect(telegramThemeParams()?.bg_color).toBe('#101418');
});
it('telegramSafeAreaInset returns zeros outside Telegram and the SDK insets inside', () => {
expect(telegramSafeAreaInset()).toEqual({ top: 0, bottom: 0, left: 0, right: 0 });
vi.stubGlobal('window', {
Telegram: { WebApp: { initData: 'x', safeAreaInset: { top: 59, bottom: 34, left: 0, right: 0 } } },
});
expect(telegramSafeAreaInset()).toEqual({ top: 59, bottom: 34, left: 0, right: 0 });
});
});
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<string, unknown> = {};
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<string, unknown> = {};
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<string, unknown> = {};
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');
});
});