Files
Notes/tests/unit/splitter.test.js
2026-09-12 17:08:25 +08:00

225 lines
7.4 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/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]);
});
});