Files
photo-live/apps/viewer/src/__tests__/viewer-shell.spec.ts
T
2026-09-22 13:49:15 +08:00

114 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import App from '../App.vue';
import { createViewerI18n, type ViewerI18n } from '../i18n';
import { router } from '../router';
// UI-02 V01 外壳与视觉 token:
// · token 断言直接读 viewer-tokens.css 源文件(vitest 不加载样式表),
// 作用域正确性由渲染分支验证——普通路由挂 .viewer-shell(token 生效),
// embed 路由走 .embed-shell 裸分支(不挂类 → 不消费任何 token,隔离红线);
// · 顶栏:品牌 PhotoLive + 紧凑语言控件 + 「⋯」轻量菜单内的减少动画入口。
vi.mock('../api/client', async (importOriginal) => {
const actual = await importOriginal<typeof import('../api/client')>();
return {
...actual,
// embed 路由数据:404 → 静默 gone(无 UI、无轮询),外壳分支足以验证。
// ApiError 经 actual 命名空间构造(工厂被提升,不能引用顶层 import 绑定)。
fetchPublicEmbedSlot: vi.fn().mockRejectedValue(
new actual.ApiError('EMBED_SLOT_NOT_FOUND', 'slot gone', 404, 'req-shell'),
),
};
});
let i18n: ViewerI18n;
let mounted: VueWrapper | null = null;
function mountApp(): VueWrapper {
const wrapper = mount(App, { global: { plugins: [router, i18n] } });
mounted = wrapper;
return wrapper;
}
describe('viewer shell tokens and embed isolation (UI-02 V01)', () => {
beforeEach(() => {
localStorage.clear();
i18n = createViewerI18n('zh-CN');
});
afterEach(() => {
mounted?.unmount();
mounted = null;
document.documentElement.className = '';
});
it('declares all UI-02 §3.1 tokens scoped to .viewer-shell (not :root/global)', () => {
const cssPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'styles', 'viewer-tokens.css');
const css = readFileSync(cssPath, 'utf8');
const expected: Record<string, string> = {
'--viewer-primary': '#1A73E8',
'--viewer-surface': '#FFFFFF',
'--viewer-page-bg': '#F3F6FA',
'--viewer-text': '#172033',
'--viewer-text-secondary': '#647084',
'--viewer-divider': '#E5EAF1',
'--viewer-dark-bg': '#0E1218',
'--viewer-like-accent': '#FF667D',
'--viewer-font-family': "system-ui, -apple-system, 'Segoe UI', 'PingFang SC', sans-serif",
'--viewer-radius-img': '4px',
'--viewer-radius-pill': '22px',
'--viewer-chip-bg': '#F5F7FA',
'--viewer-pad-info': '16px',
'--viewer-pad-photo': '4px',
'--viewer-touch-target': '44px',
};
for (const [name, value] of Object.entries(expected)) {
expect(css, `${name} token`).toContain(`${name}: ${value};`);
}
// 不得把 token 泄漏到全局根(embed iframe 会继承 :root 变量)。
expect(css).not.toContain(':root');
expect(css).toContain('.viewer-shell');
});
it('normal routes render .viewer-shell with PhotoLive brand top bar and prefs menu', async () => {
await router.push('/');
await router.isReady();
const app = mountApp();
expect(app.find('.viewer-shell').exists()).toBe(true);
expect(app.find('.embed-shell').exists()).toBe(false);
const topbar = app.find('header.topbar');
expect(topbar.exists()).toBe(true);
expect(topbar.find('.brand-name').text()).toBe('PhotoLive');
expect(topbar.find('.locale-trigger').exists()).toBe(true);
// 「⋯」轻量菜单(§3.2):减少动画入口可发现、可切换。
const more = topbar.find('.icon-btn');
expect(more.attributes('aria-haspopup')).toBe('menu');
await more.trigger('click');
const item = app.find('.prefs-item');
expect(item.text()).toBe('减少动画');
const before = item.attributes('aria-checked');
await item.trigger('click');
expect(app.find('.prefs-item').attributes('aria-checked')).toBe(before === 'true' ? 'false' : 'true');
// 还原模块级偏好单例,避免影响后续用例。
await app.find('.prefs-item').trigger('click');
});
it('embed routes keep the bare shell: no .viewer-shell, no top bar, no outer padding wrapper', async () => {
await router.push('/embed/slot-shell-test');
await router.isReady();
const app = mountApp();
await flushPromises();
expect(app.find('.embed-shell').exists()).toBe(true);
expect(app.find('.viewer-shell').exists()).toBe(false);
expect(app.find('header.topbar').exists()).toBe(false);
expect(app.find('.viewer-main').exists()).toBe(false);
});
});