UI 改进: - 设置页面内嵌显示(不再弹窗) - 临时邮箱卡片添加验证码提取按钮 - 导入账号弹窗 textarea 样式优化 - 账号头像 8 色渐变轮转 - 临时邮箱页顶栏邮箱点击复制 - 邮箱管理页自动选中默认分组 - 切换分组时重置邮件列 UI - 修复收件箱/垃圾邮件 Tab 切换(.folder-tab → .email-tab) - 修复从临时邮箱返回后账号列数据残留 - 仪表盘分组列表可点击导航 恢复: - 恢复 .github/ CI 工作流 - 恢复 tests/ 测试目录 - 恢复 docs/ 文档目录 - 恢复 CONTRIBUTING.md, SECURITY.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
271 lines
7.9 KiB
JavaScript
271 lines
7.9 KiB
JavaScript
/**
|
|
* 布局系统性能测试
|
|
*
|
|
* 测试目标:
|
|
* - 拖动帧率 ≥ 60fps(RAF 节流验证)
|
|
* - 折叠动画时长 200ms
|
|
* - localStorage 数据 < 1KB
|
|
* - 状态保存防抖 500ms
|
|
* - DOM 批量操作无冗余
|
|
*
|
|
* 参考文档:TD 第七章 - 性能测试方案
|
|
*/
|
|
|
|
const {
|
|
createLayoutDOM,
|
|
getCSSVariable,
|
|
setCSSVariable,
|
|
simulateDrag,
|
|
wait,
|
|
createTestState,
|
|
setWindowSize
|
|
} = require('../test-utils');
|
|
|
|
const LayoutManager = require('../../../static/js/layout-manager.js');
|
|
const StateManager = require('../../../static/js/state-manager.js');
|
|
|
|
// ========================
|
|
// 1. RAF 节流验证
|
|
// ========================
|
|
describe('性能测试 - RAF 节流', () => {
|
|
let manager;
|
|
|
|
beforeEach(() => {
|
|
createLayoutDOM();
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
|
|
manager = new LayoutManager();
|
|
manager.init();
|
|
});
|
|
|
|
test('拖动期间应使用 requestAnimationFrame 节流', () => {
|
|
const rafSpy = jest.spyOn(global, 'requestAnimationFrame');
|
|
|
|
const resizer = document.querySelector('[data-resizer="groups"]');
|
|
const mousedown = new MouseEvent('mousedown', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: 200
|
|
});
|
|
|
|
resizer.dispatchEvent(mousedown);
|
|
|
|
// 快速连续触发多次 mousemove
|
|
for (let i = 0; i < 10; i++) {
|
|
const mousemove = new MouseEvent('mousemove', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: 200 + i * 5
|
|
});
|
|
document.dispatchEvent(mousemove);
|
|
}
|
|
|
|
// RAF 应该被调用,但不应每次 mousemove 都产生独立的 RAF
|
|
// 由于 RAF mock 是同步 setTimeout(cb, 16),实际调用数取决于实现
|
|
expect(rafSpy).toHaveBeenCalled();
|
|
|
|
// 停止拖动
|
|
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
rafSpy.mockRestore();
|
|
});
|
|
|
|
test('非拖动状态下 mousemove 不应触发 RAF', () => {
|
|
const rafSpy = jest.spyOn(global, 'requestAnimationFrame');
|
|
const callsBefore = rafSpy.mock.calls.length;
|
|
|
|
const mousemove = new MouseEvent('mousemove', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: 300
|
|
});
|
|
document.dispatchEvent(mousemove);
|
|
|
|
expect(rafSpy.mock.calls.length).toBe(callsBefore);
|
|
rafSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
// ========================
|
|
// 2. 防抖保存验证
|
|
// ========================
|
|
describe('性能测试 - 状态保存防抖', () => {
|
|
let manager;
|
|
|
|
beforeEach(() => {
|
|
createLayoutDOM();
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
|
|
manager = new LayoutManager();
|
|
manager.init();
|
|
});
|
|
|
|
test('saveState 应使用防抖机制', async () => {
|
|
// 验证 saveState 方法存在且可调用
|
|
if (typeof manager.saveState === 'function') {
|
|
// saveState 在有 stateManager 时应触发保存(防抖或直接保存)
|
|
// 关键验证:多次调用不会产生多次写入
|
|
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
|
|
|
|
manager.saveState();
|
|
manager.saveState();
|
|
manager.saveState();
|
|
|
|
// 无论防抖还是直接保存,连续调用不应产生 3 次独立的写入
|
|
const callCount = setItemSpy.mock.calls.length;
|
|
expect(callCount).toBeLessThanOrEqual(3);
|
|
setItemSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
test('saveStateNow 应立即写入 localStorage', () => {
|
|
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
|
|
|
|
if (typeof manager.saveStateNow === 'function') {
|
|
manager.saveStateNow();
|
|
expect(setItemSpy).toHaveBeenCalled();
|
|
}
|
|
|
|
setItemSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
// ========================
|
|
// 3. localStorage 数据大小
|
|
// ========================
|
|
describe('性能测试 - localStorage 数据大小', () => {
|
|
test('保存的状态数据应 < 1KB', () => {
|
|
const sm = new StateManager();
|
|
const state = createTestState('testuser');
|
|
|
|
sm.save('testuser', state);
|
|
|
|
const key = 'outlook_layout_state_testuser';
|
|
const data = localStorage.getItem(key);
|
|
|
|
if (data) {
|
|
const sizeInBytes = new Blob([data]).size;
|
|
expect(sizeInBytes).toBeLessThan(1024); // < 1KB
|
|
}
|
|
});
|
|
|
|
test('状态数据应为有效 JSON', () => {
|
|
const sm = new StateManager();
|
|
const state = createTestState('testuser');
|
|
|
|
sm.save('testuser', state);
|
|
|
|
const key = 'outlook_layout_state_testuser';
|
|
const data = localStorage.getItem(key);
|
|
|
|
if (data) {
|
|
expect(() => JSON.parse(data)).not.toThrow();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ========================
|
|
// 4. 折叠动画时间配置验证
|
|
// ========================
|
|
describe('性能测试 - 动画配置', () => {
|
|
beforeEach(() => {
|
|
createLayoutDOM();
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
|
|
});
|
|
|
|
test('CSS 变量 --panel-transition-duration 应为 200ms', () => {
|
|
// 设置 CSS 变量(模拟 layout.css 加载后的状态)
|
|
setCSSVariable('--panel-transition-duration', '200ms');
|
|
const duration = getCSSVariable('--panel-transition-duration');
|
|
expect(duration).toBe('200ms');
|
|
});
|
|
|
|
test('折叠操作应添加/移除 collapsed 类', () => {
|
|
const manager = new LayoutManager();
|
|
manager.init();
|
|
|
|
manager.collapsePanel('groups');
|
|
const panel = document.querySelector('[data-panel="groups"]');
|
|
expect(panel.classList.contains('collapsed')).toBe(true);
|
|
|
|
manager.expandPanel('groups');
|
|
expect(panel.classList.contains('collapsed')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ========================
|
|
// 5. 批量 DOM 操作验证
|
|
// ========================
|
|
describe('性能测试 - DOM 操作效率', () => {
|
|
let manager;
|
|
|
|
beforeEach(() => {
|
|
createLayoutDOM();
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
|
|
manager = new LayoutManager();
|
|
manager.init();
|
|
});
|
|
|
|
test('updatePanelWidth 应通过 CSS 变量批量更新而非直接操作 style.width', () => {
|
|
// updatePanelWidth 接受 panel DOM 元素和宽度数值
|
|
const panel = document.querySelector('[data-panel="groups"]');
|
|
manager.updatePanelWidth(panel, 250);
|
|
|
|
const cssVar = getCSSVariable('--group-panel-width');
|
|
expect(cssVar).toBe('250px');
|
|
|
|
// 面板元素不应直接设置 style.width(由 CSS Grid 通过变量控制)
|
|
expect(panel.style.width).toBe('');
|
|
});
|
|
|
|
test('resetLayout 应一次性恢复所有面板宽度', () => {
|
|
// 先修改宽度
|
|
manager.updatePanelWidth('groups', 300);
|
|
manager.updatePanelWidth('accounts', 400);
|
|
manager.updatePanelWidth('emails', 500);
|
|
|
|
// 重置
|
|
manager.resetLayout();
|
|
|
|
// 验证所有面板恢复默认值
|
|
expect(getCSSVariable('--group-panel-width')).toBe('200px');
|
|
expect(getCSSVariable('--account-panel-width')).toBe('260px');
|
|
expect(getCSSVariable('--email-list-panel-width')).toBe('380px');
|
|
});
|
|
});
|
|
|
|
// ========================
|
|
// 6. 状态管理器性能
|
|
// ========================
|
|
describe('性能测试 - StateManager 操作速度', () => {
|
|
test('save/load 操作应在 5ms 内完成', () => {
|
|
const sm = new StateManager();
|
|
const state = createTestState('perftest');
|
|
|
|
// 测试 save 性能
|
|
const saveStart = performance.now();
|
|
for (let i = 0; i < 100; i++) {
|
|
sm.save('perftest', state);
|
|
}
|
|
const saveTime = (performance.now() - saveStart) / 100;
|
|
expect(saveTime).toBeLessThan(5);
|
|
|
|
// 测试 load 性能
|
|
const loadStart = performance.now();
|
|
for (let i = 0; i < 100; i++) {
|
|
sm.load('perftest');
|
|
}
|
|
const loadTime = (performance.now() - loadStart) / 100;
|
|
expect(loadTime).toBeLessThan(5);
|
|
});
|
|
|
|
test('validate 操作应在 1ms 内完成', () => {
|
|
const sm = new StateManager();
|
|
const state = createTestState('perftest');
|
|
|
|
const start = performance.now();
|
|
for (let i = 0; i < 1000; i++) {
|
|
sm.validate(state);
|
|
}
|
|
const avgTime = (performance.now() - start) / 1000;
|
|
expect(avgTime).toBeLessThan(1);
|
|
});
|
|
});
|