2ba7cc3086
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s
Inside the Mini App, route the destructive confirmations (resign, block, unfriend) through Telegram's native showConfirm, and present the deep-link info modals (stale invite, welcome-on-redeem) as a native showPopup whose button opens the bot chat. Outside Telegram, or on a client predating the dialogs, the existing in-app Modal is used unchanged; offline also keeps the modal so the action retains its disabled state. Adds showConfirm/showPopup wrappers to telegram.ts and a pure popup-params builder (nativedialogs.ts) with unit tests; the deep-link modal components choose native vs in-app via an effect gated on insideTelegram + dialog availability.
269 lines
10 KiB
TypeScript
269 lines
10 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
collectTelegramDiag,
|
|
insideTelegram,
|
|
loadTelegramSDK,
|
|
telegramSdkOutcome,
|
|
routeExternalLinkInTelegram,
|
|
telegramLaunch,
|
|
telegramOpenExternalLink,
|
|
telegramThemeParams,
|
|
telegramSafeAreaInset,
|
|
telegramShowSettingsButton,
|
|
telegramDialogsAvailable,
|
|
telegramShowConfirm,
|
|
telegramShowPopup,
|
|
} 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('telegramShowSettingsButton', () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
it('shows the native Settings button and wires its click inside Telegram', () => {
|
|
const onClick = vi.fn();
|
|
const show = vi.fn();
|
|
const handler = vi.fn();
|
|
vi.stubGlobal('window', { Telegram: { WebApp: { SettingsButton: { onClick, show } } } });
|
|
telegramShowSettingsButton(handler);
|
|
expect(onClick).toHaveBeenCalledWith(handler);
|
|
expect(show).toHaveBeenCalled();
|
|
});
|
|
|
|
it('is a no-op without the SDK button (older client / outside Telegram)', () => {
|
|
expect(() => telegramShowSettingsButton(() => {})).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('native dialogs', () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
it('telegramDialogsAvailable reflects showPopup presence', () => {
|
|
expect(telegramDialogsAvailable()).toBe(false);
|
|
vi.stubGlobal('window', { Telegram: { WebApp: { showPopup: () => {} } } });
|
|
expect(telegramDialogsAvailable()).toBe(true);
|
|
});
|
|
|
|
it('telegramShowConfirm resolves the user choice inside Telegram', async () => {
|
|
vi.stubGlobal('window', {
|
|
Telegram: { WebApp: { showConfirm: (_m: string, cb: (ok: boolean) => void) => cb(true) } },
|
|
});
|
|
await expect(telegramShowConfirm('Sure?')).resolves.toBe(true);
|
|
});
|
|
|
|
it('telegramShowConfirm resolves false without the SDK dialog', async () => {
|
|
await expect(telegramShowConfirm('Sure?')).resolves.toBe(false);
|
|
});
|
|
|
|
it('telegramShowPopup resolves the pressed button id', async () => {
|
|
vi.stubGlobal('window', {
|
|
Telegram: { WebApp: { showPopup: (_p: unknown, cb: (id: string) => void) => cb('bot') } },
|
|
});
|
|
await expect(telegramShowPopup({ message: 'Hi' })).resolves.toBe('bot');
|
|
});
|
|
|
|
it('telegramShowPopup resolves null without the SDK', async () => {
|
|
await expect(telegramShowPopup({ message: 'Hi' })).resolves.toBeNull();
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|