// ABOUTME: E2E tests for calendar functionality including ICS feed and calendar view. // ABOUTME: Tests calendar display, navigation, and ICS subscription features. import { expect, test } from "@playwright/test"; test.describe("calendar", () => { test.describe("unauthenticated", () => { test("calendar page redirects to login when not authenticated", async ({ page, }) => { await page.goto("/calendar"); // Should redirect to /login await expect(page).toHaveURL(/\/login/); }); }); test.describe("authenticated", () => { // These tests require TEST_USER_EMAIL and TEST_USER_PASSWORD env vars test.beforeEach(async ({ page }) => { const email = process.env.TEST_USER_EMAIL; const password = process.env.TEST_USER_PASSWORD; if (!email || !password) { test.skip(); return; } // Login via the login page await page.goto("/login"); await page.waitForLoadState("networkidle"); const emailInput = page.getByLabel(/email/i); const hasEmailForm = await emailInput.isVisible().catch(() => false); if (!hasEmailForm) { test.skip(); return; } await emailInput.fill(email); await page.getByLabel(/password/i).fill(password); await page.getByRole("button", { name: /sign in/i }).click(); await page.waitForURL("/", { timeout: 10000 }); await page.goto("/calendar"); await page.waitForLoadState("networkidle"); }); test("displays calendar page with heading", async ({ page }) => { // Check for calendar heading const heading = page.getByRole("heading", { name: /calendar/i }); await expect(heading).toBeVisible(); }); test("shows month view calendar", async ({ page }) => { // Look for calendar grid structure const calendarGrid = page .getByRole("grid") .or(page.locator('[data-testid="month-view"]')); await expect(calendarGrid).toBeVisible(); }); test("shows month and year display", async ({ page }) => { // Calendar should show current month/year const monthYear = page.getByText( /january|february|march|april|may|june|july|august|september|october|november|december/i, ); await expect(monthYear.first()).toBeVisible(); }); test("has navigation controls for months", async ({ page }) => { // Look for previous/next month buttons const prevButton = page.getByRole("button", { name: /prev|previous|←|back/i, }); const nextButton = page.getByRole("button", { name: /next|→|forward/i }); // At least one navigation control should be visible const hasPrev = await prevButton.isVisible().catch(() => false); const hasNext = await nextButton.isVisible().catch(() => false); expect(hasPrev || hasNext).toBe(true); }); test("can navigate to previous month", async ({ page }) => { const prevButton = page.getByRole("button", { name: /prev|previous|←/i }); const hasPrev = await prevButton.isVisible().catch(() => false); if (hasPrev) { // Click previous month button await prevButton.click(); // Wait for update - verify page doesn't error await page.waitForTimeout(500); // Verify calendar is still rendered const monthYear = page.getByText( /january|february|march|april|may|june|july|august|september|october|november|december/i, ); await expect(monthYear.first()).toBeVisible(); } }); test("can navigate to next month", async ({ page }) => { const nextButton = page.getByRole("button", { name: /next|→/i }); const hasNext = await nextButton.isVisible().catch(() => false); if (hasNext) { // Click next await nextButton.click(); // Wait for update await page.waitForTimeout(500); } }); test("shows ICS subscription section", async ({ page }) => { // Look for calendar subscription / ICS section const subscriptionText = page.getByText( /subscribe|subscription|calendar.*url|ics/i, ); const hasSubscription = await subscriptionText .first() .isVisible() .catch(() => false); // This may not be visible if user hasn't generated a token if (hasSubscription) { await expect(subscriptionText.first()).toBeVisible(); } }); test("shows generate or regenerate token button", async ({ page }) => { // Look for generate/regenerate button const tokenButton = page.getByRole("button", { name: /generate|regenerate/i, }); const hasButton = await tokenButton.isVisible().catch(() => false); if (hasButton) { await expect(tokenButton).toBeVisible(); } }); test("shows copy button when URL exists", async ({ page }) => { // Copy button only shows when URL is generated const copyButton = page.getByRole("button", { name: /copy/i }); const hasCopy = await copyButton.isVisible().catch(() => false); if (hasCopy) { await expect(copyButton).toBeVisible(); } }); test("shows back navigation", async ({ page }) => { const backLink = page.getByRole("link", { name: /back|home|dashboard/i }); await expect(backLink).toBeVisible(); }); test("can navigate back to dashboard", async ({ page }) => { const backLink = page.getByRole("link", { name: /back|home|dashboard/i }); await backLink.click(); await expect(page).toHaveURL("/"); }); }); test.describe("ICS endpoint", () => { test("ICS endpoint returns error for invalid user", async ({ page }) => { const response = await page.request.get( "/api/calendar/invalid-user-id/invalid-token.ics", ); // Should return 404 (user not found) or 500 (PocketBase not connected in test env) expect([404, 500]).toContain(response.status()); }); test("ICS endpoint returns error for invalid token", async ({ page }) => { // Need a valid user ID but invalid token - this would require setup // For now, just verify the endpoint exists and returns appropriate error const response = await page.request.get("/api/calendar/test/invalid.ics"); // Should return 404 (user not found), 401 (invalid token), or 500 (PocketBase not connected) expect([401, 404, 500]).toContain(response.status()); }); }); test.describe("calendar regenerate token API", () => { test("regenerate token requires authentication", async ({ page }) => { const response = await page.request.post( "/api/calendar/regenerate-token", ); // Should return 401 Unauthorized expect(response.status()).toBe(401); }); }); });