feat(export): platform-native delivery — TG popup+downloadFile, VK viewer, mobile share sheet
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Failing after 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Failing after 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
Per the on-device review, the last hop is now the most native affordance per platform, all fed by the same signed URL: - Telegram: the native showPopup chooser returns (safe here — the whole TG chain is bridge calls, which need no user activation) and both formats go to the native downloadFile dialog on iOS and Android alike. - VK: the PNG opens in VK's native photo viewer (VKWebAppShowImages) — on screen at once, saved/shared from the viewer's own controls; the GCG keeps VKWebAppDownloadFile; either falls back to a plain anchor download (the desktop iframe). The gateway now serves /dl/* via http.ServeContent (Content-Length + Range/206) — another swing at the VK Android DownloadManager hang; if it persists, the next step is the clipboard fallback for the VK-Android GCG. - Mobile browsers: the OS share sheet with the fetched file (the proven fetch-then-share pattern) — nothing lands in Downloads first; desktop keeps the anchor download. Legacy Telegram (< 8.0, no downloadFile) keeps the app modal + GCG clipboard copy and hides the image option.
This commit is contained in:
+48
-4
@@ -58,9 +58,12 @@ test('the chooser downloads the GCG through the same signed-URL route', async ({
|
||||
expect((await download).suggestedFilename()).toMatch(/^game-.+\.gcg$/);
|
||||
});
|
||||
|
||||
test('inside Telegram both formats go through the native downloadFile dialog', async ({ page }) => {
|
||||
// A Telegram stub with downloadFile present (Bot API 8.0): the image option is offered
|
||||
// and the artifact is handed to the native download — never an in-webview anchor.
|
||||
test('inside Telegram the chooser is the native popup and delivery the native downloadFile', async ({
|
||||
page,
|
||||
}) => {
|
||||
// A Telegram stub with showPopup AND downloadFile (Bot API 8.0): the whole chain stays
|
||||
// in bridge calls — the popup picks the image, the artifact goes to the native download
|
||||
// dialog, never an in-webview anchor. (Activation-safe: no gesture-gated Web APIs.)
|
||||
await page.addInitScript(() => {
|
||||
Object.assign(window, {
|
||||
Telegram: {
|
||||
@@ -69,6 +72,11 @@ test('inside Telegram both formats go through the native downloadFile dialog', a
|
||||
initDataUnsafe: {},
|
||||
ready() {},
|
||||
expand() {},
|
||||
showPopup(params: { buttons?: { id?: string; type?: string }[] }, cb: (id: string) => void) {
|
||||
const w = window as unknown as { __popup?: unknown };
|
||||
w.__popup = params;
|
||||
setTimeout(() => cb('png'), 0);
|
||||
},
|
||||
downloadFile(params: { url: string; file_name: string }) {
|
||||
const w = window as unknown as { __downloads?: { url: string; file_name: string }[] };
|
||||
(w.__downloads ??= []).push(params);
|
||||
@@ -84,7 +92,13 @@ test('inside Telegram both formats go through the native downloadFile dialog', a
|
||||
await page.locator('.scoreboard').click();
|
||||
await page.getByRole('button', { name: 'Export game' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Image (PNG)' }).click();
|
||||
// The native popup carried both formats plus cancel, and no in-app modal was mounted.
|
||||
const popup = await page.evaluate(
|
||||
() => (window as unknown as { __popup?: { buttons?: { id?: string; type?: string }[] } }).__popup,
|
||||
);
|
||||
expect(popup?.buttons?.map((b) => b.id ?? b.type)).toEqual(['png', 'gcg', 'cancel']);
|
||||
await expect(page.getByRole('button', { name: 'GCG file' })).toHaveCount(0);
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => (window as unknown as { __downloads?: { url: string; file_name: string }[] }).__downloads),
|
||||
@@ -98,6 +112,36 @@ test('inside Telegram both formats go through the native downloadFile dialog', a
|
||||
expect(dl.file_name).toMatch(/^game-.+\.png$/);
|
||||
});
|
||||
|
||||
test('a mobile browser gets the OS share sheet with the fetched file', async ({ page }) => {
|
||||
// Web Share with files present (a mobile browser): the artifact is fetched from the
|
||||
// signed URL and handed to navigator.share as a File — the sheet opens directly,
|
||||
// nothing lands in Downloads first.
|
||||
await page.addInitScript(() => {
|
||||
const shared: { name: string; type: string }[] = [];
|
||||
Object.defineProperty(navigator, 'canShare', { value: () => true, configurable: true });
|
||||
Object.defineProperty(navigator, 'share', {
|
||||
value: async (data: { files?: File[] }) => {
|
||||
for (const f of data.files ?? []) shared.push({ name: f.name, type: f.type });
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
(window as unknown as { __shared: typeof shared }).__shared = shared;
|
||||
});
|
||||
await routeDl(page);
|
||||
await openFinishedHistory(page);
|
||||
await page.getByRole('button', { name: 'Export game' }).click();
|
||||
await page.getByRole('button', { name: 'Image (PNG)' }).click();
|
||||
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => (window as unknown as { __shared: unknown[] }).__shared))
|
||||
.toHaveLength(1);
|
||||
const shared = await page.evaluate(
|
||||
() => (window as unknown as { __shared: { name: string; type: string }[] }).__shared[0],
|
||||
);
|
||||
expect(shared.name).toMatch(/^game-.+\.png$/);
|
||||
expect(shared.type).toBe('image/png');
|
||||
});
|
||||
|
||||
test('a legacy Telegram client (no downloadFile) hides the image and copies the GCG', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
// Headless engines deny the real clipboard; a permissive stub keeps the legacy
|
||||
|
||||
Reference in New Issue
Block a user