37070c3cb7
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 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m13s
Finalises the Telegram Mini App navigation work after on-device testing (Pixel 10 / Android 17 + iOS, fresh beta clients): - Remove requestFullscreen entirely. Immersive fullscreen hid Telegram's native header (and its BackButton) and the Android system swipe-back minimised the app; the owner prefers the windowed full-size (expand) presentation, so the app never requests fullscreen on any platform now. - The app's own back chevron (Header, showBack = !!back) drives back-navigation on every platform; the native Telegram BackButton is dropped — it does not render in the windowed Mini App (backVisible=false on iOS and Android), so relying on it lost back navigation (notably none on iOS). - Replace the temporary always-on diagnostic overlay with a hidden debug panel (components/DebugPanel): ten quick taps on the header title open it; it shows a privacy-safe client diagnostic snapshot (app version, locale, online, userId, Telegram chrome / viewport / SDK state — no secrets, no IP) and shares it via the OS share sheet / clipboard; a tap anywhere except Share dismisses it. - Drop the now-dead telegramRequestFullscreen / telegramBackButton / isTelegramAndroid helpers and the iOS-fullscreen unit test. Telegram has no native non-modal notification API (only modal showPopup / showAlert), so in-app toasts stay ours. Docs: UI_DESIGN.md.
199 lines
7.4 KiB
TypeScript
199 lines
7.4 KiB
TypeScript
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<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');
|
|
});
|
|
});
|