Files
scrabble-game/ui/src/lib/share.test.ts
T
Ilia Denisov e850ecd83b fix(ui): don't strand the Mini App on a cancelled GCG share
On iOS WKWebView (the Telegram Mini App), cancelling the Web Share sheet fell
through to the Blob <a download> fallback. iOS ignores the download attribute,
so clicking the anchor navigated the webview to the blob: URL — replacing the
SPA with the raw GCG file, with no way back (force-quit only).

The share path no longer falls back to a download: Web Share is available on
that platform, so a cancelled or failed share is a no-op and the user can
retry. The Blob download stays the desktop-only path (no Web Share).
2026-06-17 14:01:02 +02:00

63 lines
2.5 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { pickGcgDelivery, shareOrDownloadGcg } from './share';
import type { GcgExport } from './model';
const file = {} as File;
const gcg: GcgExport = { gameId: 'g1', filename: 'game.gcg', content: '#title game' };
describe('pickGcgDelivery', () => {
it('shares when the platform can share files', () => {
expect(pickGcgDelivery({ canShare: () => true, share: async () => {} }, file)).toBe('share');
});
it('downloads when the platform cannot share files', () => {
expect(pickGcgDelivery({ canShare: () => false, share: async () => {} }, file)).toBe('download');
});
it('downloads when there is no navigator', () => {
expect(pickGcgDelivery(undefined, file)).toBe('download');
});
it('downloads when the Web Share API is incomplete', () => {
expect(pickGcgDelivery({ canShare: () => true } as never, file)).toBe('download');
});
});
describe('shareOrDownloadGcg', () => {
afterEach(() => vi.unstubAllGlobals());
function stubDownloadEnv(canShare: boolean, share: () => Promise<void>) {
const anchor = { href: '', download: '', click: vi.fn(), remove: vi.fn() };
const createElement = vi.fn(() => anchor);
vi.stubGlobal('File', class {});
vi.stubGlobal('Blob', class {});
vi.stubGlobal('navigator', { canShare: () => canShare, share });
vi.stubGlobal('document', { createElement, body: { appendChild: vi.fn() } });
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:x'), revokeObjectURL: vi.fn() });
return { anchor, createElement };
}
it('never falls back to the navigating Blob download when a share is cancelled', async () => {
// Reproduces the iOS Telegram Mini App break: Web Share is available and the user cancels it
// (AbortError). The <a download> fallback navigates the WKWebView to the blob: URL and strands
// the app, so a cancelled (or failed) share must do nothing here.
const share = vi.fn().mockRejectedValue(new DOMException('cancelled', 'AbortError'));
const { createElement } = stubDownloadEnv(true, share);
await shareOrDownloadGcg(gcg);
expect(share).toHaveBeenCalledOnce();
expect(createElement).not.toHaveBeenCalled(); // no download anchor → no webview navigation
});
it('downloads via an anchor when the platform cannot share files', async () => {
const { anchor, createElement } = stubDownloadEnv(false, vi.fn());
await shareOrDownloadGcg(gcg);
expect(createElement).toHaveBeenCalledWith('a');
expect(anchor.click).toHaveBeenCalledOnce();
});
});