|
| 1 | +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | +import { beforeEach, describe, expect, Mock, test, vi } from "vitest"; |
| 4 | + |
| 5 | +import { getLogicalClientX as originalGetLogicalClientX } from "@cloudscape-design/component-toolkit/internal"; |
| 6 | + |
| 7 | +import type { Transition } from "../../../../lib/components/internal/item-container"; |
| 8 | +import { determineHandleActiveState } from "../../../../lib/components/internal/item-container/utils"; |
| 9 | +import type { Operation } from "../../dnd-controller/controller"; |
| 10 | +import { Coordinates } from "../../utils/coordinates"; |
| 11 | +import { calculateInitialPointerData, DetermineHandleActiveStateArgs, getDndOperationType } from "../utils"; |
| 12 | + |
| 13 | +const mockRect = { |
| 14 | + insetInlineStart: 10, |
| 15 | + insetBlockStart: 20, |
| 16 | + insetInlineEnd: 110, // 10 + 100 (width) |
| 17 | + insetBlockEnd: 120, // 20 + 100 (height) |
| 18 | + inlineSize: 100, |
| 19 | + blockSize: 100, |
| 20 | + left: 10, |
| 21 | + right: 110, |
| 22 | + top: 20, |
| 23 | + bottom: 120, |
| 24 | + width: 100, |
| 25 | + height: 100, |
| 26 | + x: 10, |
| 27 | + y: 20, |
| 28 | +}; |
| 29 | + |
| 30 | +const mockPointerEvent = (clientX: number, clientY: number): Partial<PointerEvent> => ({ |
| 31 | + clientX, |
| 32 | + clientY, |
| 33 | +}); |
| 34 | + |
| 35 | +vi.mock("@cloudscape-design/component-toolkit/internal", async (importOriginal) => { |
| 36 | + const actual = (await importOriginal()) as any; |
| 37 | + return { |
| 38 | + ...actual, |
| 39 | + getLogicalClientX: vi.fn(), |
| 40 | + }; |
| 41 | +}); |
| 42 | +const mockGetLogicalClientX = originalGetLogicalClientX as Mock; |
| 43 | + |
| 44 | +describe("getDndOperationType", () => { |
| 45 | + interface TestCases { |
| 46 | + operation: "drag" | "resize"; |
| 47 | + isPlaced: boolean; |
| 48 | + expected: Operation; |
| 49 | + description: string; |
| 50 | + } |
| 51 | + |
| 52 | + const testCases: Array<TestCases> = [ |
| 53 | + { operation: "resize", isPlaced: true, expected: "resize", description: "resize when placed" }, |
| 54 | + { |
| 55 | + operation: "resize", |
| 56 | + isPlaced: false, |
| 57 | + expected: "resize", |
| 58 | + description: "resize when not placed (should still be resize)", |
| 59 | + }, |
| 60 | + { operation: "drag", isPlaced: true, expected: "reorder", description: "reorder when drag and placed" }, |
| 61 | + { operation: "drag", isPlaced: false, expected: "insert", description: "insert when drag and not placed" }, |
| 62 | + ]; |
| 63 | + |
| 64 | + test.each(testCases)('should return "$expected" for $description', ({ operation, isPlaced, expected }) => { |
| 65 | + expect(getDndOperationType(operation, isPlaced)).toBe<Operation>(expected); |
| 66 | + }); |
| 67 | +}); |
| 68 | + |
| 69 | +describe("calculateInitialPointerData", () => { |
| 70 | + const getMinSizeMock = vi.fn(); |
| 71 | + const MOCK_DOCUMENT_CLIENT_WIDTH = 1000; // For RTL simulation |
| 72 | + |
| 73 | + beforeEach(() => { |
| 74 | + getMinSizeMock.mockReset(); |
| 75 | + mockGetLogicalClientX.mockReset(); |
| 76 | + }); |
| 77 | + |
| 78 | + describe('when operation is "drag"', () => { |
| 79 | + test("should calculate pointerOffset from top-left and null boundaries for LTR", () => { |
| 80 | + mockGetLogicalClientX.mockImplementation((event: PointerEvent) => event.clientX); |
| 81 | + const event = mockPointerEvent(50, 60) as PointerEvent; |
| 82 | + const result = calculateInitialPointerData({ |
| 83 | + event, |
| 84 | + operation: "drag", |
| 85 | + rect: mockRect, |
| 86 | + getMinSize: getMinSizeMock, |
| 87 | + isRtl: false, |
| 88 | + }); |
| 89 | + expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, false); |
| 90 | + |
| 91 | + const expectedPointerOffsetX = event.clientX - mockRect.insetInlineStart; // 50 - 10 = 40 |
| 92 | + const expectedPointerOffsetY = event.clientY - mockRect.insetBlockStart; // 60 - 20 = 40 |
| 93 | + expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); |
| 94 | + expect(result.pointerBoundaries).toBeNull(); |
| 95 | + expect(getMinSizeMock).not.toHaveBeenCalled(); |
| 96 | + }); |
| 97 | + |
| 98 | + test("should calculate pointerOffset from top-left and null boundaries for RTL", () => { |
| 99 | + mockGetLogicalClientX.mockImplementation((event: PointerEvent) => MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX); |
| 100 | + const event = mockPointerEvent(950, 60) as PointerEvent; |
| 101 | + const result = calculateInitialPointerData({ |
| 102 | + event, |
| 103 | + operation: "drag", |
| 104 | + rect: mockRect, |
| 105 | + getMinSize: getMinSizeMock, |
| 106 | + isRtl: true, |
| 107 | + }); |
| 108 | + expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, true); |
| 109 | + |
| 110 | + const logicalClientX = MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX; // 1000 - 950 = 50 |
| 111 | + const expectedPointerOffsetX = logicalClientX - mockRect.insetInlineStart; // 50 - 10 = 40 |
| 112 | + const expectedPointerOffsetY = event.clientY - mockRect.insetBlockStart; // 60 - 20 = 40 |
| 113 | + expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); |
| 114 | + expect(result.pointerBoundaries).toBeNull(); |
| 115 | + expect(getMinSizeMock).not.toHaveBeenCalled(); |
| 116 | + }); |
| 117 | + }); |
| 118 | + |
| 119 | + describe('when operation is "resize"', () => { |
| 120 | + const minWidth = 50; |
| 121 | + const minHeight = 50; |
| 122 | + |
| 123 | + beforeEach(() => { |
| 124 | + getMinSizeMock.mockReturnValue({ minWidth, minHeight }); |
| 125 | + }); |
| 126 | + |
| 127 | + test("should calculate pointerOffset from bottom-right and boundaries for LTR", () => { |
| 128 | + mockGetLogicalClientX.mockImplementation((event: PointerEvent) => event.clientX); |
| 129 | + const event = mockPointerEvent(150, 160) as PointerEvent; // Pointer beyond item |
| 130 | + const result = calculateInitialPointerData({ |
| 131 | + event, |
| 132 | + operation: "resize", |
| 133 | + rect: mockRect, |
| 134 | + getMinSize: getMinSizeMock, |
| 135 | + isRtl: false, |
| 136 | + }); |
| 137 | + |
| 138 | + expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, false); |
| 139 | + expect(getMinSizeMock).toHaveBeenCalledTimes(1); |
| 140 | + |
| 141 | + const expectedPointerOffsetX = event.clientX - mockRect.insetInlineEnd; // 150 - 110 = 40 |
| 142 | + const expectedPointerOffsetY = event.clientY - mockRect.insetBlockEnd; // 160 - 120 = 40 |
| 143 | + expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); |
| 144 | + |
| 145 | + const expectedBoundaryX = event.clientX - mockRect.inlineSize + minWidth; // 150 - 100 + 50 = 100 |
| 146 | + const expectedBoundaryY = event.clientY - mockRect.blockSize + minHeight; // 160 - 100 + 50 = 110 |
| 147 | + expect(result.pointerBoundaries).toEqual(new Coordinates({ x: expectedBoundaryX, y: expectedBoundaryY })); |
| 148 | + }); |
| 149 | + |
| 150 | + test("should calculate pointerOffset from bottom-right and boundaries for RTL", () => { |
| 151 | + mockGetLogicalClientX.mockImplementation((event: PointerEvent) => MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX); |
| 152 | + const event = mockPointerEvent(850, 160) as PointerEvent; |
| 153 | + const result = calculateInitialPointerData({ |
| 154 | + event, |
| 155 | + operation: "resize", |
| 156 | + rect: mockRect, |
| 157 | + getMinSize: getMinSizeMock, |
| 158 | + isRtl: true, |
| 159 | + }); |
| 160 | + |
| 161 | + expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, true); |
| 162 | + expect(getMinSizeMock).toHaveBeenCalledTimes(1); |
| 163 | + |
| 164 | + const logicalClientX = MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX; // 1000 - 850 = 150 |
| 165 | + const expectedPointerOffsetX = logicalClientX - mockRect.insetInlineEnd; // 150 - 110 = 40 |
| 166 | + const expectedPointerOffsetY = event.clientY - mockRect.insetBlockEnd; // 160 - 120 = 40 |
| 167 | + expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); |
| 168 | + |
| 169 | + const expectedBoundaryX = logicalClientX - mockRect.inlineSize + minWidth; // 150 - 100 + 50 = 100 |
| 170 | + const expectedBoundaryY = event.clientY - mockRect.blockSize + minHeight; // 160 - 100 + 50 = 110 |
| 171 | + expect(result.pointerBoundaries).toEqual(new Coordinates({ x: expectedBoundaryX, y: expectedBoundaryY })); |
| 172 | + }); |
| 173 | + }); |
| 174 | +}); |
| 175 | + |
| 176 | +describe("determineHandleActiveState", () => { |
| 177 | + const mockTransition = (operation: Operation): Transition => ({ |
| 178 | + itemId: "test-item", |
| 179 | + operation: operation, |
| 180 | + interactionType: "pointer", // Default value while testing, doesn't affect function's logic |
| 181 | + sizeTransform: null, |
| 182 | + positionTransform: null, |
| 183 | + }); |
| 184 | + |
| 185 | + type InteractionHookValue = DetermineHandleActiveStateArgs["interactionHookValue"]; |
| 186 | + |
| 187 | + const activeStateTestCases: Array<{ |
| 188 | + description: string; |
| 189 | + args: Partial<DetermineHandleActiveStateArgs>; |
| 190 | + expected: "pointer" | "uap" | null; |
| 191 | + targetOperation?: Operation; |
| 192 | + }> = [ |
| 193 | + // "pointer" states |
| 194 | + { |
| 195 | + description: 'return "pointer" if globally active, resize transition, dnd-start', |
| 196 | + args: { |
| 197 | + isHandleActive: true, |
| 198 | + currentTransition: mockTransition("resize"), |
| 199 | + interactionHookValue: "dnd-start", |
| 200 | + targetOperation: "resize", |
| 201 | + }, |
| 202 | + expected: "pointer", |
| 203 | + }, |
| 204 | + { |
| 205 | + description: 'return "pointer" if globally active, reorder transition, dnd-start', |
| 206 | + args: { |
| 207 | + isHandleActive: true, |
| 208 | + currentTransition: mockTransition("reorder"), |
| 209 | + interactionHookValue: "dnd-start", |
| 210 | + targetOperation: "reorder", |
| 211 | + }, |
| 212 | + expected: "pointer", |
| 213 | + }, |
| 214 | + // "uap" states |
| 215 | + { |
| 216 | + description: 'return "uap" if globally active, resize transition, uap-action-start', |
| 217 | + args: { |
| 218 | + isHandleActive: true, |
| 219 | + currentTransition: mockTransition("resize"), |
| 220 | + interactionHookValue: "uap-action-start", |
| 221 | + targetOperation: "resize", |
| 222 | + }, |
| 223 | + expected: "uap", |
| 224 | + }, |
| 225 | + { |
| 226 | + description: 'return "uap" if globally active, reorder transition, uap-action-start', |
| 227 | + args: { |
| 228 | + isHandleActive: true, |
| 229 | + currentTransition: mockTransition("reorder"), |
| 230 | + interactionHookValue: "uap-action-start", |
| 231 | + targetOperation: "reorder", |
| 232 | + }, |
| 233 | + expected: "uap", |
| 234 | + }, |
| 235 | + // Null states |
| 236 | + { |
| 237 | + description: "return null if not globally active", |
| 238 | + args: { |
| 239 | + isHandleActive: false, |
| 240 | + currentTransition: mockTransition("resize"), |
| 241 | + interactionHookValue: "dnd-start", |
| 242 | + targetOperation: "resize", |
| 243 | + }, |
| 244 | + expected: null, |
| 245 | + }, |
| 246 | + { |
| 247 | + description: "return null if no current transition", |
| 248 | + args: { |
| 249 | + isHandleActive: true, |
| 250 | + currentTransition: null, |
| 251 | + interactionHookValue: "dnd-start", |
| 252 | + targetOperation: "resize", |
| 253 | + }, |
| 254 | + expected: null, |
| 255 | + }, |
| 256 | + { |
| 257 | + description: "return null if current transition operation mismatches target", |
| 258 | + args: { |
| 259 | + isHandleActive: true, |
| 260 | + currentTransition: mockTransition("reorder"), |
| 261 | + interactionHookValue: "dnd-start", |
| 262 | + targetOperation: "resize", |
| 263 | + }, |
| 264 | + expected: null, |
| 265 | + }, |
| 266 | + { |
| 267 | + description: 'return null if interaction hook is not "dnd-start" or "uap-action-start" (e.g., "dnd-active")', |
| 268 | + args: { |
| 269 | + isHandleActive: true, |
| 270 | + currentTransition: mockTransition("resize"), |
| 271 | + interactionHookValue: "dnd-active", |
| 272 | + targetOperation: "resize", |
| 273 | + }, |
| 274 | + expected: null, |
| 275 | + }, |
| 276 | + { |
| 277 | + description: "return null if interaction hook is null", |
| 278 | + args: { |
| 279 | + isHandleActive: true, |
| 280 | + currentTransition: mockTransition("resize"), |
| 281 | + interactionHookValue: null, |
| 282 | + targetOperation: "resize", |
| 283 | + }, |
| 284 | + expected: null, |
| 285 | + }, |
| 286 | + { |
| 287 | + description: "return null if interaction hook is undefined", |
| 288 | + args: { |
| 289 | + isHandleActive: true, |
| 290 | + currentTransition: mockTransition("resize"), |
| 291 | + interactionHookValue: undefined as unknown as InteractionHookValue, |
| 292 | + targetOperation: "resize", |
| 293 | + }, |
| 294 | + expected: null, |
| 295 | + }, |
| 296 | + { |
| 297 | + description: "return null if interaction hook is an arbitrary string", |
| 298 | + args: { |
| 299 | + isHandleActive: true, |
| 300 | + currentTransition: mockTransition("resize"), |
| 301 | + interactionHookValue: "some-other-state" as InteractionHookValue, |
| 302 | + targetOperation: "resize", |
| 303 | + }, |
| 304 | + expected: null, |
| 305 | + }, |
| 306 | + // Combined null conditions |
| 307 | + { |
| 308 | + description: 'return null if not globally active, even if other conditions match for "pointer"', |
| 309 | + args: { |
| 310 | + isHandleActive: false, |
| 311 | + currentTransition: mockTransition("resize"), |
| 312 | + interactionHookValue: "dnd-start", |
| 313 | + targetOperation: "resize", |
| 314 | + }, |
| 315 | + expected: null, |
| 316 | + }, |
| 317 | + { |
| 318 | + description: 'return null if not globally active, even if other conditions match for "uap"', |
| 319 | + args: { |
| 320 | + isHandleActive: false, |
| 321 | + currentTransition: mockTransition("resize"), |
| 322 | + interactionHookValue: "uap-action-start", |
| 323 | + targetOperation: "resize", |
| 324 | + }, |
| 325 | + expected: null, |
| 326 | + }, |
| 327 | + ]; |
| 328 | + |
| 329 | + test.each(activeStateTestCases)("should $description", ({ args, expected }) => { |
| 330 | + expect(determineHandleActiveState(args as DetermineHandleActiveStateArgs)).toBe(expected); |
| 331 | + }); |
| 332 | +}); |
0 commit comments