1/** 2 * TASK-0135 — dnd-kit drag helper for the editor (final-flow step 5). 3 * 4 * Why a manual mouse drag instead of `locator.dragTo()`: 5 * 6 * - The editor uses dnd-kit `PointerSensor` with `activationConstraint: 7 * { distance: 6 }`. Playwright's `dragTo()` synthesizes the pointer events 8 * too quickly/uniformly and, in this app, does NOT reliably activate the 9 * sensor's drop (no `POST /blocks/:id/move` request fires). A manual 10 * `page.mouse` drag with stepped moves DOES activate and drop (verified 11 * live against the dev stack). 12 * - The per-row drag handle sits in a left gutter that is `opacity-0` + 13 * `pointer-events-none` until the ROW is hovered (the `group`/ 14 * `group-hover:` utilities in page-view.tsx). Hovering the grip directly 15 * fails Playwright's hit-test (the `<main>` scroll container intercepts) — 16 * you MUST hover the row first so the gutter becomes interactive. 17 * 18 * Server contract (verified): `POST /blocks/:id/move` accepts 19 * `afterBlockId` (insert BELOW that block) and `beforeBlockId` (insert ABOVE 20 * that block); the server computes the new base-62 position. 21 */ 22import type { Page, Locator } from "@playwright/test"; 23 24/** 25 * Drag the block whose row has `data-testid="block-<sourceId>"` so it lands 26 * just ABOVE the block `data-testid="block-<targetId>"` (i.e. insert before 27 * the target). 28 * 29 * @param sourceId the API block id being moved. 30 * @param targetId the API block id the source should end up immediately above. 31 * @param settleMs wait for the /move round-trip + react-query refetch. 32 */ 33export async function dragBlockAbove( 34 page: Page, 35 sourceId: string, 36 targetId: string, 37 settleMs = 1500, 38): Promise<void> { 39 const sourceRow: Locator = page.getByTestId(`block-${sourceId}`); 40 const targetRow: Locator = page.getByTestId(`block-${targetId}`); 41 await expectVisible(sourceRow); 42 await expectVisible(targetRow); 43 44 // 1) Hover the SOURCE row → enables the gutter (opacity-0 → 1, 45 // pointer-events-none → auto) so the grip is hit-testable. 46 await sourceRow.hover(); 47 const grip = sourceRow.getByTestId("drag-handle").first(); 48 await grip.hover(); 49 50 // 2) Manual mouse drag (dnd-kit PointerSensor does not fire on dragTo). 51 const g = await grip.boundingBox(); 52 const t = await targetRow.boundingBox(); 53 if (!g || !t) throw new Error("drag: bounding boxes missing"); 54 const sx = g.x + g.width / 2; 55 const sy = g.y + g.height / 2; 56 // Aim at the TARGET row's CENTER (not its top edge): dnd-kit `closestCenter` 57 // then unambiguously resolves `over` to the target row, so `onDragEnd`'s 58 // `arrayMove(sorted, idx(source), idx(target))` places the source exactly at 59 // the target's index (target shifts down) — the deterministic "insert at 60 // target slot" the spec asserts. A top-edge drop is ambiguous (the pointer 61 // can instead match the row ABOVE the target). 62 const ex = t.x + Math.min(80, t.width / 4); 63 const ey = t.y + t.height / 2; 64 await page.mouse.move(sx, sy); 65 await page.waitForTimeout(80); 66 await page.mouse.down(); 67 const steps = 16; 68 for (let i = 1; i <= steps; i++) { 69 await page.mouse.move(sx + ((ex - sx) * i) / steps, sy + ((ey - sy) * i) / steps); 70 await page.waitForTimeout(30); 71 } 72 await page.waitForTimeout(120); 73 await page.mouse.up(); 74 // 3) Let the /blocks/:id/move round-trip + react-query invalidation settle. 75 await page.waitForTimeout(settleMs); 76} 77 78async function expectVisible(locator: Locator): Promise<void> { 79 const { expect } = await import("@playwright/test"); 80 await expect(locator).toBeVisible({ timeout: 15_000 }); 81} 82