Wiki/e2e/tests/database-view-kanban-drag.spec.ts
Corentin JOGUET e9695450ef
Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
E2E Playwright / Playwright e2e (chromium) (push) Waiting to run
test(e2e): add Playwright cross-stack tests for R3.1.e database-view
7 scenarios covering the full bridge+DocAdenice+Baserow chain:
auth login, database-view insert, inline edit persistence, SSE realtime
update (no reload), RBAC write-denied, kanban drag-drop, calendar reschedule.

Includes docker-compose.e2e.yml (Postgres+Redis+Baserow+bridge+DocAdenice),
playwright.config.ts (3 projects: chromium/firefox/webkit), auth+baserow+cleanup
fixtures, global setup (API login + Baserow seed), and GitHub Actions e2e.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 00:37:23 +02:00

148 lines
5 KiB
TypeScript

/**
* Scenario: database-view-kanban-drag
*
* Verifies that dragging a kanban card from one column to another:
* 1. Triggers the useUpdateRow mutation (PATCH to bridge).
* 2. The card appears in the target column.
* 3. A page reload confirms the new column assignment persists (Baserow updated).
*
* @dnd-kit drag simulation: Playwright cannot simulate native HTML5 drag-drop.
* @dnd-kit uses PointerSensor (pointer events). We simulate this by:
* 1. pointerdown on the card.
* 2. pointermove to the target column center (5px+ to activate the sensor).
* 3. pointerup to drop.
*
* The activation constraint in KanbanRenderer is `distance: 5` — so we move
* at least 10px before releasing.
*/
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
import type { BaserowSeed } from "../fixtures/baserow";
import { updateRowViaBaserowApi } from "../fixtures/baserow";
const BASE_URL = process.env.E2E_DOCMOST_URL ?? "http://localhost:5173";
const SEED_FILE = path.resolve(__dirname, "../.auth/baserow-seed.json");
test.describe("database-view kanban drag", () => {
let seed: BaserowSeed;
test.beforeAll(() => {
if (!fs.existsSync(SEED_FILE)) {
throw new Error(`Seed file not found at ${SEED_FILE}.`);
}
seed = JSON.parse(fs.readFileSync(SEED_FILE, "utf-8")) as BaserowSeed;
});
test(
"drag card from Todo column to Done column persists after reload",
async ({ page, request }) => {
await page.goto(BASE_URL);
// Look for the kanban board — it may require navigating to a page that has
// a kanban database-view node. Depends on insert spec or a pre-created page.
const kanbanBoard = page
.getByTestId("kanban-board")
.or(page.locator("[data-node-type='database-view'] [class*='board']"))
.first();
const boardVisible = await kanbanBoard.isVisible({ timeout: 5_000 }).catch(() => false);
if (!boardVisible) {
test.skip(
true,
"No kanban database-view page found. Create one with viewType=kanban first.",
);
return;
}
// Locate the "Todo" column and the "Done" column.
const todoColumn = page
.getByTestId("kanban-column-Todo")
.or(page.locator("[class*='column']").filter({ hasText: "Todo" }))
.first();
const doneColumn = page
.getByTestId("kanban-column-Done")
.or(page.locator("[class*='column']").filter({ hasText: "Done" }))
.first();
await todoColumn.waitFor({ state: "visible", timeout: 10_000 });
await doneColumn.waitFor({ state: "visible", timeout: 10_000 });
// Find the card for "Task Alpha" (Status: Todo) in the Todo column.
const card = todoColumn
.getByTestId(`kanban-card-${seed.rowIds[0]}`)
.or(todoColumn.getByText("Task Alpha"))
.first();
await card.waitFor({ state: "visible", timeout: 10_000 });
// Perform pointer-based drag to "Done" column.
const cardBox = await card.boundingBox();
const doneBox = await doneColumn.boundingBox();
if (!cardBox || !doneBox) {
throw new Error("Could not get bounding boxes for drag simulation.");
}
const startX = cardBox.x + cardBox.width / 2;
const startY = cardBox.y + cardBox.height / 2;
const endX = doneBox.x + doneBox.width / 2;
const endY = doneBox.y + doneBox.height / 2;
// Simulate @dnd-kit PointerSensor drag.
await page.mouse.move(startX, startY);
await page.mouse.down();
// Move gradually to exceed the 5px activation threshold.
const steps = 20;
for (let i = 1; i <= steps; i++) {
await page.mouse.move(
startX + ((endX - startX) * i) / steps,
startY + ((endY - startY) * i) / steps,
);
// Small delay between moves to let the pointer sensor detect.
await page.waitForTimeout(20);
}
await page.mouse.up();
// The card must now appear in the "Done" column.
await expect(doneColumn.getByText("Task Alpha")).toBeVisible({
timeout: 15_000,
});
// The "Todo" column must no longer contain "Task Alpha".
await expect(todoColumn.getByText("Task Alpha")).toBeHidden({
timeout: 5_000,
});
// Reload to confirm persistence.
await page.reload();
// Wait for kanban board to re-render.
await kanbanBoard.waitFor({ state: "visible", timeout: 20_000 });
// "Task Alpha" must still be in "Done" after reload.
const doneColumnAfterReload = page
.getByTestId("kanban-column-Done")
.or(page.locator("[class*='column']").filter({ hasText: "Done" }))
.first();
await expect(doneColumnAfterReload.getByText("Task Alpha")).toBeVisible({
timeout: 15_000,
});
},
);
test.afterAll(async ({ request }) => {
// Restore Task Alpha to "Todo" status.
if (!seed?.rowIds[0]) return;
await updateRowViaBaserowApi(request, seed.token, seed.tableId, seed.rowIds[0], {
[seed.singleSelectFieldName]: "Todo",
});
});
});