Prevent cancelled drags from committing moves
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s

This commit is contained in:
Shinuwa 2026-09-13 08:37:16 +02:00
parent e6e08b458c
commit 6bcde57e88
6 changed files with 148 additions and 12 deletions

92
e2e/form-reorder.spec.js Normal file
View file

@ -0,0 +1,92 @@
// Rôle : vérifie les brouillons de formulaire et le cycle de déplacement par pointeur sans assertions de code source.
/* global window, document */
import { test, expect } from "@playwright/test";
import { fileURLToPath, URL } from "node:url";
const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/form-reorder-probe.js", import.meta.url))}`;
async function setup(page, kind) {
await page.goto("/");
await page.evaluate(async ({ url, kind }) => {
const { mountFormReorderProbe } = await import(url);
document.getElementById("app").style.display = "none";
const element = document.createElement("div");
document.body.prepend(element);
window.interactionProbe = mountFormReorderProbe(element, kind);
}, { url: probeUrl, kind });
}
test("draft forms retain edits, report validation, prevent navigation and reset after success", async ({ page }) => {
await setup(page, "form");
const title = page.getByRole("textbox", { name: "Title", exact: true });
const detail = page.getByRole("textbox", { name: "Detail", exact: true });
await title.fill("");
await page.getByRole("button", { name: "Submit", exact: true }).click();
await expect(page.getByRole("alert")).toHaveText("Required");
await title.fill("Draft");
await expect(page.getByRole("alert")).toBeEmpty();
await page.evaluate(() => window.interactionProbe.update(1));
await expect(title).toHaveValue("Draft");
await expect(detail).toHaveValue("Kept");
await page.getByRole("button", { name: "Patch", exact: true }).click();
await expect(title).toHaveValue("Patched");
await expect(detail).toHaveValue("Kept");
await page.getByRole("button", { name: "Replace", exact: true }).click();
await expect(title).toHaveValue("Replacement");
await expect(detail).toHaveValue("New");
await title.press("Enter");
await expect(title).toHaveValue("Initial");
await expect(detail).toHaveValue("Kept");
expect(await page.evaluate(() => window.interactionProbe.events)).toEqual([
"changed", { title: "", detail: "Kept" }, { prevented: true },
"changed", { title: "Replacement", detail: "New" }, { prevented: true }
]);
});
async function start(page) {
await page.getByRole("button", { name: "a", exact: true }).hover();
await page.mouse.down();
await expect(page.getByLabel("Dragging")).toHaveText("a");
}
test("pointer drops resolve before and after using the latest callback and reject invalid targets", async ({ page }) => {
await setup(page, "reorder");
await page.getByRole("button", { name: "a", exact: true }).hover();
await page.mouse.down({ button: "right" });
await expect(page.getByLabel("Dragging")).toBeEmpty();
await page.mouse.up({ button: "right" });
const target = await page.locator('[data-probe-item="b"]').boundingBox();
for (const [offset, placement] of [[10, "before"], [90, "after"]]) {
await start(page);
await page.evaluate(() => window.interactionProbe.update(1));
await page.mouse.move(target.x + 200, target.y + offset);
await page.mouse.up();
await expect(page.getByLabel("Dragging")).toBeEmpty();
expect(await page.evaluate(() => window.interactionProbe.events.at(-1))).toEqual({ source: "a", target: "b", placement, revision: 1 });
}
await start(page);
await page.locator('[data-probe-item="blocked"]').hover();
await page.mouse.up();
await start(page);
await page.mouse.up();
expect(await page.evaluate(() => window.interactionProbe.events.length)).toBe(2);
});
test("pointer cancellation never commits a drop and unmount removes active listeners", async ({ page }) => {
await setup(page, "reorder");
const target = await page.locator('[data-probe-item="b"]').boundingBox();
await start(page);
await page.mouse.move(target.x + 200, target.y + 50);
await page.getByRole("button", { name: "a", exact: true }).dispatchEvent("pointercancel", { clientX: target.x + 200, clientY: target.y + 50, pointerId: 1 });
await expect(page.getByLabel("Dragging")).toBeEmpty();
await page.mouse.up();
expect(await page.evaluate(() => window.interactionProbe.events)).toEqual([]);
await start(page);
await page.evaluate(() => {
const remainingTarget = document.querySelector('[data-probe-item="b"]').cloneNode(true);
window.interactionProbe.unmount();
document.body.prepend(remainingTarget);
});
await page.locator('[data-probe-item="b"]').hover();
await page.mouse.up();
expect(await page.evaluate(() => window.interactionProbe.events)).toEqual([]);
});