This commit is contained in:
2026-09-12 17:08:25 +08:00
parent 9c06d3f4be
commit a1089efd18
7 changed files with 476 additions and 22 deletions

225
tests/unit/splitter.test.js Normal file
View File

@@ -0,0 +1,225 @@
// src/splitter.js —— 通用拖拽分割条工厂测试
//
// 覆盖 getDragSign 的方向反转逻辑:
// - 不传 getDragSign默认→ 鼠标右移让目标变大(+1
// - 传 getDragSign 返回 -1 → 鼠标右移让目标变小(用于 AI 面板在 splitter
// 右侧且贴窗口右边缘的场景,避免 splitter 视觉反向跟随鼠标)
// - getDragSign 抛错或返回非 -1/1 → 兜底 +1不让一次坏拖动摧毁整个交互
//
// 通过 jsdom 模拟 document/element绕过 mountAiSplitter 的 DOM 依赖,直接
// 驱动 createSplitter 的 mousedown/mousemove 路径验证 setSize 调用序列。
//
// 不覆盖:
// - 命中区hitZone扩展 → 那是 mountSidebarSplitter/mountAiSplitter 的
// 调用方职责,工厂只关心"接到 hit zone 触发后行为正确"。
// - touch 路径 → 与 mouse 路径走同一道 startDrag/applyDrag代码层同源。
// @vitest-environment jsdom
import { describe, it, expect, vi } from 'vitest';
import { createSplitter } from '../../src/splitter.js';
/**
* 在 jsdom 里构造一个最小可用的 splitter
* - handle 是按钮元素
* - getSize/setSize/clampSize/onDragEnd 由用例注入
* - 不传 hitZone / initialSize / windowListeners与 mountAiSplitter 主路径
* 解耦,单独验证方向反转这条新分支)
*
* 返回 {handle, fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls}
* - fireMouseDown(clientX) / fireMouseMove / fireMouseUp 模拟真实事件
* - setSizeCalls 记录所有 setSize 调用,可断言方向与幅度
*/
function makeSplitter({ getSize, setSize, clampSize, getDragSign }) {
const handle = document.createElement('div');
handle.style.width = '4px';
document.body.appendChild(handle);
const onDragEnd = vi.fn();
const inst = createSplitter({
handle,
axis: 'x',
getSize,
setSize,
clampSize,
onDragEnd,
getDragSign,
});
const setSizeCalls = [];
const wrappedSetSize = (px) => {
setSizeCalls.push(px);
setSize(px);
};
// 重新绑一个 setSize 用于观察
inst.dispose();
const inst2 = createSplitter({
handle,
axis: 'x',
getSize,
setSize: wrappedSetSize,
clampSize,
onDragEnd,
getDragSign,
});
const fireMouseDown = (clientX) => {
handle.dispatchEvent(new MouseEvent('mousedown', { clientX, bubbles: true }));
};
const fireMouseMove = (clientX) => {
document.dispatchEvent(new MouseEvent('mousemove', { clientX, bubbles: true }));
};
const fireMouseUp = (clientX = 0) => {
document.dispatchEvent(new MouseEvent('mouseup', { clientX, bubbles: true }));
};
return {
handle,
inst: inst2,
onDragEnd,
fireMouseDown,
fireMouseMove,
fireMouseUp,
setSizeCalls,
};
}
describe('createSplitter —— 方向反转 (getDragSign)', () => {
it('不传 getDragSign鼠标右移 → 目标变大(默认 +1', () => {
let current = 300;
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls, onDragEnd } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(200, Math.min(800, px)),
});
fireMouseDown(500);
fireMouseMove(560); // +60
fireMouseUp();
expect(setSizeCalls).toEqual([360]); // 300 + 60
expect(onDragEnd).toHaveBeenCalledWith(360);
});
it('getDragSign 返回 -1鼠标右移 → 目标变小AI 面板在 splitter 右侧场景)', () => {
let current = 360;
const signProvider = vi.fn(() => -1);
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls, onDragEnd } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(220, Math.min(720, px)),
getDragSign: signProvider,
});
fireMouseDown(500);
fireMouseMove(560); // +60 但应反向 → 减 60
fireMouseUp();
expect(setSizeCalls).toEqual([300]); // 360 - 60
expect(signProvider).toHaveBeenCalledTimes(1); // 仅 startDrag 时调用一次
expect(onDragEnd).toHaveBeenCalledWith(300);
});
it('getDragSign 返回 +1与默认行为一致防御性测试', () => {
let current = 360;
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(220, Math.min(720, px)),
getDragSign: () => 1,
});
fireMouseDown(500);
fireMouseMove(560);
fireMouseUp();
expect(setSizeCalls).toEqual([420]); // 360 + 60
});
it('getDragSign 在每次 startDrag 时重新求值(应对切模式)', () => {
let current = 360;
// 模拟「第一次拖动split 模式)= +1第二次拖动preview 模式)= -1」
const signProvider = vi.fn()
.mockReturnValueOnce(1)
.mockReturnValueOnce(-1);
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(220, Math.min(720, px)),
getDragSign: signProvider,
});
// 第一次拖动(+1鼠标右移 50 → current 应 +50
fireMouseDown(500);
fireMouseMove(550);
fireMouseUp();
expect(current).toBe(410);
// 第二次拖动(-1鼠标右移 50 → current 应 -50
fireMouseDown(500);
fireMouseMove(550);
fireMouseUp();
expect(current).toBe(360);
expect(signProvider).toHaveBeenCalledTimes(2);
expect(setSizeCalls).toEqual([410, 360]);
});
it('getDragSign 抛错 → 兜底 +1不让坏回调炸掉拖动', () => {
let current = 360;
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(220, Math.min(720, px)),
// 抛错 → 工厂内部 try/catch 兜底为 +1拖动继续按默认方向进行
getDragSign: () => { throw new Error('layout not ready'); },
});
fireMouseDown(500); // 内部吞掉 getDragSign 抛错dragSign = 1
fireMouseMove(560); // startSize(360) + dragSign(1) * +60 = 420
fireMouseUp();
expect(setSizeCalls).toEqual([420]);
});
it('getDragSign 返回非 -1/1 的奇怪值 → 归一为 +1', () => {
let current = 360;
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(220, Math.min(720, px)),
getDragSign: () => 0, // 0 不是合法符号 → 应被识别为 +1
});
fireMouseDown(500);
fireMouseMove(560);
fireMouseUp();
expect(setSizeCalls).toEqual([420]); // 360 + 60按 +1 算)
});
it('reverse 方向下 clamp 仍生效(防止拖出 MIN_WIDTH', () => {
let current = 230;
const { fireMouseDown, fireMouseMove, fireMouseUp, setSizeCalls } =
makeSplitter({
getSize: () => current,
setSize: (px) => { current = px; },
clampSize: (px) => Math.max(220, Math.min(720, px)),
getDragSign: () => -1,
});
fireMouseDown(500);
// 鼠标右移 100 → 当前期望 current - 100 = 130但 clamp 应钉到 MIN=220
fireMouseMove(600);
fireMouseUp();
expect(setSizeCalls).toEqual([220]);
});
});