Files
胡绍康andCopilot cf7817bb95 feat: UI全局美化重设计 + 恢复CI/测试/文档
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>
2026-03-01 21:24:06 +08:00

354 lines
11 KiB
JavaScript
Raw Permalink 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.
/**
* 布局系统可访问性测试
*
* 测试目标:
* - ARIA 属性完整性
* - 键盘导航
* - 焦点管理
* - 屏幕阅读器兼容性(aria-label, aria-expanded, aria-valuenow)
*
* 参考文档:TD 第九章 - 可访问性测试方案
* 标准:WCAG 2.1 AA
*/
const {
createLayoutDOM,
getCSSVariable,
simulateKeyPress,
wait,
hasFocus
} = require('../test-utils');
const LayoutManager = require('../../../static/js/layout-manager.js');
// ========================
// 1. Resizer ARIA 属性
// ========================
describe('可访问性 - Resizer ARIA 属性', () => {
beforeEach(() => {
createLayoutDOM();
});
test('所有 resizer 应有 role="separator"', () => {
const resizers = document.querySelectorAll('.resizer');
resizers.forEach(resizer => {
expect(resizer.getAttribute('role')).toBe('separator');
});
});
test('所有 resizer 应有 aria-orientation="vertical"', () => {
const resizers = document.querySelectorAll('.resizer');
resizers.forEach(resizer => {
expect(resizer.getAttribute('aria-orientation')).toBe('vertical');
});
});
test('所有 resizer 应有 aria-label', () => {
const resizers = document.querySelectorAll('.resizer');
resizers.forEach(resizer => {
const label = resizer.getAttribute('aria-label');
expect(label).toBeTruthy();
expect(label.length).toBeGreaterThan(0);
});
});
test('所有 resizer 应有 aria-valuenow/min/max', () => {
const resizers = document.querySelectorAll('.resizer');
resizers.forEach(resizer => {
expect(resizer.getAttribute('aria-valuenow')).toBeTruthy();
expect(resizer.getAttribute('aria-valuemin')).toBeTruthy();
expect(resizer.getAttribute('aria-valuemax')).toBeTruthy();
// 数值应为正整数
const now = parseInt(resizer.getAttribute('aria-valuenow'));
const min = parseInt(resizer.getAttribute('aria-valuemin'));
const max = parseInt(resizer.getAttribute('aria-valuemax'));
expect(now).toBeGreaterThan(0);
expect(min).toBeGreaterThan(0);
expect(max).toBeGreaterThan(min);
expect(now).toBeGreaterThanOrEqual(min);
expect(now).toBeLessThanOrEqual(max);
});
});
test('所有 resizer 应有 tabindex="0" 以支持键盘聚焦', () => {
const resizers = document.querySelectorAll('.resizer');
resizers.forEach(resizer => {
expect(resizer.getAttribute('tabindex')).toBe('0');
});
});
test('所有 resizer 应有 data-resizer 属性标识对应面板', () => {
const expected = ['groups', 'accounts', 'emails'];
const resizers = document.querySelectorAll('.resizer');
const actual = Array.from(resizers).map(r => r.getAttribute('data-resizer'));
expected.forEach(panel => {
expect(actual).toContain(panel);
});
});
});
// ========================
// 2. 折叠按钮 ARIA 属性
// ========================
describe('可访问性 - 折叠按钮 ARIA 属性', () => {
beforeEach(() => {
createLayoutDOM();
});
test('所有折叠按钮应有 aria-expanded 属性', () => {
const buttons = document.querySelectorAll('.collapse-btn');
buttons.forEach(btn => {
expect(btn.getAttribute('aria-expanded')).toBeTruthy();
});
});
test('所有折叠按钮应有 aria-label 属性', () => {
const buttons = document.querySelectorAll('.collapse-btn');
buttons.forEach(btn => {
const label = btn.getAttribute('aria-label');
expect(label).toBeTruthy();
expect(label.length).toBeGreaterThan(0);
});
});
test('所有折叠按钮应有 data-panel 属性', () => {
const buttons = document.querySelectorAll('.collapse-btn');
buttons.forEach(btn => {
expect(btn.getAttribute('data-panel')).toBeTruthy();
});
});
test('初始状态下所有面板 aria-expanded 应为 "true"', () => {
const buttons = document.querySelectorAll('.collapse-btn');
buttons.forEach(btn => {
expect(btn.getAttribute('aria-expanded')).toBe('true');
});
});
});
// ========================
// 3. 折叠/展开时 ARIA 更新
// ========================
describe('可访问性 - 折叠/展开 ARIA 更新', () => {
let manager;
beforeEach(() => {
createLayoutDOM();
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
manager = new LayoutManager();
manager.init();
});
test('折叠面板后 aria-expanded 应更新为 "false"', () => {
manager.collapsePanel('groups');
const btn = document.querySelector('.collapse-btn[data-panel="groups"]');
expect(btn.getAttribute('aria-expanded')).toBe('false');
});
test('展开面板后 aria-expanded 应恢复为 "true"', () => {
manager.collapsePanel('groups');
manager.expandPanel('groups');
const btn = document.querySelector('.collapse-btn[data-panel="groups"]');
expect(btn.getAttribute('aria-expanded')).toBe('true');
});
test('togglePanel 应正确切换 aria-expanded', () => {
const btn = document.querySelector('.collapse-btn[data-panel="accounts"]');
expect(btn.getAttribute('aria-expanded')).toBe('true');
manager.togglePanel('accounts');
expect(btn.getAttribute('aria-expanded')).toBe('false');
manager.togglePanel('accounts');
expect(btn.getAttribute('aria-expanded')).toBe('true');
});
});
// ========================
// 4. 键盘导航
// ========================
describe('可访问性 - 键盘导航', () => {
let manager;
beforeEach(() => {
createLayoutDOM();
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
manager = new LayoutManager();
manager.init();
});
test('resizer 应可通过 Tab 键聚焦', () => {
const resizer = document.querySelector('[data-resizer="groups"]');
resizer.focus();
expect(document.activeElement).toBe(resizer);
});
test('← 键应减小面板宽度(每次 10px)', () => {
const resizer = document.querySelector('[data-resizer="groups"]');
resizer.focus();
const initialWidth = getCSSVariable('--group-panel-width');
const initialValue = parseInt(initialWidth);
simulateKeyPress(resizer, 'ArrowLeft', false);
const newWidth = getCSSVariable('--group-panel-width');
const newValue = parseInt(newWidth);
// 宽度应减小(可能受最小宽度限制)
expect(newValue).toBeLessThanOrEqual(initialValue);
});
test('→ 键应增大面板宽度(每次 10px)', () => {
const resizer = document.querySelector('[data-resizer="groups"]');
resizer.focus();
const initialWidth = getCSSVariable('--group-panel-width');
const initialValue = parseInt(initialWidth);
simulateKeyPress(resizer, 'ArrowRight', false);
const newWidth = getCSSVariable('--group-panel-width');
const newValue = parseInt(newWidth);
// 宽度应增大(可能受最大宽度限制)
expect(newValue).toBeGreaterThanOrEqual(initialValue);
});
test('Shift + → 键应快速增大面板宽度(每次 50px)', () => {
const resizer = document.querySelector('[data-resizer="groups"]');
resizer.focus();
// 先用普通 → 键测量普通步进
const before1 = parseInt(getCSSVariable('--group-panel-width'));
simulateKeyPress(resizer, 'ArrowRight', false);
const after1 = parseInt(getCSSVariable('--group-panel-width'));
const normalDelta = after1 - before1;
// 再用 Shift + → 键测量快速步进
const before2 = parseInt(getCSSVariable('--group-panel-width'));
simulateKeyPress(resizer, 'ArrowRight', true);
const after2 = parseInt(getCSSVariable('--group-panel-width'));
const shiftDelta = after2 - before2;
// Shift 步进应 >= 普通步进(除非已达最大宽度限制)
if (after2 < 400) {
expect(shiftDelta).toBeGreaterThanOrEqual(normalDelta);
} else {
expect(shiftDelta).toBeGreaterThanOrEqual(0);
}
});
test('折叠按钮绑定了 click 事件并可触发折叠', () => {
const btn = document.querySelector('.collapse-btn[data-panel="groups"]');
btn.focus();
// LayoutManager 通过 addEventListener('click') 绑定折叠逻辑
// 在 jsdom 中直接派发 click 事件
btn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
const panel = document.querySelector('[data-panel="groups"]');
expect(panel.classList.contains('collapsed')).toBe(true);
});
});
// ========================
// 5. 键盘调整后 ARIA 值更新
// ========================
describe('可访问性 - 键盘调整 ARIA 值更新', () => {
let manager;
beforeEach(() => {
createLayoutDOM();
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
manager = new LayoutManager();
manager.init();
});
test('键盘调整宽度后 aria-valuenow 应更新', () => {
const resizer = document.querySelector('[data-resizer="groups"]');
const initialValue = resizer.getAttribute('aria-valuenow');
resizer.focus();
simulateKeyPress(resizer, 'ArrowRight', false);
const newValue = resizer.getAttribute('aria-valuenow');
// 值应发生变化(除非已达到最大值)
const initial = parseInt(initialValue);
const updated = parseInt(newValue);
expect(updated).toBeGreaterThanOrEqual(initial);
});
});
// ========================
// 6. 面板语义结构
// ========================
describe('可访问性 - 面板结构', () => {
beforeEach(() => {
createLayoutDOM();
});
test('每个可折叠面板应有 panel-header 和 panel-content', () => {
const panels = document.querySelectorAll('.resizable-panel');
panels.forEach(panel => {
expect(panel.querySelector('.panel-header')).toBeTruthy();
expect(panel.querySelector('.panel-content')).toBeTruthy();
});
});
test('邮件详情面板应存在且不可折叠', () => {
const detail = document.querySelector('.email-detail-panel');
expect(detail).toBeTruthy();
// 不应有折叠按钮
const collapseBtn = detail.querySelector('.collapse-btn');
expect(collapseBtn).toBeNull();
});
test('main-container 应包含所有面板', () => {
const container = document.querySelector('.main-container');
expect(container).toBeTruthy();
const panels = container.querySelectorAll('.resizable-panel');
expect(panels.length).toBe(3);
const detail = container.querySelector('.email-detail-panel');
expect(detail).toBeTruthy();
});
});
// ========================
// 7. 折叠指示条可访问性
// ========================
describe('可访问性 - 折叠指示条', () => {
let manager;
beforeEach(() => {
createLayoutDOM();
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 });
manager = new LayoutManager();
manager.init();
});
test('折叠后应创建可交互的指示条', () => {
manager.collapsePanel('groups');
// 检查是否存在折叠指示条
const indicator = document.querySelector('.collapsed-indicator');
if (indicator) {
// 指示条应可聚焦或可点击
expect(
indicator.getAttribute('tabindex') === '0' ||
indicator.getAttribute('role') === 'button' ||
indicator.tagName === 'BUTTON'
).toBe(true);
}
});
});