/** * Scenario: database-view-calendar-reschedule * * Verifies that dragging an event on the FullCalendar to a new date: * 1. Triggers the useUpdateRow mutation (PATCH to bridge via onEventDrop). * 2. The event appears on the new date. * 3. A page reload confirms the new date persists (Baserow updated). * * FullCalendar event drag: FullCalendar uses its own drag implementation on * top of the interaction plugin. The rendered events have a title attribute * and can be dragged via mouse simulation. We: * 1. Locate the event element by its title text. * 2. Get its bounding box. * 3. Locate the target date cell. * 4. Simulate mouse drag from event to target date cell. * * The test targets "Task Alpha" which is seeded to today+1. We move it to today+8. */ 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"); /** * Format a date as "YYYY-MM-DD" (ISO 8601 date only). */ function isoDate(date: Date): string { return date.toISOString().slice(0, 10); } /** * Add days to a date and return a new Date. */ function addDays(date: Date, days: number): Date { const result = new Date(date); result.setDate(result.getDate() + days); return result; } test.describe("database-view calendar reschedule", () => { 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 calendar event to new date persists after reload", async ({ page, request }) => { await page.goto(BASE_URL); // Wait for the calendar renderer. const calendarWrapper = page .getByTestId("calendar-renderer") .or(page.locator("[data-node-type='database-view'] .fc")) .first(); const calendarVisible = await calendarWrapper .isVisible({ timeout: 5_000 }) .catch(() => false); if (!calendarVisible) { test.skip( true, "No calendar database-view page found. Create one with viewType=calendar first.", ); return; } // Wait for FullCalendar to fully render (events loaded). await calendarWrapper.waitFor({ state: "visible", timeout: 15_000 }); // Locate the "Task Alpha" event on the calendar. const eventEl = calendarWrapper .locator(".fc-event") .filter({ hasText: "Task Alpha" }) .first(); await eventEl.waitFor({ state: "visible", timeout: 10_000 }); // Determine the target date: today + 8 days (far enough from today+1 to // land on a different date cell in month view). const now = new Date(); const targetDate = addDays(now, 8); const targetDateStr = isoDate(targetDate); // FullCalendar renders day cells with data-date attributes in month view. const targetCell = calendarWrapper .locator(`[data-date="${targetDateStr}"]`) .first(); const targetVisible = await targetCell .isVisible({ timeout: 5_000 }) .catch(() => false); if (!targetVisible) { // Target date is outside the current month view — navigate forward. const nextBtn = calendarWrapper .locator(".fc-next-button, button[aria-label*='next']") .first(); if (await nextBtn.isVisible()) { await nextBtn.click(); await page.waitForTimeout(500); } const targetCellAfterNav = calendarWrapper .locator(`[data-date="${targetDateStr}"]`) .first(); const visibleAfterNav = await targetCellAfterNav .isVisible({ timeout: 5_000 }) .catch(() => false); if (!visibleAfterNav) { test.skip( true, `Target date cell ${targetDateStr} not found in current or next month view.`, ); return; } } // Get bounding boxes. const eventBox = await eventEl.boundingBox(); const targetBox = await targetCell.boundingBox(); if (!eventBox || !targetBox) { throw new Error("Could not get bounding boxes for calendar drag."); } const startX = eventBox.x + eventBox.width / 2; const startY = eventBox.y + eventBox.height / 2; const endX = targetBox.x + targetBox.width / 2; const endY = targetBox.y + targetBox.height / 2; // Simulate drag — FullCalendar's interaction plugin listens on mousedown/move/up. await page.mouse.move(startX, startY); await page.mouse.down(); // Move gradually to avoid snap-back. const steps = 25; for (let i = 1; i <= steps; i++) { await page.mouse.move( startX + ((endX - startX) * i) / steps, startY + ((endY - startY) * i) / steps, ); await page.waitForTimeout(15); } await page.mouse.up(); // Wait for the event to appear on the target date. await expect( targetCell.locator(".fc-event").filter({ hasText: "Task Alpha" }), ).toBeVisible({ timeout: 15_000 }); // Reload to confirm persistence. await page.reload(); await calendarWrapper.waitFor({ state: "visible", timeout: 20_000 }); // Re-locate target cell after reload. const targetCellAfterReload = calendarWrapper .locator(`[data-date="${targetDateStr}"]`) .first(); // Navigate forward if needed. const targetVisibleAfterReload = await targetCellAfterReload .isVisible({ timeout: 3_000 }) .catch(() => false); if (!targetVisibleAfterReload) { const nextBtn = calendarWrapper .locator(".fc-next-button, button[aria-label*='next']") .first(); if (await nextBtn.isVisible()) await nextBtn.click(); await page.waitForTimeout(500); } await expect( calendarWrapper .locator(`[data-date="${targetDateStr}"] .fc-event`) .filter({ hasText: "Task Alpha" }), ).toBeVisible({ timeout: 15_000 }); }, ); test.afterAll(async ({ request }) => { // Restore Task Alpha's date to today+1. if (!seed?.rowIds[0]) return; const tomorrow = addDays(new Date(), 1); await updateRowViaBaserowApi(request, seed.token, seed.tableId, seed.rowIds[0], { [seed.dateFieldName]: isoDate(tomorrow), }); }); });