2026-08-25 20:47:08 +08:00
|
|
|
|
// 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';
|
|
|
|
|
|
|
2026-08-31 12:56:47 +08:00
|
|
|
|
import AppShell from '../../../../ui/src/components/layout/AppShell.vue';
|
2026-08-25 20:47:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|