Files
Notes/tests/unit/context-menu.test.js
2026-09-12 14:15:26 +08:00

115 lines
3.7 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.
// src/context-menu.js 的单测
//
// 主要覆盖 audit #C3critical memory leak修复
// - 快速 show→hide→show 循环rAF 回调里的 addEventListener 不应泄漏
// - 同步 hide() 应当取消未 fire 的 rAF避免它在我们 detach 之后再去挂监听
/* @vitest-environment jsdom */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ContextMenu } from '../../src/context-menu.js';
describe('ContextMenu show/hide lifecycle', () => {
let menu;
let container;
beforeEach(() => {
// 用独立 container避免污染全局 body / 与其它测试串扰
container = document.createElement('div');
container.id = 'context-menu-test-container';
document.body.appendChild(container);
menu = new ContextMenu({ container });
});
afterEach(() => {
menu.hide();
if (container.parentElement) container.parentElement.removeChild(container);
});
it('show 后 isVisible() → true', () => {
menu.show({
x: 100, y: 100,
items: [{ label: '复制', value: 'copy' }],
onSelect: () => {},
});
expect(menu.isVisible()).toBe(true);
expect(container.querySelector('.context-menu')).toBeTruthy();
});
it('hide 后 isVisible() → false', () => {
menu.show({
x: 100, y: 100,
items: [{ label: '复制', value: 'copy' }],
onSelect: () => {},
});
menu.hide();
expect(menu.isVisible()).toBe(false);
expect(container.querySelector('.context-menu')).toBeNull();
});
it('hide() 可重复调用(不抛错)', () => {
menu.show({
x: 100, y: 100,
items: [{ label: '复制', value: 'copy' }],
onSelect: () => {},
});
expect(() => {
menu.hide();
menu.hide();
menu.hide();
}).not.toThrow();
});
it('audit #C3show 后立即 hide应取消挂起的 rAF', async () => {
// 模拟场景用户打开菜单立刻关闭onSelect 路径)。原实现的 rAF 仍会
// fire 并挂 5 个 listener —— 现在必须被取消。
menu.show({
x: 100, y: 100,
items: [{ label: '复制', value: 'copy' }],
onSelect: () => {},
});
// rAF 还没 firejsdom 默认 fire但 show 同步执行后立即 hide 应抢先)
menu.hide();
// 等待足够时间让原 rAF如果没被取消会 fire 的窗口过去
await new Promise((r) => setTimeout(r, 50));
// 此时再 show 一次:必须能成功挂上新菜单
expect(() => {
menu.show({
x: 200, y: 200,
items: [{ label: '粘贴', value: 'paste' }],
onSelect: () => {},
});
}).not.toThrow();
expect(menu.isVisible()).toBe(true);
});
it('audit #C3show→hide→show 快速循环不抛错、不泄漏菜单节点', async () => {
// 模拟右键 → 选项 → 立即右键 的快速循环
for (let i = 0; i < 5; i += 1) {
menu.show({
x: 50 + i * 10, y: 50 + i * 10,
items: [{ label: `Item ${i}`, value: `v${i}` }],
onSelect: () => {},
});
menu.hide();
}
// 让所有挂起的 rAF 跑完
await new Promise((r) => setTimeout(r, 50));
// 容器里只应该有一个当前菜单,或者零个(取决于时序)
const menus = container.querySelectorAll('.context-menu');
expect(menus.length).toBeLessThanOrEqual(1);
expect(menu.isVisible()).toBe(false);
});
it('空 items 数组 → 不创建菜单', () => {
menu.show({ x: 100, y: 100, items: [], onSelect: () => {} });
expect(menu.isVisible()).toBe(false);
expect(container.querySelector('.context-menu')).toBeNull();
});
it('非数组 items → 不创建菜单', () => {
menu.show({ x: 100, y: 100, items: null, onSelect: () => {} });
expect(menu.isVisible()).toBe(false);
});
});