import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { copyText } from '../support/clipboard'; import { shareOrCopy } from '../support/share'; // UI-02 V05 分享编排单测(§5.4):navigator.share 优先(成功即原生面板收尾); // AbortError = 用户取消(静默,不落剪贴板);其他失败/不可用回退剪贴板; // 复制失败返回 failed(调用方展示可选中文本)。 vi.mock('../support/clipboard', () => ({ copyText: vi.fn() })); const mockedCopyText = vi.mocked(copyText); function stubShare(implementation: () => Promise): ReturnType { const fn = vi.fn(implementation); Object.defineProperty(navigator, 'share', { value: fn, configurable: true }); return fn; } describe('share orchestration (UI-02 V05)', () => { beforeEach(() => { mockedCopyText.mockReset(); }); afterEach(() => { Reflect.deleteProperty(navigator, 'share'); vi.restoreAllMocks(); }); it('uses navigator.share when available and does not touch the clipboard', async () => { const share = stubShare(() => Promise.resolve()); const outcome = await shareOrCopy({ title: 'Annual Party', url: 'https://x.test/a/tok?lang=en' }); expect(outcome).toBe('shared'); expect(share).toHaveBeenCalledWith({ title: 'Annual Party', url: 'https://x.test/a/tok?lang=en' }); expect(mockedCopyText).not.toHaveBeenCalled(); }); it('treats AbortError as a silent cancel (no error, no clipboard fallback)', async () => { const share = stubShare(() => { const error = new Error('The operation was aborted.'); error.name = 'AbortError'; return Promise.reject(error); }); const outcome = await shareOrCopy({ title: 't', url: 'https://x.test/a/tok' }); expect(outcome).toBe('cancelled'); expect(share).toHaveBeenCalledTimes(1); expect(mockedCopyText).not.toHaveBeenCalled(); }); it('falls back to the clipboard when navigator.share rejects with another error', async () => { stubShare(() => Promise.reject(new TypeError('Not allowed to share'))); mockedCopyText.mockResolvedValue(true); const outcome = await shareOrCopy({ title: 't', url: 'https://x.test/a/tok' }); expect(outcome).toBe('copied'); expect(mockedCopyText).toHaveBeenCalledWith('https://x.test/a/tok'); }); it('falls back to the clipboard without navigator.share; reports failure when copying fails', async () => { expect('share' in navigator).toBe(false); // jsdom 默认无 navigator.share mockedCopyText.mockResolvedValueOnce(true); await expect(shareOrCopy({ title: 't', url: 'https://x.test/a/tok' })).resolves.toBe('copied'); mockedCopyText.mockResolvedValueOnce(false); await expect(shareOrCopy({ title: 't', url: 'https://x.test/a/tok' })).resolves.toBe('failed'); }); });