feat: AI-game & resign UX cleanup (lobby drop, board reveal, GCG share fix, hide AI export) #80
@@ -1,8 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { pickGcgDelivery } from './share';
|
||||
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');
|
||||
@@ -20,3 +23,40 @@ describe('pickGcgDelivery', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
+7
-2
@@ -25,12 +25,17 @@ export async function shareOrDownloadGcg(gcg: GcgExport): Promise<void> {
|
||||
const file = new File([gcg.content], gcg.filename, { type: 'application/x-gcg' });
|
||||
const nav = typeof navigator !== 'undefined' ? navigator : undefined;
|
||||
if (pickGcgDelivery(nav, file) === 'share' && nav) {
|
||||
// Web Share is available (mobile, including the iOS Telegram Mini App): use it and stop here
|
||||
// whatever the outcome. Do NOT fall back to the Blob download — on iOS WKWebView an
|
||||
// <a download> navigates the webview to the blob: URL, replacing the SPA with the raw file
|
||||
// and stranding the app, so a cancelled or failed share must simply do nothing (the user can
|
||||
// retry). The download path is only for desktop browsers without Web Share.
|
||||
try {
|
||||
await nav.share({ files: [file], title: gcg.filename });
|
||||
return;
|
||||
} catch {
|
||||
// The user cancelled or sharing failed — fall back to a download.
|
||||
/* cancelled or failed — intentionally a no-op (see above) */
|
||||
}
|
||||
return;
|
||||
}
|
||||
downloadFile(gcg.content, gcg.filename);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user