Skip to content

Commit 1f9fe6d

Browse files
committed
test(layers): characterize LayerPickingManager delegate (stage-6 6.1)
阶段 6.1:为阶段 1.3b LayerPickingManager delegate 补独立特征化 spec (picking-manager.spec.ts 22 cases),此前仅经 BaseLayer 子类渲染/插件 路径间接覆盖。以满足窄依赖面的 mock layer 直连 delegate。 pick/boxSelect:thin forward(interactionService.triggerHover / pickingService.boxPickLayer 透传)。pickId/selectedId 状态读写(默认 null)。 needPick 合成规则 8 case:isVisible 短路、eventNames 含 type/un+type、 click/dblclick + enableSelect、mousemove + enableHighlight 或 mouseenter/unmousemove/mouseout。active/select 配置派生 6 case: object→enable=true+color/mix,boolean→!!options,返回 layer 链式。 setActive/setSelect 双分支 4 case:object id {x,y} → updateLayerConfig (color/mix 来自 options) + pick;number id → updateLayerConfig ({pickedFeatureID, color/mix 来自 getLayerConfig}) + hooks.beforeHighlight/beforeSelect.call(encodePickingColor(id)) + 1ms setTimeout rerender(魔法延迟 quirk,fake timers 锁定)。 注:SyncHook.call 返回 Promise(@antv/async-hook series),故 .then 成立。 纯测试刀:仅改 __tests__/ + docs/PLAN,不进发布包,零产线改动、 零 changeset。验证:eslint 0 error、prettier 通过、新 spec 22 passed、 非 GL 回归子集 25 suites/158 passed 与基线一致。
1 parent 226f6cc commit 1f9fe6d

2 files changed

Lines changed: 327 additions & 1 deletion

File tree

docs/refactoring/layers/PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ citybuilding/ geometry/ — 12 个具体图层 extends BaseLayer(各自只 ove
112112
### 阶段 6 — 测试 & 文档(持续)
113113

114114
- 6.1 为每个 delegate(1.1–1.9)补独立 spec(mock container + 插件),不再仅靠子类间接覆盖。
115-
- 进度:LayerConfigModel(`config-tracks.spec`)✅ / LayerRelativeCoords(1.4 `relative-coords.spec`)✅ / LayerAnimateState(1.5 `animate-state.spec`)✅ / LayerVisibilityZoom(1.8 `visibility-zoom.spec`)✅ / LayerScaleLegend(1.6 `scale-legend.spec`)✅ / LayerMaskManager(1.7 `mask-manager.spec`)✅ / encodeStyles(3.3 `encode-styles.spec`)✅。剩 LayerPickService(1.3a)/LayerPickingManager(1.3b)/TextureService(1.6/1.7) 待补。
115+
- 进度:LayerConfigModel(`config-tracks.spec`)✅ / LayerRelativeCoords(1.4 `relative-coords.spec`)✅ / LayerAnimateState(1.5 `animate-state.spec`)✅ / LayerVisibilityZoom(1.8 `visibility-zoom.spec`)✅ / LayerScaleLegend(1.6 `scale-legend.spec`)✅ / LayerMaskManager(1.7 `mask-manager.spec`)✅ / LayerPickingManager(1.3b `picking-manager.spec`)✅ / encodeStyles(3.3 `encode-styles.spec`)✅。剩 LayerPickService(1.3a)/TextureService(1.6/1.7) 待补。
116116
- 6.2 交互编码开关(`enableShaderEncodeStyles/enableDataEncodeStyles`)补 spec,锁定 shader/data 编码分流。
117117
- 6.3 ✅ 相对坐标、animate 状态机补纯单测:为阶段 1.4 `LayerRelativeCoords` 与 1.5 `LayerAnimateState` 两个 delegate 补独立特征化 spec(`__tests__/core/relative-coords.spec.ts` 8 cases + `animate-state.spec.ts` 9 cases),以满足窄依赖面的 mock layer 直连 delegate,锁定 guard/默认初值/wiring/快照时序/control flow/时钟透传/副作用等可观测契约;此前仅经 BaseLayer 子类渲染路径间接覆盖。纯测试刀,零产线改动。
118118
- 6.4 `BaseLayer` 的 delegate 转发用「接口契约 spec」模式(类似 maps `base-map-event.spec.ts`)锁定对外透明。
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
import { encodePickingColor } from '@antv/l7-utils';
2+
import type BaseLayer from '../../src/core/BaseLayer';
3+
import LayerPickingManager from '../../src/core/LayerPickingManager';
4+
5+
/**
6+
* 阶段 6.1:拾取编排 delegate(`LayerPickingManager`,阶段 1.3b 抽出)独立
7+
* 特征化 spec。当前该 delegate 仅经 BaseLayer 子类渲染/插件路径间接覆盖;
8+
* 本 spec 以满足窄依赖面的 mock layer 直连 delegate,锁定其可观测契约:
9+
*
10+
* - pick/boxSelect:thin forward(interactionService.triggerHover /
11+
* pickingService.boxPickLayer 透传)
12+
* - pickId/selectedId 状态:set/get 读写
13+
* - needPick:`isVisible() && isPick`,isPick 由 eventNames indexOf +
14+
* enableHighlight/enableSelect + click/dblclick/mousemove 规则合成
15+
* - active/select:配置派生(object → enable=true + color/mix;boolean → !!)
16+
* + 返回 layer(链式)
17+
* - setActive/setSelect 双分支:object id {x,y} → updateLayerConfig(color/mix)
18+
* + pick({x,y});number id → updateLayerConfig({pickedFeatureID, color, mix})
19+
* + hooks.beforeHighlight/beforeSelect.call(encodePickingColor(id))
20+
* .then(() => setTimeout(rerender, 1))(魔法 1ms 延迟 quirk)
21+
*
22+
* 任何后续 delegate 改动破坏 forward / 配置派生 / hook 调度 / 延迟即应使本 spec 失败。
23+
*/
24+
25+
interface MockHook {
26+
call: (arg: unknown) => Promise<void>;
27+
}
28+
interface MockHandle {
29+
container: {
30+
interactionService: { triggerHover: (pos: { x: number; y: number }) => void };
31+
pickingService: {
32+
boxPickLayer: (layer: unknown, box: number[], cb: (...a: unknown[]) => void) => void;
33+
};
34+
};
35+
getLayerConfig: () => Record<string, unknown>;
36+
eventNames: () => string[];
37+
isVisible: () => boolean;
38+
updateLayerConfig: (patch: Record<string, unknown>) => void;
39+
hooks: { beforeHighlight: MockHook; beforeSelect: MockHook };
40+
calls: {
41+
update: Array<Record<string, unknown>>;
42+
triggerHover: Array<{ x: number; y: number }>;
43+
boxPick: number;
44+
highlightCallArgs: unknown[];
45+
selectCallArgs: unknown[];
46+
};
47+
}
48+
49+
function makeLayer(cfg: Record<string, unknown> = {}): MockHandle {
50+
const config = {
51+
enableHighlight: true,
52+
enableSelect: true,
53+
highlightColor: '#fff',
54+
activeMix: 0,
55+
selectColor: '#000',
56+
selectMix: 0,
57+
...cfg,
58+
};
59+
let eventNames: string[] = [];
60+
let visible = true;
61+
const calls: MockHandle['calls'] = {
62+
update: [],
63+
triggerHover: [],
64+
boxPick: 0,
65+
highlightCallArgs: [],
66+
selectCallArgs: [],
67+
};
68+
return {
69+
calls,
70+
container: {
71+
interactionService: {
72+
triggerHover: (pos) => {
73+
calls.triggerHover.push(pos);
74+
},
75+
},
76+
pickingService: {
77+
boxPickLayer: () => {
78+
calls.boxPick++;
79+
},
80+
},
81+
},
82+
getLayerConfig: () => config,
83+
eventNames: () => eventNames,
84+
isVisible: () => visible,
85+
updateLayerConfig: (patch) => {
86+
Object.assign(config, patch);
87+
calls.update.push(patch);
88+
},
89+
hooks: {
90+
beforeHighlight: {
91+
call: (arg) => {
92+
calls.highlightCallArgs.push(arg);
93+
return Promise.resolve();
94+
},
95+
},
96+
beforeSelect: {
97+
call: (arg) => {
98+
calls.selectCallArgs.push(arg);
99+
return Promise.resolve();
100+
},
101+
},
102+
},
103+
// 测试辅助(非 delegate 依赖面)
104+
setEventNames: (n: string[]) => {
105+
eventNames = n;
106+
},
107+
setVisible: (v: boolean) => {
108+
visible = v;
109+
},
110+
} as MockHandle;
111+
}
112+
113+
function bind(layer: MockHandle, rerender: () => void): LayerPickingManager {
114+
return new LayerPickingManager(layer as unknown as BaseLayer, rerender);
115+
}
116+
117+
describe('picking-manager delegate (stage-6 6.1)', () => {
118+
describe('pick / boxSelect forward', () => {
119+
it('pick({x,y}) → interactionService.triggerHover 透传', () => {
120+
const layer = makeLayer();
121+
const s = bind(layer, () => {});
122+
s.pick({ x: 10, y: 20 });
123+
expect(layer.calls.triggerHover).toEqual([{ x: 10, y: 20 }]);
124+
});
125+
126+
it('boxSelect(box, cb) → pickingService.boxPickLayer 透传', () => {
127+
const layer = makeLayer();
128+
const s = bind(layer, () => {});
129+
const cb = () => {};
130+
s.boxSelect([0, 0, 10, 10] as [number, number, number, number], cb);
131+
expect(layer.calls.boxPick).toBe(1);
132+
});
133+
});
134+
135+
describe('pickId / selectedId 状态', () => {
136+
it('setCurrentPickId/getCurrentPickId 读写(默认 null)', () => {
137+
const s = bind(makeLayer(), () => {});
138+
expect(s.getCurrentPickId()).toBeNull();
139+
s.setCurrentPickId(7);
140+
expect(s.getCurrentPickId()).toBe(7);
141+
});
142+
143+
it('setCurrentSelectedId/getCurrentSelectedId 读写(默认 null)', () => {
144+
const s = bind(makeLayer(), () => {});
145+
expect(s.getCurrentSelectedId()).toBeNull();
146+
s.setCurrentSelectedId(3);
147+
expect(s.getCurrentSelectedId()).toBe(3);
148+
});
149+
});
150+
151+
describe('needPick 合成规则', () => {
152+
it('isVisible=false → 短路 false', () => {
153+
const layer = makeLayer();
154+
layer.setVisible(false);
155+
layer.setEventNames(['click']);
156+
const s = bind(layer, () => {});
157+
expect(s.needPick('click')).toBe(false);
158+
});
159+
160+
it('eventNames 含 type → true(visible)', () => {
161+
const layer = makeLayer();
162+
layer.setEventNames(['click']);
163+
const s = bind(layer, () => {});
164+
expect(s.needPick('click')).toBe(true);
165+
});
166+
167+
it('eventNames 含 un+type → true', () => {
168+
const layer = makeLayer();
169+
layer.setEventNames(['unclick']);
170+
const s = bind(layer, () => {});
171+
expect(s.needPick('click')).toBe(true);
172+
});
173+
174+
it('click 不在 eventNames + enableSelect=true → true', () => {
175+
const layer = makeLayer({ enableSelect: true });
176+
layer.setEventNames([]);
177+
const s = bind(layer, () => {});
178+
expect(s.needPick('click')).toBe(true);
179+
});
180+
181+
it('click + enableSelect=false + 不在 eventNames → false', () => {
182+
const layer = makeLayer({ enableSelect: false });
183+
layer.setEventNames([]);
184+
const s = bind(layer, () => {});
185+
expect(s.needPick('click')).toBe(false);
186+
});
187+
188+
it('mousemove + enableHighlight=true → true(即使不在 eventNames)', () => {
189+
const layer = makeLayer({ enableHighlight: true });
190+
layer.setEventNames([]);
191+
const s = bind(layer, () => {});
192+
expect(s.needPick('mousemove')).toBe(true);
193+
});
194+
195+
it('mousemove + enableHighlight=false + 无 mouseenter/unmousemove/mouseout → false', () => {
196+
const layer = makeLayer({ enableHighlight: false });
197+
layer.setEventNames([]);
198+
const s = bind(layer, () => {});
199+
expect(s.needPick('mousemove')).toBe(false);
200+
});
201+
202+
it('mousemove + enableHighlight=false + eventNames 含 mouseenter → true', () => {
203+
const layer = makeLayer({ enableHighlight: false });
204+
layer.setEventNames(['mouseenter']);
205+
const s = bind(layer, () => {});
206+
expect(s.needPick('mousemove')).toBe(true);
207+
});
208+
});
209+
210+
describe('active / select 配置派生', () => {
211+
it('active(true) → enableHighlight=true,返回 layer(链式)', () => {
212+
const layer = makeLayer();
213+
const s = bind(layer, () => {});
214+
const ret = s.active(true);
215+
expect(ret).toBe(layer);
216+
expect(layer.calls.update).toContainEqual({ enableHighlight: true });
217+
});
218+
219+
it('active(false) → enableHighlight=false', () => {
220+
const layer = makeLayer();
221+
const s = bind(layer, () => {});
222+
s.active(false);
223+
expect(layer.calls.update).toContainEqual({ enableHighlight: false });
224+
});
225+
226+
it('active({color,mix}) → enableHighlight=true + highlightColor + activeMix', () => {
227+
const layer = makeLayer();
228+
const s = bind(layer, () => {});
229+
s.active({ color: '#abc', mix: 0.5 });
230+
expect(layer.calls.update).toContainEqual({
231+
enableHighlight: true,
232+
highlightColor: '#abc',
233+
activeMix: 0.5,
234+
});
235+
});
236+
237+
it('active({}) → 仅 enableHighlight=true', () => {
238+
const layer = makeLayer();
239+
const s = bind(layer, () => {});
240+
s.active({});
241+
expect(layer.calls.update).toContainEqual({ enableHighlight: true });
242+
});
243+
244+
it('select(boolean) → enableSelect 派生同 active 模式', () => {
245+
const layer = makeLayer();
246+
const s = bind(layer, () => {});
247+
s.select(false);
248+
expect(layer.calls.update).toContainEqual({ enableSelect: false });
249+
});
250+
251+
it('select({color,mix}) → enableSelect=true + selectColor + selectMix', () => {
252+
const layer = makeLayer();
253+
const s = bind(layer, () => {});
254+
s.select({ color: '#xyz', mix: 0.3 });
255+
expect(layer.calls.update).toContainEqual({
256+
enableSelect: true,
257+
selectColor: '#xyz',
258+
selectMix: 0.3,
259+
});
260+
});
261+
});
262+
263+
describe('setActive / setSelect 双分支 + 1ms 延迟 quirk', () => {
264+
beforeEach(() => {
265+
jest.useFakeTimers();
266+
});
267+
afterEach(() => {
268+
jest.useRealTimers();
269+
});
270+
271+
it('setActive({x,y}) → updateLayerConfig(color/mix 来自 options) + pick({x,y}),不调 hook', () => {
272+
const layer = makeLayer({ highlightColor: 'default', activeMix: 9 });
273+
const rerender = jest.fn();
274+
const s = bind(layer, rerender);
275+
s.setActive({ x: 5, y: 6 }, { color: '#opt', mix: 0.7 });
276+
expect(layer.calls.update).toContainEqual({ highlightColor: '#opt', activeMix: 0.7 });
277+
expect(layer.calls.triggerHover).toEqual([{ x: 5, y: 6 }]);
278+
expect(layer.calls.highlightCallArgs).toHaveLength(0); // object 分支不调 hook
279+
expect(rerender).not.toHaveBeenCalled();
280+
});
281+
282+
it('setActive(number) → updateLayerConfig({pickedFeatureID, color/mix 来自 getLayerConfig}) + hook.call(encodePickingColor(id)) + 1ms 后 rerender', async () => {
283+
const layer = makeLayer({ highlightColor: 'cfgHC', activeMix: 8 });
284+
const rerender = jest.fn();
285+
const s = bind(layer, rerender);
286+
s.setActive(5);
287+
expect(layer.calls.update).toContainEqual({
288+
pickedFeatureID: 5,
289+
highlightColor: 'cfgHC',
290+
activeMix: 8,
291+
});
292+
expect(layer.calls.highlightCallArgs).toEqual([encodePickingColor(5)]);
293+
expect(rerender).not.toHaveBeenCalled(); // setTimeout 未到
294+
await Promise.resolve(); // flush .then 微任务 → 注册 setTimeout
295+
expect(rerender).not.toHaveBeenCalled(); // 仍未到
296+
jest.advanceTimersByTime(1);
297+
expect(rerender).toHaveBeenCalledTimes(1); // 1ms quirk
298+
});
299+
300+
it('setSelect({x,y}) → updateLayerConfig(selectColor/selectMix 来自 options) + pick', () => {
301+
const layer = makeLayer({ selectColor: 'def', selectMix: 1 });
302+
const rerender = jest.fn();
303+
const s = bind(layer, rerender);
304+
s.setSelect({ x: 1, y: 2 }, { color: '#s', mix: 0.4 });
305+
expect(layer.calls.update).toContainEqual({ selectColor: '#s', selectMix: 0.4 });
306+
expect(layer.calls.triggerHover).toEqual([{ x: 1, y: 2 }]);
307+
expect(layer.calls.selectCallArgs).toHaveLength(0);
308+
});
309+
310+
it('setSelect(number) → hook.beforeSelect.call(encodePickingColor(id)) + 1ms 后 rerender', async () => {
311+
const layer = makeLayer({ selectColor: 'cfgSC', selectMix: 2 });
312+
const rerender = jest.fn();
313+
const s = bind(layer, rerender);
314+
s.setSelect(9);
315+
expect(layer.calls.update).toContainEqual({
316+
pickedFeatureID: 9,
317+
selectColor: 'cfgSC',
318+
selectMix: 2,
319+
});
320+
expect(layer.calls.selectCallArgs).toEqual([encodePickingColor(9)]);
321+
await Promise.resolve();
322+
jest.advanceTimersByTime(1);
323+
expect(rerender).toHaveBeenCalledTimes(1);
324+
});
325+
});
326+
});

0 commit comments

Comments
 (0)