feat(viewer): UI-02 heart particles, interaction notices, share and download
Task 5 (V05 §5.4/§6): decorative heart particles on the lightbox stage (local like clicks + others' WS bursts for the current photo), compact cross-photo interaction notices above the action bar (click to locate), navigator.share-first share orchestration with clipboard fallback for album and photo deep links (lang param), and download failures attributed to the photo that triggered them. Self-echo dedup stays in the realtime router (own batches never produce particles/notices); particles cap at 12 with visibility/unmount cleanup and are suppressed under reduced motion; notices cap at 3 with ~4s auto-dismiss and throttled SR summary. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { defineComponent, nextTick, ref } from 'vue';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
ApiError,
|
||||
@@ -89,9 +89,10 @@ function spyWindowOpen() {
|
||||
}
|
||||
let openSpy: ReturnType<typeof spyWindowOpen>;
|
||||
|
||||
/** 宿主实例形态:setup 暴露 albumRef(ref 经代理解包)+ download 控制器。 */
|
||||
/** 宿主实例形态:setup 暴露 albumRef/currentRef(ref 经代理解包)+ download 控制器。 */
|
||||
interface HostVm {
|
||||
albumRef: PublicAlbum | null;
|
||||
currentRef: string;
|
||||
download: DownloadController;
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ function mountLightbox(album: PublicAlbum): VueWrapper & { vm: HostVm } {
|
||||
album: () => albumRef.value,
|
||||
photoId: () => current.value,
|
||||
});
|
||||
return { sequence, photoId: current, download, albumRef };
|
||||
return { sequence, photoId: current, currentRef: current, download, albumRef };
|
||||
},
|
||||
template: `<PhotoLightbox v-model:photo-id="photoId" :sequence="sequence" :download="download" />`,
|
||||
});
|
||||
@@ -251,6 +252,31 @@ describe('photo download (P4a Task 4)', () => {
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('attributes an async failure to the photo that triggered the download, not the current one', async () => {
|
||||
// 在途请求挂起期间切图:错误归属触发时的 photoId(UI-02 §5.4)。
|
||||
let rejectDownload!: (error: Error) => void;
|
||||
mockedGetPhotoDownload.mockReset().mockImplementation(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectDownload = reject;
|
||||
}),
|
||||
);
|
||||
const wrapper = mountLightbox(makeAlbum());
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('.download-main').trigger('click'); // p1 触发
|
||||
wrapper.vm.currentRef = 'p2'; // 请求在途时切到 p2
|
||||
rejectDownload(new Error('boom'));
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.vm.download.failedFor).toBe('p1');
|
||||
expect(wrapper.find('.download-failed').exists()).toBe(false); // p2 不背锅
|
||||
|
||||
wrapper.vm.currentRef = 'p1'; // 切回 p1:错误提示可见
|
||||
await nextTick();
|
||||
expect(wrapper.find('.download-failed[role="alert"]').text()).toContain('下载失败');
|
||||
});
|
||||
|
||||
it('DOWNLOAD_DISABLED surfaces the dedicated localized notice (T15 漏译修复)', async () => {
|
||||
// 相册中途关闭下载开关(载荷仍开但端点 403)→ 专属文案而非通用失败。
|
||||
mockedGetPhotoDownload.mockReset().mockRejectedValue(
|
||||
|
||||
Reference in New Issue
Block a user