1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {ZonedInteractionHandler} from './zoned_interaction_handler'; 16 17describe('ZonedInteractionHandler', () => { 18 let zih: ZonedInteractionHandler; 19 let div: HTMLElement; 20 21 beforeEach(() => { 22 // Create a DOM element 23 div = document.createElement('div'); 24 div.style.width = '100px'; 25 div.style.height = '100px'; 26 document.body.appendChild(div); 27 zih = new ZonedInteractionHandler(div); 28 }); 29 30 // Utility functions for simulating mouse events 31 function mouseup(x: number, y: number) { 32 simulateMouseEvent('mouseup', x, y); 33 } 34 35 function mousedown(x: number, y: number) { 36 simulateMouseEvent('mousedown', x, y); 37 } 38 39 function mousemove(x: number, y: number) { 40 simulateMouseEvent('mousemove', x, y); 41 } 42 43 function simulateMouseEvent(kind: string, x: number, y: number) { 44 div.dispatchEvent( 45 new MouseEvent(kind, { 46 bubbles: true, 47 clientX: x, 48 clientY: y, 49 }), 50 ); 51 } 52 53 test('overlapping zones', () => { 54 zih.update([ 55 { 56 id: 'foo', 57 area: {x: 50, y: 50, width: 50, height: 50}, 58 cursor: 'grab', 59 }, 60 { 61 id: 'bar', 62 area: {x: 0, y: 0, width: 100, height: 100}, 63 cursor: 'pointer', 64 }, 65 ]); 66 67 mousemove(30, 30); // inside 'bar' 68 expect(div.style.cursor).toBe('pointer'); 69 70 mousemove(70, 70); // inside 'foo' 71 expect(div.style.cursor).toBe('grab'); 72 }); 73 74 test('click', () => { 75 const handleMouseClick = jest.fn(() => {}); 76 77 zih.update([ 78 { 79 id: 'foo', 80 area: {x: 0, y: 0, width: 60, height: 60}, 81 onClick: handleMouseClick, 82 }, 83 ]); 84 85 // Simulate a mouse click 86 mousedown(50, 50); 87 mouseup(50, 50); 88 89 expect(handleMouseClick).toHaveBeenCalled(); 90 91 handleMouseClick.mockClear(); 92 93 // Simulate a mouse down then a mouseup outside the zone 94 mousedown(50, 50); 95 mouseup(80, 80); 96 97 expect(handleMouseClick).not.toHaveBeenCalled(); 98 }); 99 100 test('drag', () => { 101 const handleDrag = jest.fn(() => {}); 102 const handleDragEnd = jest.fn(() => {}); 103 104 zih.update([ 105 { 106 id: 'foo', 107 area: {x: 0, y: 0, width: 100, height: 100}, 108 drag: { 109 cursorWhileDragging: 'grabbing', 110 onDrag: handleDrag, 111 onDragEnd: handleDragEnd, 112 }, 113 }, 114 ]); 115 116 // Simulate a mouse drag start 117 mousedown(0, 0); 118 expect(div.style.cursor).toBe('grabbing'); 119 120 // Simulate a mouse drag move 121 mousemove(50, 0); 122 123 expect(handleDrag).toHaveBeenCalled(); 124 125 // Simulate a drag end 126 mouseup(60, 0); 127 expect(handleDragEnd).toHaveBeenCalled(); 128 }); 129 130 test('drag with minimum distance', () => { 131 const handleDrag = jest.fn(); 132 const handleDragEnd = jest.fn(); 133 134 zih.update([ 135 { 136 id: 'dragZone', 137 area: {x: 0, y: 0, width: 100, height: 100}, 138 drag: { 139 minDistance: 20, 140 onDrag: handleDrag, 141 onDragEnd: handleDragEnd, 142 }, 143 }, 144 ]); 145 146 // Simulate drag start 147 mousedown(10, 10); 148 149 // Move within the minimum distance 150 mousemove(15, 15); 151 expect(handleDrag).not.toHaveBeenCalled(); 152 153 // Move beyond the minimum distance 154 mousemove(40, 40); 155 expect(handleDrag).toHaveBeenCalled(); 156 157 // End the drag 158 mouseup(50, 50); 159 expect(handleDragEnd).toHaveBeenCalled(); 160 }); 161 162 test('onWheel', () => { 163 const handleWheel = jest.fn(); 164 165 zih.update([ 166 { 167 id: 'foo', 168 area: {x: 0, y: 0, width: 100, height: 100}, 169 onWheel: handleWheel, 170 }, 171 ]); 172 173 // Simulate a wheel event inside the zone 174 div.dispatchEvent( 175 new WheelEvent('wheel', { 176 bubbles: true, 177 clientX: 50, 178 clientY: 50, 179 deltaX: 5, 180 deltaY: 10, 181 }), 182 ); 183 184 expect(handleWheel).toHaveBeenCalled(); 185 expect(handleWheel.mock.calls[0][0]).toMatchObject({ 186 position: {x: 50, y: 50}, 187 deltaX: 5, 188 deltaY: 10, 189 }); 190 }); 191 192 test('key modifiers', () => { 193 const handleMouseClick = jest.fn(); 194 195 zih.update([ 196 { 197 id: 'modifierZone', 198 area: {x: 0, y: 0, width: 100, height: 100}, 199 keyModifier: 'shift', 200 onClick: handleMouseClick, 201 }, 202 ]); 203 204 // Attempt click without holding the modifier key 205 mousedown(50, 50); 206 mouseup(50, 50); 207 expect(handleMouseClick).not.toHaveBeenCalled(); 208 209 // Simulate holding down the shift key and clicking 210 document.dispatchEvent(new KeyboardEvent('keydown', {shiftKey: true})); 211 mousedown(50, 50); 212 mouseup(50, 50); 213 expect(handleMouseClick).toHaveBeenCalled(); 214 215 // Simulate releasing the shift key 216 document.dispatchEvent(new KeyboardEvent('keyup', {shiftKey: false})); 217 mousedown(50, 50); 218 mouseup(50, 50); 219 expect(handleMouseClick).toHaveBeenCalledTimes(1); // No additional call 220 }); 221 222 test('move zone during drag', () => { 223 const handleDrag = jest.fn(); 224 const handleDragEnd = jest.fn(); 225 226 zih.update([ 227 { 228 id: 'dragZone', 229 area: {x: 0, y: 0, width: 100, height: 100}, 230 drag: { 231 onDrag: handleDrag, 232 onDragEnd: handleDragEnd, 233 }, 234 }, 235 ]); 236 237 // Start a drag 238 mousedown(10, 10); 239 240 // Update zones while dragging 241 zih.update([ 242 { 243 id: 'dragZone', 244 area: {x: 0, y: 0, width: 10, height: 10}, 245 drag: { 246 onDrag: handleDrag, 247 onDragEnd: handleDragEnd, 248 }, 249 }, 250 ]); 251 252 // Continue dragging - drags are sticky, so even if we drag outside of the 253 // zone, the drag persists 254 mousemove(50, 50); 255 expect(handleDrag).toHaveBeenCalled(); 256 257 // End drag 258 mouseup(60, 60); 259 expect(handleDragEnd).toHaveBeenCalled(); 260 }); 261 262 test('click and move but stay in zone', () => { 263 const handleMouseClick = jest.fn(() => {}); 264 265 zih.update([ 266 { 267 id: 'foo', 268 area: {x: 0, y: 0, width: 60, height: 60}, 269 onClick: handleMouseClick, 270 }, 271 ]); 272 273 // Simulate a mouse click where the cursor has moved a little by remains 274 // inside the zone with the click event handler. 275 mousedown(30, 30); 276 mouseup(50, 50); 277 278 expect(handleMouseClick).toHaveBeenCalled(); 279 }); 280 281 test('click and move out of zone', () => { 282 const handleMouseClick = jest.fn(() => {}); 283 284 zih.update([ 285 { 286 id: 'foo', 287 area: {x: 0, y: 0, width: 60, height: 60}, 288 onClick: handleMouseClick, 289 }, 290 ]); 291 292 // Simulate a mouse click where the cursor has moved outside of the zone. 293 mousedown(50, 50); 294 mouseup(80, 80); 295 296 expect(handleMouseClick).not.toHaveBeenCalled(); 297 }); 298 299 afterEach(() => { 300 document.body.removeChild(div); 301 }); 302}); 303