Files
CapaKraken/apps/web/e2e/timeline.spec.ts
T

186 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("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");
});
});