Files
TREK/client/src/utils/fileShare.test.ts
T
Maurice 94674e7572 fix(tours): open the GPX share sheet inside the tap
Sharing a tour fetched the GPX file first and only then called
navigator.share, so the tap that asked had often run out by then. The
retry path kept that file for one more tap only, and a tap while a sheet
was still open made the browser throw InvalidStateError, which read as a
failed share.

The share button now fetches the file ahead where sharing is offered
(not while the planner has unsaved changes), and every tap hands a fresh
File built from the kept bytes to the sheet before anything is awaited.
A file a tap had to fetch is kept for the next tap. A tap while a sheet
is open is let be, and InvalidStateError and a closed sheet stay quiet;
only a real failure still says the download is left.
2026-10-20 20:56:19 +02:00

59 lines
2.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { canShareFile, shareFile } from './fileShare';
const gpx = () => new File(['<gpx/>'], 'Kramer.gpx', { type: 'application/gpx+xml' });
function stubShare(share: unknown, canShare: unknown) {
Object.defineProperty(navigator, 'share', { value: share, configurable: true });
Object.defineProperty(navigator, 'canShare', { value: canShare, configurable: true });
}
afterEach(() => stubShare(undefined, undefined));
describe('canShareFile (IO-01)', () => {
it('asks the platform about this very kind of file', () => {
const canShare = vi.fn((data: ShareData) => data.files?.[0]?.name.endsWith('.gpx') === true);
stubShare(vi.fn(), canShare);
expect(canShareFile('tour.gpx', 'application/gpx+xml')).toBe(true);
expect(canShare.mock.calls[0][0].files?.[0]).toMatchObject({ name: 'tour.gpx', type: 'application/gpx+xml' });
canShare.mockReturnValue(false);
expect(canShareFile('tour.gpx', 'application/gpx+xml')).toBe(false);
});
it('is no without the share API, without canShare, or when the question itself throws', () => {
stubShare(undefined, () => true);
expect(canShareFile('tour.gpx', 'application/gpx+xml')).toBe(false);
stubShare(vi.fn(), undefined);
expect(canShareFile('tour.gpx', 'application/gpx+xml')).toBe(false);
stubShare(vi.fn(), () => {
throw new TypeError('files not supported');
});
expect(canShareFile('tour.gpx', 'application/gpx+xml')).toBe(false);
});
});
describe('shareFile (IO-01)', () => {
it('opens the sheet with the file and its title', async () => {
const share = vi.fn().mockResolvedValue(undefined);
stubShare(share, () => true);
const file = gpx();
await expect(shareFile(file, 'Kramer')).resolves.toBe('shared');
expect(share).toHaveBeenCalledWith({ files: [file], title: 'Kramer' });
});
it('tells a closed sheet, a refused tap, an open sheet and any other failure apart', async () => {
const share = vi.fn();
stubShare(share, () => true);
share.mockRejectedValueOnce(new DOMException('closed', 'AbortError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('cancelled');
share.mockRejectedValueOnce(new DOMException('no activation', 'NotAllowedError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('needs-gesture');
share.mockRejectedValueOnce(new DOMException('earlier share still open', 'InvalidStateError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('busy');
share.mockRejectedValueOnce(new DOMException('bad data', 'DataError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('failed');
share.mockRejectedValueOnce(new Error('odd'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('failed');
});
});