import { describe, it, expect, vi, beforeEach } from "vitest"; import { pw_drag_drop } from "./pw_drag_drop"; import type { Locator, Page } from "playwright"; // --- Mock factories --- function makeLocator(bbox: { x: number; y: number; width: number; height: number } | null): Locator { return { boundingBox: vi.fn().mockResolvedValue(bbox), } as unknown as Locator; } function makeFirstLocator(bbox: { x: number; y: number; width: number; height: number } | null): Locator { const loc = makeLocator(bbox); return loc; } function makePage( sourceBbox: { x: number; y: number; width: number; height: number } | null, targetBbox: { x: number; y: number; width: number; height: number } | null, ): Page { const moveStub = vi.fn().mockResolvedValue(undefined); const downStub = vi.fn().mockResolvedValue(undefined); const upStub = vi.fn().mockResolvedValue(undefined); const waitStub = vi.fn().mockResolvedValue(undefined); // locator() returns an object with .first() that returns the locator mock const locatorFactory = (bbox: typeof sourceBbox) => { const inner = makeFirstLocator(bbox); return { first: vi.fn().mockReturnValue(inner) }; }; let callCount = 0; const locatorMock = vi.fn().mockImplementation(() => { callCount++; if (callCount === 1) return locatorFactory(sourceBbox); return locatorFactory(targetBbox); }); return { mouse: { move: moveStub, down: downStub, up: upStub, }, locator: locatorMock, waitForTimeout: waitStub, } as unknown as Page; } // --- Tests --- describe("pw_drag_drop", () => { describe("happy path", () => { it("calls mouse.down once, mouse.up once, mouse.move at least steps+2 times, waitForTimeout steps times when hoverMs=0", async () => { const steps = 5; const sourceBbox = { x: 10, y: 20, width: 100, height: 50 }; const targetBbox = { x: 300, y: 20, width: 100, height: 50 }; const page = makePage(sourceBbox, targetBbox); const sourceLocator = makeLocator(sourceBbox); const targetLocator = makeLocator(targetBbox); await pw_drag_drop(page, { source: sourceLocator, target: targetLocator, steps, delayMs: 0, hoverMs: 0, }); expect((page.mouse.down as ReturnType).mock.calls.length).toBe(1); expect((page.mouse.up as ReturnType).mock.calls.length).toBe(1); // move(sx,sy) + move(activate, {steps:2}) + steps moves = at least steps + 2 calls const moveCalls = (page.mouse.move as ReturnType).mock.calls.length; expect(moveCalls).toBeGreaterThanOrEqual(steps + 2); // waitForTimeout called exactly `steps` times (no hoverMs) const waitCalls = (page.waitForTimeout as ReturnType).mock.calls.length; expect(waitCalls).toBe(steps); }); }); describe("no source bbox throws", () => { it("throws when source.boundingBox returns null", async () => { const page = makePage(null, { x: 300, y: 20, width: 100, height: 50 }); const sourceLocator = makeLocator(null); const targetLocator = makeLocator({ x: 300, y: 20, width: 100, height: 50 }); await expect( pw_drag_drop(page, { source: sourceLocator, target: targetLocator }), ).rejects.toThrow("source element has no bounding box"); }); }); describe("no target bbox throws", () => { it("throws when target.boundingBox returns null", async () => { const page = makePage({ x: 10, y: 20, width: 100, height: 50 }, null); const sourceLocator = makeLocator({ x: 10, y: 20, width: 100, height: 50 }); const targetLocator = makeLocator(null); await expect( pw_drag_drop(page, { source: sourceLocator, target: targetLocator }), ).rejects.toThrow("target element has no bounding box"); }); }); describe("hoverMs adds wait", () => { it("calls waitForTimeout steps+1 times when hoverMs>0", async () => { const steps = 4; const sourceBbox = { x: 10, y: 20, width: 100, height: 50 }; const targetBbox = { x: 300, y: 20, width: 100, height: 50 }; const page = makePage(sourceBbox, targetBbox); const sourceLocator = makeLocator(sourceBbox); const targetLocator = makeLocator(targetBbox); await pw_drag_drop(page, { source: sourceLocator, target: targetLocator, steps, delayMs: 0, hoverMs: 400, }); const waitCalls = (page.waitForTimeout as ReturnType).mock.calls.length; // steps waits for delayMs + 1 wait for hoverMs expect(waitCalls).toBe(steps + 1); }); }); describe("string selectors resolve", () => { it("calls page.locator(selector).first() when source/target are strings", async () => { const sourceBbox = { x: 10, y: 20, width: 100, height: 50 }; const targetBbox = { x: 300, y: 20, width: 100, height: 50 }; const page = makePage(sourceBbox, targetBbox); await pw_drag_drop(page, { source: "[data-card-id='abc']", target: "[data-column-id='xyz']", steps: 2, delayMs: 0, }); const locatorCalls = (page.locator as ReturnType).mock.calls; expect(locatorCalls.length).toBe(2); expect(locatorCalls[0]?.[0]).toBe("[data-card-id='abc']"); expect(locatorCalls[1]?.[0]).toBe("[data-column-id='xyz']"); }); }); });