/** * TASK-0135 — dnd-kit drag helper for the editor (final-flow step 5). * * Why a manual mouse drag instead of `locator.dragTo()`: * * - The editor uses dnd-kit `PointerSensor` with `activationConstraint: * { distance: 6 }`. Playwright's `dragTo()` synthesizes the pointer events * too quickly/uniformly and, in this app, does NOT reliably activate the * sensor's drop (no `POST /blocks/:id/move` request fires). A manual * `page.mouse` drag with stepped moves DOES activate and drop (verified * live against the dev stack). * - The per-row drag handle sits in a left gutter that is `opacity-0` + * `pointer-events-none` until the ROW is hovered (the `group`/ * `group-hover:` utilities in page-view.tsx). Hovering the grip directly * fails Playwright's hit-test (the `
` scroll container intercepts) — * you MUST hover the row first so the gutter becomes interactive. * * Server contract (verified): `POST /blocks/:id/move` accepts * `afterBlockId` (insert BELOW that block) and `beforeBlockId` (insert ABOVE * that block); the server computes the new base-62 position. */ import type { Page, Locator } from "@playwright/test"; /** * Drag the block whose row has `data-testid="block-"` so it lands * just ABOVE the block `data-testid="block-"` (i.e. insert before * the target). * * @param sourceId the API block id being moved. * @param targetId the API block id the source should end up immediately above. * @param settleMs wait for the /move round-trip + react-query refetch. */ export async function dragBlockAbove( page: Page, sourceId: string, targetId: string, settleMs = 1500, ): Promise { const sourceRow: Locator = page.getByTestId(`block-${sourceId}`); const targetRow: Locator = page.getByTestId(`block-${targetId}`); await expectVisible(sourceRow); await expectVisible(targetRow); // 1) Hover the SOURCE row → enables the gutter (opacity-0 → 1, // pointer-events-none → auto) so the grip is hit-testable. await sourceRow.hover(); const grip = sourceRow.getByTestId("drag-handle").first(); await grip.hover(); // 2) Manual mouse drag (dnd-kit PointerSensor does not fire on dragTo). const g = await grip.boundingBox(); const t = await targetRow.boundingBox(); if (!g || !t) throw new Error("drag: bounding boxes missing"); const sx = g.x + g.width / 2; const sy = g.y + g.height / 2; // Aim at the TARGET row's CENTER (not its top edge): dnd-kit `closestCenter` // then unambiguously resolves `over` to the target row, so `onDragEnd`'s // `arrayMove(sorted, idx(source), idx(target))` places the source exactly at // the target's index (target shifts down) — the deterministic "insert at // target slot" the spec asserts. A top-edge drop is ambiguous (the pointer // can instead match the row ABOVE the target). const ex = t.x + Math.min(80, t.width / 4); const ey = t.y + t.height / 2; await page.mouse.move(sx, sy); await page.waitForTimeout(80); await page.mouse.down(); const steps = 16; for (let i = 1; i <= steps; i++) { await page.mouse.move(sx + ((ex - sx) * i) / steps, sy + ((ey - sy) * i) / steps); await page.waitForTimeout(30); } await page.waitForTimeout(120); await page.mouse.up(); // 3) Let the /blocks/:id/move round-trip + react-query invalidation settle. await page.waitForTimeout(settleMs); } async function expectVisible(locator: Locator): Promise { const { expect } = await import("@playwright/test"); await expect(locator).toBeVisible({ timeout: 15_000 }); }