docs: UI-02 acceptance record and visual verification

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-21 23:10:58 +08:00
parent 160e7e5f1a
commit 2a489aa16a
23 changed files with 297 additions and 75 deletions
+25
View File
@@ -232,6 +232,31 @@ describe('photo feed (Task 7 / Task 8 sequence props)', () => {
expect(wrapper.find('.new-photo-bar').exists()).toBe(false);
});
// —— UI-02 Task 6(§7 状态矩阵):加载更多失败——保留已有照片,底部重试 ——
it('load-more failure keeps the loaded photos and offers a bottom retry', async () => {
const { wrapper } = mountFeed();
await flushPromises();
expect(wrapper.findAll('.cell')).toHaveLength(3);
const sentinel = wrapper.find('.sentinel');
mockedGetPublicPhotos.mockRejectedValueOnce(new ApiError('NETWORK_ERROR', 'network down', 0, null));
sentinelObserver().trigger([sentinel.element], true);
await flushPromises();
// 已有照片不被清空;不渲染整页错误块,只在底部给重试。
expect(wrapper.findAll('.cell')).toHaveLength(3);
expect(wrapper.find('.error[role="alert"]').exists()).toBe(false);
const retry = wrapper.find('.more-failed button');
expect(retry.text()).toBe('重试');
// 底部重试成功 → 追加第 2 页并到尽头。
mockedGetPublicPhotos.mockResolvedValueOnce(makeList([makePhoto('p4')], null));
await retry.trigger('click');
await flushPromises();
expect(wrapper.findAll('.cell')).toHaveLength(4);
expect(wrapper.find('.end-note').text()).toBe('已经到底了');
});
it('renders the localized empty state when the album has no photos', async () => {
mockedGetPublicPhotos.mockResolvedValueOnce(makeList([]));
const { wrapper } = mountFeed();
+22
View File
@@ -53,4 +53,26 @@ describe('viewer i18n', () => {
expect(wrapper.text()).toContain('打开相册链接即可观看');
expect(localStorage.getItem(LOCALE_STORAGE_KEY)).toBe('zh-CN');
});
// UI-02 Task 6(V05 §5.4 分享链路闭环):分享 URL 携带 ?lang=,接收端
// 首载据此定语言——优先于本地持久化偏好;不合法值(fr/auto)自然回退。
it('initial locale honors the ?lang= share parameter over the stored preference', () => {
localStorage.setItem(LOCALE_STORAGE_KEY, 'zh-CN');
window.history.pushState({}, '', '/a/token?lang=en');
expect(createViewerI18n().global.locale.value).toBe('en');
// 非法/不支持值:回落到持久化偏好。
window.history.pushState({}, '', '/a/token?lang=auto');
expect(createViewerI18n().global.locale.value).toBe('zh-CN');
window.history.pushState({}, '', '/a/token?lang=fr');
expect(createViewerI18n().global.locale.value).toBe('zh-CN');
// 无参数:持久化偏好照旧生效。
window.history.pushState({}, '', '/a/token');
expect(createViewerI18n().global.locale.value).toBe('zh-CN');
// 参数不写 localStorage——接收者未做选择,显式切换才持久化。
expect(localStorage.getItem(LOCALE_STORAGE_KEY)).toBe('zh-CN');
});
});
@@ -432,6 +432,32 @@ describe('photo lightbox dark viewer (UI-02 V04)', () => {
trigger.remove();
});
// UI-02 Task 6 无障碍抽检(§5.3):Tab 循环困于对话框——两端折回,不穿透。
it('traps Tab focus inside the dialog (wrap-around at both ends)', async () => {
const wrapper = mountLightbox('p2');
document.body.appendChild(wrapper.element);
await flushPromises();
const focusables = Array.from(
wrapper.element.querySelectorAll('button:not([disabled])'),
).filter((el): el is HTMLElement => el instanceof HTMLElement);
expect(focusables.length).toBeGreaterThan(1);
const first = focusables[0]!;
const last = focusables[focusables.length - 1]!;
// 末元素正向 Tab → 折回首元素。
last.focus();
last.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true }));
expect(document.activeElement).toBe(first);
// 首元素 Shift+Tab → 折回末元素。
first.focus();
first.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true, cancelable: true }),
);
expect(document.activeElement).toBe(last);
});
it('locks body scroll while open and restores the previous value on close', async () => {
document.body.style.overflow = 'auto';
const wrapper = mountLightbox('p2');
@@ -454,6 +480,16 @@ describe('photo lightbox dark viewer (UI-02 V04)', () => {
expect(source).toContain('env(safe-area-inset-top, 0px)');
expect(source).toContain('env(safe-area-inset-bottom, 0px)');
});
it('layers the fullscreen overlay above the sticky topbar (close button clickable)', () => {
// jsdom 无布局:源级断言(同上)。覆盖层 z=200 须高于 sticky 顶栏 z=120
// (App.vue)——否则顶栏盖住关闭按钮的可点区(e2e 曾拦截 click 的真实回归)。
const source = readFileSync(
join(dirname(fileURLToPath(import.meta.url)), '..', 'components', 'PhotoLightbox.vue'),
'utf8',
);
expect(source).toMatch(/\.photo-viewer\s*\{[^}]*z-index:\s*200/s);
});
});
describe('photo lightbox interactions (UI-02 V05)', () => {