|
| 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