- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化 - Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab - 四档响应式(桌面/笔记本/平板/手机)、深浅主题 - 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测 - 真实凭据已脱敏,运行时配置在 backend/.env(不入 git) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
145 lines
5.7 KiB
JavaScript
145 lines
5.7 KiB
JavaScript
// AppShell 响应式单测(B1):四档断点的 class 切换(FR-RESP-001..004)
|
||
// jsdom + matchMedia mock。验证:
|
||
// desktop ≥1280:无 drawers-mode,折叠 class 生效
|
||
// laptop 1024–1279:无 drawers-mode(CSS 强制折叠由样式层处理)
|
||
// tablet 768–1023:drawers-mode
|
||
// mobile <768:drawers-mode + mobile
|
||
// 折叠持久化:localStorage pa-side-collapsed / pa-bench-collapsed
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import { createPinia } from 'pinia';
|
||
import { nextTick } from 'vue';
|
||
|
||
import AppShell from '../../../../frontend/src/components/layout/AppShell.vue';
|
||
|
||
// mock fetch(health store 若被使用则不真发请求)
|
||
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }))));
|
||
|
||
/** 用 matchMedia mock 设置视口宽度 */
|
||
function setViewport(w) {
|
||
window.matchMedia = (q) => ({
|
||
matches: false,
|
||
media: q,
|
||
onchange: null,
|
||
addListener: vi.fn(),
|
||
removeListener: vi.fn(),
|
||
addEventListener: vi.fn(),
|
||
removeEventListener: vi.fn(),
|
||
dispatchEvent: vi.fn(),
|
||
});
|
||
Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: w });
|
||
}
|
||
|
||
async function mountShell(w = 1440) {
|
||
setViewport(w);
|
||
const pinia = createPinia();
|
||
const wrapper = mount(AppShell, { global: { plugins: [pinia] } });
|
||
await nextTick();
|
||
return wrapper;
|
||
}
|
||
|
||
describe('AppShell 响应式四档断点', () => {
|
||
beforeEach(() => {
|
||
localStorage.clear();
|
||
});
|
||
|
||
it('desktop ≥1280px:三栏,无 drawers-mode;折叠 class 与 localStorage 联动', async () => {
|
||
const w = await mountShell(1440);
|
||
const shell = w.find('.app-shell');
|
||
expect(shell.exists()).toBe(true);
|
||
expect(shell.classes()).not.toContain('drawers-mode');
|
||
expect(shell.classes()).not.toContain('mobile');
|
||
|
||
// 默认展开
|
||
expect(shell.classes()).not.toContain('side-collapsed');
|
||
expect(shell.classes()).not.toContain('bench-collapsed');
|
||
|
||
// 折叠把手点击 → side-collapsed + localStorage
|
||
await w.find('.side-handle-btn').trigger('click');
|
||
expect(w.find('.app-shell').classes()).toContain('side-collapsed');
|
||
expect(localStorage.getItem('pa-side-collapsed')).toBe('1');
|
||
await w.find('.side-handle-btn').trigger('click');
|
||
expect(localStorage.getItem('pa-side-collapsed')).toBe('0');
|
||
|
||
// bench 折叠(折叠入口按钮)
|
||
await w.find('.bench-collapse-btn').trigger('click');
|
||
expect(w.find('.app-shell').classes()).toContain('bench-collapsed');
|
||
expect(localStorage.getItem('pa-bench-collapsed')).toBe('1');
|
||
w.unmount();
|
||
});
|
||
|
||
it('折叠状态刷新后保持(localStorage 持久化)', async () => {
|
||
localStorage.setItem('pa-side-collapsed', '1');
|
||
localStorage.setItem('pa-bench-collapsed', '1');
|
||
const w = await mountShell(1440);
|
||
const cls = w.find('.app-shell').classes();
|
||
expect(cls).toContain('side-collapsed');
|
||
expect(cls).toContain('bench-collapsed');
|
||
w.unmount();
|
||
});
|
||
|
||
it('laptop 1024–1279px:三栏保留(CSS 锁折叠),无 drawers-mode', async () => {
|
||
const w = await mountShell(1180);
|
||
const cls = w.find('.app-shell').classes();
|
||
expect(cls).not.toContain('drawers-mode');
|
||
expect(cls).not.toContain('mobile');
|
||
// 折叠把手在笔记本档由 CSS 隐藏(类逻辑上仍可点击,但桌面外不提供)
|
||
expect(w.find('.side-handle-btn').exists()).toBe(true);
|
||
w.unmount();
|
||
});
|
||
|
||
it('tablet 768–1023px:drawers-mode(左右栏改抽屉),无 mobile', async () => {
|
||
const w = await mountShell(900);
|
||
const cls = w.find('.app-shell').classes();
|
||
expect(cls).toContain('drawers-mode');
|
||
expect(cls).not.toContain('mobile');
|
||
expect(cls).toContain('has-tools');
|
||
// 网格中的左右栏不再渲染(抽屉形态)
|
||
expect(w.find('.aside-host').exists()).toBe(false);
|
||
expect(w.find('.bench-wrap').exists()).toBe(false);
|
||
// 抽屉组件存在(初始关闭)
|
||
expect(w.findAllComponents({ name: 'Drawer' })).toBeTruthy();
|
||
w.unmount();
|
||
});
|
||
|
||
it('mobile <768px:drawers-mode + mobile', async () => {
|
||
const w = await mountShell(600);
|
||
const cls = w.find('.app-shell').classes();
|
||
expect(cls).toContain('drawers-mode');
|
||
expect(cls).toContain('mobile');
|
||
expect(w.find('.aside-host').exists()).toBe(false);
|
||
expect(w.find('.bench-wrap').exists()).toBe(false);
|
||
w.unmount();
|
||
});
|
||
|
||
it('汉堡按钮打开会话抽屉(<1024px)', async () => {
|
||
const w = await mountShell(900);
|
||
// 顶栏汉堡按钮(menuAvailable 为 true 时渲染)
|
||
const burger = w.find('.top-bar .hamburger-btn');
|
||
expect(burger.exists()).toBe(true);
|
||
await burger.trigger('click');
|
||
await nextTick();
|
||
// Drawer 已打开(Teleport 到 body,用 document 查询)
|
||
expect(document.querySelectorAll('.drawer.left.open').length).toBe(1);
|
||
// Esc 关闭
|
||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||
await nextTick();
|
||
expect(document.querySelectorAll('.drawer.left.open').length).toBe(0);
|
||
w.unmount();
|
||
});
|
||
|
||
it('工作台按钮打开右抽屉(<1024px)', async () => {
|
||
const w = await mountShell(900);
|
||
const btn = w.find('.top-bar .bench-btn');
|
||
expect(btn.exists()).toBe(true);
|
||
await btn.trigger('click');
|
||
await nextTick();
|
||
expect(document.querySelectorAll('.drawer.right.open').length).toBe(1);
|
||
// scrim 点击关闭
|
||
document.querySelector('.drawer-scrim')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||
await nextTick();
|
||
expect(document.querySelectorAll('.drawer.right.open').length).toBe(0);
|
||
w.unmount();
|
||
});
|
||
});
|