import { expect, test, type Page } from "@playwright/test"; async function signInAsAdmin(page: Page) { await page.goto("/auth/signin"); await page.fill('input[type="email"]', "admin@capakraken.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); } test.describe("Timeline", () => { test.describe.configure({ mode: "serial" }); test.beforeEach(async ({ page }) => { await page.addInitScript(() => { localStorage.setItem("capakraken_theme", JSON.stringify({ mode: "dark" })); }); await signInAsAdmin(page); await page.goto("/timeline"); }); test("loads and displays the timeline", async ({ page }) => { await expect(page.locator("text=Resource view")).toBeVisible(); await expect(page.locator("text=Project view")).toBeVisible(); await expect(page.getByRole("button", { name: /All Clients|Clients:/ })).toBeVisible(); await expect(page.getByRole("button", { name: /All Chapters|Chapters:/ })).toBeVisible(); await expect(page.getByRole("button", { name: /All people|People:/ })).toBeVisible(); // Timeline canvas should be visible await expect(page.locator("div.app-surface.relative.flex-1.overflow-auto")).toBeVisible(); }); test("can switch between resource and project view", async ({ page }) => { await page.click("text=Project view"); await expect( page.locator("text=0 projects").or(page.locator("text=/\\d+ projects/")), ).toBeVisible(); await page.click("text=Resource view"); await expect(page.locator("text=/\\d+ resources/")).toBeVisible(); }); test("can navigate forward and back", async ({ page }) => { const todayBtn = page.locator("button", { hasText: "Today" }); await expect(todayBtn).toBeVisible(); await page.locator("button", { hasText: "›" }).click(); await page.locator("button", { hasText: "‹" }).click(); await todayBtn.click(); }); test("keeps timeline data populated after navigating from allocations", async ({ page }) => { await page.goto("/allocations"); await expect( page.locator("h1").filter({ hasText: /Allocations|Planning/i }), ).toBeVisible({ timeout: 10000 }); await page.locator('nav a >> text="Timeline"').first().click(); await expect(page).toHaveURL(/\/timeline/); await expect( page.locator(".app-toolbar").getByText(/[1-9]\d* resources · [1-9]\d* allocations/), ).toBeVisible({ timeout: 10000 }); await expect(page.getByTestId("timeline-resource-row-canvas").first()).toBeVisible({ timeout: 10000, }); await expect(page.locator("text=No allocations in this time range")).not.toBeVisible(); }); test("filter panel opens and closes", async ({ page }) => { await page.locator("button", { hasText: "Filter" }).click(); await expect(page.getByRole("heading", { name: "Filters" })).toBeVisible(); await expect(page.getByPlaceholder("Search projects…")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("heading", { name: "Filters" })).not.toBeVisible(); }); test("shows placeholder bars for unassigned allocations", async ({ page }) => { // Filter to show placeholders (enabled by default) // The timeline should have at least one dashed placeholder bar from seed data await page.waitForSelector(".overflow-auto", { state: "visible" }); // Check that the timeline loaded (resource rows or empty state visible) await expect( page.locator(".app-toolbar").getByText(/\d+ resources · \d+ allocations/), ).toBeVisible(); }); test("clicking a placeholder opens the fill placeholder modal", async ({ page }) => { // Wait for timeline to load await page.waitForSelector(".overflow-auto"); await page.waitForTimeout(1000); // let tRPC queries settle // Try to find and click a placeholder bar (dashed border style) const placeholderBar = page.locator("[style*='dashed']").first(); if ((await placeholderBar.count()) > 0) { await placeholderBar.click(); await expect( page.locator("text=Fill Placeholder").or(page.locator("text=Assign Resource")), ).toBeVisible(); await page.keyboard.press("Escape"); } }); test("resource and project views keep tooltips opaque in dark mode and support right click", async ({ page, }) => { await page.waitForSelector(".overflow-auto", { state: "visible" }); await page.waitForTimeout(1000); const heatmapTooltip = page .locator("div.fixed.pointer-events-none.rounded-xl.border.border-gray-800") .first(); const allocationPopoverField = page.getByText("Hours / day"); const resourceHoverTarget = page.getByTestId("timeline-resource-row-canvas").first(); const resourceHoverBox = await resourceHoverTarget.boundingBox(); expect(resourceHoverBox).not.toBeNull(); if (!resourceHoverBox) { throw new Error("Expected a resource timeline row canvas to be available"); } await page.mouse.move(resourceHoverBox.x + 120, resourceHoverBox.y + 20); await expect(heatmapTooltip).toBeVisible(); await expect .poll(async () => { return heatmapTooltip.evaluate((element) => getComputedStyle(element).backgroundColor); }) .toBe("rgba(3, 7, 18, 0.96)"); const resourceAllocation = page .locator( "div.absolute.rounded-md.flex.items-stretch.overflow-hidden.transition-all.duration-75", ) .first(); await resourceAllocation.click({ button: "right" }); await expect(allocationPopoverField).toBeVisible(); await page.mouse.click(40, 40); await page.getByText("Project view").click(); await expect(page.getByText(/projects/)).toBeVisible(); await page.waitForTimeout(500); const projectHoverTarget = page.getByTestId("timeline-project-resource-row-canvas").first(); const projectHoverBox = await projectHoverTarget.boundingBox(); const projectAllocation = page.locator("div[style*='top: 2px'][style*='bottom: 2px']").nth(1); const projectAllocationBox = await projectAllocation.boundingBox(); expect(projectHoverBox).not.toBeNull(); expect(projectAllocationBox).not.toBeNull(); if (!projectHoverBox) { throw new Error("Expected a project timeline row canvas to be available"); } if (!projectAllocationBox) { throw new Error("Expected a project allocation block to be available"); } await page.mouse.move(projectAllocationBox.x + (projectAllocationBox.width / 2), projectHoverBox.y + 20); await expect(heatmapTooltip).toBeVisible(); await expect .poll(async () => { return heatmapTooltip.evaluate((element) => getComputedStyle(element).backgroundColor); }) .toBe("rgba(3, 7, 18, 0.96)"); await projectAllocation.click({ button: "right" }); await expect(allocationPopoverField).toBeVisible(); }); test("shows resolved holiday overlays in the resource timeline and exposes the holiday name in the tooltip", async ({ page, }) => { await page.goto("/timeline?startDate=2026-04-01&days=14&eids=bruce.banner", { waitUntil: "domcontentloaded", }); const row = page.locator('[data-testid="timeline-resource-row-canvas"][data-resource-eid="bruce.banner"]').first(); await expect(row).toBeVisible(); const holidayBlock = row.locator( '[data-testid="timeline-vacation-block"][data-vacation-type="PUBLIC_HOLIDAY"][data-vacation-note="Karfreitag"]', ).first(); await expect(holidayBlock).toBeVisible(); const rowBox = await row.boundingBox(); const holidayBox = await holidayBlock.boundingBox(); expect(rowBox).not.toBeNull(); expect(holidayBox).not.toBeNull(); if (!rowBox || !holidayBox) { throw new Error("Expected timeline row and holiday block bounding boxes to be available"); } await row.hover({ position: { x: holidayBox.x - rowBox.x + holidayBox.width / 2, y: holidayBox.y - rowBox.y + Math.min(holidayBox.height / 2, rowBox.height - 4), }, }); const holidayTooltip = page .locator("div.fixed.pointer-events-none.rounded-xl.border.border-amber-700\\/50") .or(page.locator("div.fixed.pointer-events-none.rounded-xl").filter({ hasText: "Karfreitag" })) .first(); await expect(holidayTooltip).toBeVisible(); await expect(holidayTooltip).toContainText("Karfreitag"); await expect(holidayTooltip).toContainText("3 April 2026"); }); });