ae5090b851
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 1m18s
The Telegram SDK was a render-blocking <script src="telegram.org/..."> in the shared index.html shell, so it ran on every entry (/telegram/, /app/, native). On a network that blocks telegram.org — common where Telegram itself reaches users only over a proxy — the script hangs forever, stranding the whole page, including the launch-diagnostic screen meant to surface exactly this failure. This is the likely root cause of the Android "won't open" reports (all launch methods fail identically; iOS on a different network works). Remove the head <script> and load the SDK dynamically (lib/telegram.ts loadTelegramSDK) with a 10s timeout, only on a Telegram entry (the /telegram/ path or a tgWebApp launch fragment). The SPA — served from our own reachable origin — boots first and controls the load: on a block/error it falls through to the diagnostic screen (reporting sdk: no) instead of hanging, and Retry re-attempts. /app/ and the native build no longer touch telegram.org. Pin the SDK to the version the official page recommends (?62) for the newer client features the app already uses (fullscreen, safe areas, swipe guard). Tests: loadTelegramSDK unit tests (present / error / timeout); an e2e that aborts the script fetch and asserts the diagnostic still renders.
136 lines
6.0 KiB
TypeScript
136 lines
6.0 KiB
TypeScript
import { expect, test } from './fixtures';
|
|
|
|
// The shared fixture already neutralises the real telegram-web-app.js, so these
|
|
// specs control window.Telegram deterministically (injected below) with no network.
|
|
|
|
// A minimal valid-looking Telegram WebApp stub: non-empty initData triggers the Mini
|
|
// App launch path (the mock gateway accepts any initData and returns a durable
|
|
// session); themeParams override the design tokens.
|
|
function webAppStub(startParam = '') {
|
|
return {
|
|
Telegram: {
|
|
WebApp: {
|
|
initData: 'query_id=test&user=%7B%22id%22%3A1%7D&auth_date=1&hash=deadbeef',
|
|
initDataUnsafe: startParam ? { start_param: startParam } : {},
|
|
themeParams: { bg_color: '#101418', text_color: '#ffffff' },
|
|
ready() {},
|
|
expand() {},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
test('Telegram launch auto-authenticates into the lobby and applies the theme', async ({ page }) => {
|
|
await page.addInitScript((stub) => {
|
|
Object.assign(window, stub);
|
|
}, webAppStub());
|
|
await page.goto('/');
|
|
|
|
// No guest-login click: the Mini App authenticates from initData and lands on the lobby.
|
|
await expect(page.getByText('Your turn')).toBeVisible();
|
|
|
|
// The Telegram themeParams override the background token at runtime.
|
|
await expect
|
|
.poll(() => page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--bg').trim()))
|
|
.toBe('#101418');
|
|
});
|
|
|
|
test('a Telegram launch fragment in the URL still lands on the lobby and normalises the hash', async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript((stub) => {
|
|
Object.assign(window, stub);
|
|
}, webAppStub());
|
|
// Telegram appends its launch params to the URL fragment on a cold launch; the router must
|
|
// not treat that as a route (it parsed as notfound, which re-keyed the pane and slid the
|
|
// lobby in as if returning from another screen).
|
|
await page.goto(
|
|
'/#tgWebAppData=query_id%3Dtest%26user%3D%257B%2522id%2522%253A1%257D&tgWebAppVersion=7.0&tgWebAppPlatform=ios',
|
|
);
|
|
await expect(page.getByText('Your turn')).toBeVisible();
|
|
// The lobby is the root: bootstrap normalised the launch-param fragment to '#/'.
|
|
await expect.poll(() => new URL(page.url()).hash).toBe('#/');
|
|
});
|
|
|
|
test('tg-fullscreen header keeps a constant native-nav gap as the font scales', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: /guest/i }).click();
|
|
await expect(page.getByText('Your turn')).toBeVisible(); // the lobby header is present + settled
|
|
|
|
// Emulate Telegram fullscreen: the class + safe-area vars our header positions against.
|
|
await page.evaluate(() => {
|
|
const h = document.documentElement;
|
|
h.classList.add('tg-fullscreen');
|
|
h.style.setProperty('--tg-safe-top', '47px');
|
|
h.style.setProperty('--tg-content-top', '50px');
|
|
});
|
|
const probe = () =>
|
|
page.evaluate(() => {
|
|
const bar = document.querySelector('.bar')!.getBoundingClientRect();
|
|
const h1 = document.querySelector('.bar h1')!.getBoundingClientRect();
|
|
return { h1Top: Math.round(h1.top), overflows: h1.bottom > bar.bottom + 0.5 };
|
|
});
|
|
|
|
const normal = await probe();
|
|
// Scale the root font up (an OS / Telegram "larger text" setting scales rem-based text).
|
|
await page.evaluate(() => (document.documentElement.style.fontSize = '28px'));
|
|
const large = await probe();
|
|
|
|
// The gap to Telegram's native controls is a fixed px, so the title's top does not move…
|
|
expect(large.h1Top).toBe(normal.h1Top);
|
|
// …the title grows downward inside the bar (which grows with it), never overflowing it.
|
|
expect(normal.overflows).toBe(false);
|
|
expect(large.overflows).toBe(false);
|
|
});
|
|
|
|
test('inside Telegram, a failed launch shows the retry screen, not the web login', async ({ page }) => {
|
|
// initData carrying the mock's "bootfail" sentinel makes authTelegram reject, simulating a
|
|
// backend outage during launch (e.g. a deploy rolling). The Mini App must surface its own
|
|
// boot-error/retry screen and never fall back to the web (guest/email) login.
|
|
await page.addInitScript(() => {
|
|
Object.assign(window, {
|
|
Telegram: {
|
|
WebApp: {
|
|
initData: 'query_id=bootfail&user=%7B%22id%22%3A1%7D&auth_date=1&hash=deadbeef',
|
|
initDataUnsafe: {},
|
|
ready() {},
|
|
expand() {},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
await page.goto('/');
|
|
|
|
// After the silent retries, the boot-error screen with its Retry button shows…
|
|
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
// …and the web login (guest) is never shown inside Telegram.
|
|
await expect(page.getByRole('button', { name: /guest/i })).toHaveCount(0);
|
|
});
|
|
|
|
test('outside Telegram, the /telegram/ entry shows the launch diagnostic, not a redirect', async ({
|
|
page,
|
|
}) => {
|
|
await page.goto('/telegram/');
|
|
|
|
// The entry no longer bounces a visitor without Telegram sign-in data to the marketing landing;
|
|
// it shows a compact diagnostic screen (Share + Retry) that helps pinpoint why initData was
|
|
// absent — notably the Android empty-initData failure.
|
|
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
// It stays on /telegram/ (no redirect) and never shows the web (guest) login.
|
|
await expect(page).toHaveURL(/\/telegram\//);
|
|
await expect(page.getByRole('button', { name: /guest/i })).toHaveCount(0);
|
|
});
|
|
|
|
test('a blocked telegram-web-app.js does not hang the diagnostic screen', async ({ page }) => {
|
|
// Simulate a network where telegram.org is unreachable: the SDK fetch fails. Because the SPA
|
|
// loads the SDK dynamically with a timeout (not a render-blocking <script>), a failed/blocked
|
|
// fetch must not strand the page — the diagnostic screen still renders, reporting no SDK. (This
|
|
// route overrides the fixture's empty-body fulfill; the later registration wins.)
|
|
await page.route('**/telegram-web-app.js*', (route) => route.abort());
|
|
await page.goto('/telegram/');
|
|
|
|
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
|
await expect(page.getByText('sdk: no')).toBeVisible();
|
|
});
|