diff --git a/CLAUDE.md b/CLAUDE.md index 2efee5b..e71ded0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `npm run test:e2e:debug` - Debug tests - `npm run test:e2e:report` - Show test report - `npm run test:setup` - Setup test environment -- `npm run test:setup:full` - Setup full local Supabase ## Architecture Overview diff --git a/package.json b/package.json index 6a73878..25a4739 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "test:e2e:debug": "playwright test --debug", "test:e2e:report": "playwright show-report", "test:setup": "bash scripts/setup-test-env.sh", - "test:setup:full": "bash scripts/setup-local-supabase.sh", "types:generate": "supabase gen types typescript --project-id qssmazlqrmxiudxckxvi > src/integrations/supabase/types.ts", "types:generate:local": "supabase gen types typescript --local > src/integrations/supabase/types.ts", "typecheck": "tsc --noEmit --project tsconfig.app.json", diff --git a/scripts/setup-local-supabase.sh b/scripts/setup-local-supabase.sh deleted file mode 100755 index dfb7486..0000000 --- a/scripts/setup-local-supabase.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Comprehensive local Supabase setup script - -echo "🚀 Setting up local Supabase environment..." - -# Check if Supabase CLI is installed -if ! command -v supabase &> /dev/null; then - echo "❌ Supabase CLI not found. Please install it first:" - echo " npm install -g supabase" - exit 1 -fi - -# Check if Docker is running -if ! docker info &> /dev/null; then - echo "❌ Docker is not running. Please start Docker first." - exit 1 -fi - -# Stop any existing Supabase instance -echo "🛑 Stopping any existing Supabase instances..." -supabase stop 2>/dev/null || true - -# Start local Supabase -echo "📦 Starting local Supabase..." -supabase start - -# Wait for Supabase to be ready -echo "⏳ Waiting for Supabase to be ready..." -sleep 15 - -# Check if Supabase is running -if ! supabase status &> /dev/null; then - echo "❌ Failed to start Supabase. Please check Docker and try again." - exit 1 -fi - -# Run migrations -echo "🔄 Running database migrations..." -supabase db reset - -# Verify migrations -echo "✅ Verifying database schema..." -supabase db diff --schema public diff --git a/src/pages/EditionSelection.tsx b/src/pages/EditionSelection.tsx index e6d16f5..245e837 100644 --- a/src/pages/EditionSelection.tsx +++ b/src/pages/EditionSelection.tsx @@ -118,15 +118,15 @@ export default function EditionSelection() { ); } - const formatDate = (dateString: string) => { + function formatDate(dateString: string) { return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); - }; + } - const getEditionStatus = (edition: FestivalEdition) => { + function getEditionStatus(edition: FestivalEdition) { const now = new Date(); const startDate = new Date(edition.start_date || ""); const endDate = new Date(edition.end_date || ""); @@ -138,7 +138,7 @@ export default function EditionSelection() { } else { return { status: "ended", label: "Ended", color: "gray" }; } - }; + } return (
diff --git a/tests/e2e/admin.spec.ts b/tests/e2e/admin.spec.ts new file mode 100644 index 0000000..5b30e24 --- /dev/null +++ b/tests/e2e/admin.spec.ts @@ -0,0 +1,272 @@ +import { test, expect } from "@playwright/test"; +import { TestHelpers } from "../utils/test-helpers"; + +let testHelpers: TestHelpers; + +test.beforeEach(async ({ page }) => { + testHelpers = new TestHelpers(page); +}); + +test("should display admin dashboard or login prompt", async ({ page }) => { + await testHelpers.navigateTo("/admin"); + + // Should show admin content or redirect to login + const adminContent = page + .getByRole("main") + .or(page.locator('[data-testid*="admin"]')); + + const loginPrompt = page.getByText(/sign in|login|unauthorized/i); + + // Either show admin content or login prompt + const hasValidResponse = await Promise.race([ + adminContent.isVisible(), + loginPrompt.isVisible(), + ]); + + expect(hasValidResponse).toBe(true); +}); + +test("should show admin navigation when authorized", async ({ page }) => { + await testHelpers.signIn(); + await testHelpers.navigateTo("/admin"); + + // Look for admin navigation elements + const adminNav = page + .getByRole("navigation") + .or(page.locator('[data-testid*="admin-nav"]')) + .or(page.locator(".admin-nav, .sidebar")); + + if (await adminNav.isVisible()) { + await expect(adminNav).toBeVisible(); + + // Should have admin-specific links + const adminLinks = adminNav.getByRole("link", { + name: /dashboard|artists|festivals|analytics|roles/i, + }); + if (await adminLinks.first().isVisible()) { + await expect(adminLinks.first()).toBeVisible(); + } + } +}); + +test("should navigate to artists management", async ({ page }) => { + await testHelpers.navigateTo("/admin/artists"); + + // Should show artists management or auth prompt + const artistsManagement = page.getByRole("main"); + await expect(artistsManagement).toBeVisible(); + + // If authorized, should show artists table or management interface + const artistsTable = page + .getByRole("table") + .or(page.locator('[data-testid*="artists"]')); + + if (await artistsTable.isVisible()) { + await expect(artistsTable).toBeVisible(); + } +}); + +test("should navigate to festivals management", async ({ page }) => { + await testHelpers.navigateTo("/admin/festivals"); + + const festivalsContent = page.getByRole("main"); + await expect(festivalsContent).toBeVisible(); + + // Look for festivals management interface + const festivalsTable = page + .getByRole("table") + .or(page.locator('[data-testid*="festivals"]')); + + if (await festivalsTable.isVisible()) { + await expect(festivalsTable).toBeVisible(); + } +}); + +test("should navigate to analytics", async ({ page }) => { + await testHelpers.navigateTo("/admin/analytics"); + + const analyticsContent = page.getByRole("main"); + await expect(analyticsContent).toBeVisible(); + + // Look for analytics dashboard elements + const analyticsElements = page + .locator('[data-testid*="analytics"]') + .or(page.locator(".chart, .metric, .stats")); + + if (await analyticsElements.first().isVisible()) { + await expect(analyticsElements.first()).toBeVisible(); + } +}); + +test("should navigate to admin roles", async ({ page }) => { + await testHelpers.navigateTo("/admin/admins"); + + const rolesContent = page.getByRole("main"); + await expect(rolesContent).toBeVisible(); + + // Look for roles management interface + const rolesTable = page + .getByRole("table") + .or(page.locator('[data-testid*="roles"], [data-testid*="admins"]')); + + if (await rolesTable.isVisible()) { + await expect(rolesTable).toBeVisible(); + } +}); + +test("should show add artist functionality", async ({ page }) => { + await testHelpers.navigateTo("/admin/artists"); + + // Look for add artist button + const addButton = page + .getByRole("button", { name: /add artist|new artist|create/i }) + .or(page.locator('[data-testid*="add-artist"]')); + + if (await addButton.isVisible()) { + await expect(addButton).toBeVisible(); + + // Click to open dialog + await addButton.click(); + + // Should show add artist dialog + const dialog = page.getByRole("dialog"); + if (await dialog.isVisible()) { + await expect(dialog).toBeVisible(); + + // Should have artist name input + const nameInput = page + .getByLabel(/name|title/i) + .or(page.getByPlaceholder(/artist name/i)); + + if (await nameInput.isVisible()) { + await expect(nameInput).toBeVisible(); + } + } + } +}); + +test("should show edit artist functionality", async ({ page }) => { + await testHelpers.navigateTo("/admin/artists"); + + // Look for edit buttons in artists table + const editButtons = page + .getByRole("button", { name: /edit/i }) + .or(page.locator('[data-testid*="edit"]')); + + if (await editButtons.first().isVisible()) { + await expect(editButtons.first()).toBeVisible(); + + // Click edit button + await editButtons.first().click(); + + // Should show edit dialog + const dialog = page.getByRole("dialog"); + if (await dialog.isVisible()) { + await expect(dialog).toBeVisible(); + } + } +}); + +test("should show festival creation functionality", async ({ page }) => { + await testHelpers.navigateTo("/admin/festivals"); + + // Look for create festival button + const createButton = page + .getByRole("button", { name: /add festival|new festival|create/i }) + .or(page.locator('[data-testid*="add-festival"]')); + + if (await createButton.isVisible()) { + await expect(createButton).toBeVisible(); + + await createButton.click(); + + // Should show festival creation dialog + const dialog = page.getByRole("dialog"); + if (await dialog.isVisible()) { + await expect(dialog).toBeVisible(); + } + } +}); + +test("should show CSV import functionality", async ({ page }) => { + await testHelpers.navigateTo("/admin/artists"); + + // Look for import/CSV functionality + const importButton = page + .getByRole("button", { name: /import|csv|upload/i }) + .or(page.locator('[data-testid*="import"]')); + + if (await importButton.isVisible()) { + await expect(importButton).toBeVisible(); + + await importButton.click(); + + // Should show import dialog + const dialog = page.getByRole("dialog"); + if (await dialog.isVisible()) { + await expect(dialog).toBeVisible(); + + // Should have file input + const fileInput = page.locator('input[type="file"]'); + if (await fileInput.isVisible()) { + await expect(fileInput).toBeVisible(); + } + } + } +}); + +test("should handle unauthorized access gracefully", async ({ page }) => { + // Test accessing admin without proper permissions + await testHelpers.navigateTo("/admin"); + + const isAuth = await testHelpers.isAuthenticated(); + if (!isAuth) { + // Should redirect to login or show unauthorized message + const unauthorizedContent = page + .getByText(/sign in|login|unauthorized|access denied/i) + .or(page.getByRole("button", { name: /sign in/i })); + + const hasAuthPrompt = await unauthorizedContent.isVisible(); + expect(hasAuthPrompt).toBe(true); + } +}); + +test("should show data tables with proper headers", async ({ page }) => { + await testHelpers.navigateTo("/admin/artists"); + + const table = page.getByRole("table"); + + if (await table.isVisible()) { + await expect(table).toBeVisible(); + + // Should have table headers + const headers = table.locator('thead th, [role="columnheader"]'); + if (await headers.first().isVisible()) { + const headerCount = await headers.count(); + expect(headerCount).toBeGreaterThan(0); + } + } +}); + +test("should support search/filter in admin tables", async ({ page }) => { + await testHelpers.navigateTo("/admin/artists"); + + // Look for search functionality + const searchInput = page + .getByPlaceholder(/search/i) + .or(page.getByLabel(/search/i)); + + if (await searchInput.isVisible()) { + await expect(searchInput).toBeVisible(); + + // Try searching + await searchInput.fill("test"); + await testHelpers.waitForPageLoad(); + + // Table should still be visible + const table = page.getByRole("table"); + if (await table.isVisible()) { + await expect(table).toBeVisible(); + } + } +}); diff --git a/tests/e2e/festival-selection.spec.ts b/tests/e2e/festival-selection.spec.ts new file mode 100644 index 0000000..597d6ec --- /dev/null +++ b/tests/e2e/festival-selection.spec.ts @@ -0,0 +1,238 @@ +import { test, expect } from "@playwright/test"; +import { TestHelpers } from "../utils/test-helpers"; + +let testHelpers: TestHelpers; + +test.beforeEach(async ({ page }) => { + testHelpers = new TestHelpers(page); +}); + +test("should display festival selection page", async ({ page }) => { + await testHelpers.navigateTo("/"); + + await page + .getByText( + "Choose a festival to start voting and collaborating with your community.", + ) + .isVisible(); + + await page.getByRole("list", { name: "festival list" }).isVisible(); +}); + +test("should display available festivals or empty list", async ({ page }) => { + await testHelpers.navigateTo("/"); + + // Look for festival cards or list items + const list = page.getByRole("list", { name: "festival list" }); + const festivalItems = list.getByRole("listitem"); + + if (await festivalItems.first().isVisible()) { + await expect(festivalItems.first()).toBeVisible(); + + // Should have festival names + const festivalName = festivalItems + .first() + .getByRole("heading") + .or(festivalItems.first().locator("h1, h2, h3, h4")); + + await festivalName.isVisible(); + } else { + // Should show empty state if no festivals + const emptyState = page + .getByText(/no festivals|coming soon|empty/i) + .or(page.locator('[data-testid*="empty"]')); + + await emptyState.isVisible(); + } +}); + +test("should navigate to festival editions", async ({ page }) => { + await testHelpers.navigateTo("/"); + + const list = page.getByRole("list", { name: "festival list" }); + const festivalItems = list.getByRole("listitem"); + + if (await festivalItems.first().isVisible()) { + await festivalItems.first().click(); + + // Should navigate to festival editions page + await expect(page).toHaveURL(/\/(festivals\/[^/]+|editions)/); + + await expect( + page + .getByText("Choose Festival Edition") + .or(page.getByText("artists available for voting")) + .or(page.getByText("No editions available", {})), + ).toBeVisible(); + } +}); + +test("should display festival editions", async ({ page }) => { + await testHelpers.navigateTo("/"); + + // First navigate to a festival if available + const festivalLinks = page.locator('a[href*="/festivals/"]'); + + if (await festivalLinks.first().isVisible()) { + await festivalLinks.first().click(); + + // Look for edition cards or list items + const editionItems = page + .locator('[data-testid*="edition"]') + .or(page.locator(".edition-card, .edition-item")) + .or( + page.getByText(/20\d{2}|\d{4}/i), // Year patterns + ); + + if (await editionItems.first().isVisible()) { + await expect(editionItems.first()).toBeVisible(); + } + } +}); + +test("should navigate to edition view", async ({ page }) => { + await testHelpers.navigateTo("/"); + + // Navigate through festival -> edition + const festivalLinks = page.locator('a[href*="/festivals/"]'); + + if (await festivalLinks.first().isVisible()) { + await festivalLinks.first().click(); + + // Look for edition links + const editionLinks = page + .locator('a[href*="/editions/"]') + .or(page.locator('[data-testid*="edition"] a')); + + if (await editionLinks.first().isVisible()) { + await editionLinks.first().click(); + + // Should navigate to edition view (main festival page) + await expect(page).toHaveURL(/\/editions\/[^/]+/); + + // Should show main festival interface + const mainContent = page.getByRole("main"); + await expect(mainContent).toBeVisible(); + } + } +}); + +test("should show festival information", async ({ page }) => { + await testHelpers.navigateTo("/"); + + const festivalCards = page + .locator('[data-testid*="festival"]') + .or(page.locator(".festival-card")); + + if (await festivalCards.first().isVisible()) { + // Should show festival name + const festivalName = festivalCards.first().getByRole("heading"); + if (await festivalName.isVisible()) { + await expect(festivalName).toBeVisible(); + } + + // Should show dates or year information + const dateInfo = festivalCards + .first() + .getByText(/20\d{2}|\d{1,2}\/\d{1,2}|\w+\s+\d{1,2}/i); + if (await dateInfo.isVisible()) { + await expect(dateInfo).toBeVisible(); + } + + // Should show location information + const locationInfo = festivalCards + .first() + .getByText(/location|where|venue|city/i); + if (await locationInfo.isVisible()) { + await expect(locationInfo).toBeVisible(); + } + } +}); + +test("should handle festival logo/images", async ({ page }) => { + await testHelpers.navigateTo("/"); + + // Look for festival images or logos + const festivalImages = page + .locator('[data-testid*="festival"] img') + .or(page.locator(".festival-card img, .festival-logo")); + + if (await festivalImages.first().isVisible()) { + await expect(festivalImages.first()).toBeVisible(); + + // Image should have alt text + const altText = await festivalImages.first().getAttribute("alt"); + expect(altText).toBeTruthy(); + } +}); + +test("should support direct festival URL access", async ({ page }) => { + // Test accessing festival directly via subdomain or path + // This would depend on the actual festival slugs available + + await testHelpers.navigateTo("/festivals/test-festival"); + + // Should either show editions page or redirect appropriately + const pageContent = page.getByRole("main"); + await expect(pageContent).toBeVisible(); + + // Might be 404 if no test festival exists, which is also valid + const notFoundContent = page.getByText(/not found|404/i); + const validContent = page.locator( + '[data-testid*="edition"], [data-testid*="festival"]', + ); + + const hasValidResponse = await Promise.race([ + notFoundContent.isVisible(), + validContent.first().isVisible(), + ]); + + expect(hasValidResponse).toBe(true); +}); + +test("should handle edition URL access", async ({ page }) => { + // Test accessing specific edition directly + await testHelpers.navigateTo("/festivals/test-festival/editions/2024"); + + // Should show either the edition page or appropriate error + const pageContent = page.getByRole("main"); + await expect(pageContent).toBeVisible(); +}); + +test("should show upcoming vs past festivals", async ({ page }) => { + await testHelpers.navigateTo("/"); + + // Look for any indicators of festival status (upcoming, current, past) + const statusIndicators = page + .getByText(/upcoming|current|past|live|ended/i) + .or(page.locator('[data-testid*="status"]')); + + if (await statusIndicators.first().isVisible()) { + await expect(statusIndicators.first()).toBeVisible(); + } +}); + +test("should handle search/filter functionality", async ({ page }) => { + await testHelpers.navigateTo("/"); + + // Look for search or filter controls + const searchInput = page + .getByPlaceholder(/search/i) + .or(page.getByLabel(/search/i)); + + const filterControls = page + .getByRole("button", { name: /filter/i }) + .or(page.locator('[data-testid*="filter"]')); + + if (await searchInput.isVisible()) { + await expect(searchInput).toBeVisible(); + + // Try searching + await searchInput.fill("test"); + await testHelpers.waitForPageLoad(); + } + + if (await filterControls.isVisible()) { + await expect(filterControls).toBeVisible(); + } +}); diff --git a/tests/e2e/groups.spec.ts b/tests/e2e/groups.spec.ts new file mode 100644 index 0000000..2b3c719 --- /dev/null +++ b/tests/e2e/groups.spec.ts @@ -0,0 +1,205 @@ +import { test, expect } from "@playwright/test"; +import { TestHelpers } from "../utils/test-helpers"; + +test.describe("Groups", () => { + let testHelpers: TestHelpers; + + test.beforeEach(async ({ page }) => { + testHelpers = new TestHelpers(page); + }); + + test("should display groups page", async ({ page }) => { + await testHelpers.navigateTo("/groups"); + + // Should show groups page content + const groupsContent = page + .locator('[data-testid*="groups"]') + .or(page.getByRole("main")); + + await expect(groupsContent).toBeVisible(); + + // Should have page title or heading + const heading = page.getByRole("heading", { name: /groups?/i }).first(); + await expect(heading).toBeVisible(); + }); + + test("should show create group option when authenticated", async ({ + page, + }) => { + await testHelpers.navigateTo("/groups"); + + const isAuth = await testHelpers.isAuthenticated(); + + if (isAuth) { + // Look for create group button or dialog + const createGroupButton = page + .getByRole("button", { name: /create group|new group|add group/i }) + .or(page.getByText(/create group|new group/i)); + + if (await createGroupButton.isVisible()) { + await expect(createGroupButton).toBeVisible(); + + // Click to open create group dialog + await createGroupButton.click(); + + // Should show create group dialog + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + // Should have group name input + const nameInput = page + .getByLabel(/name|title/i) + .or(page.getByPlaceholder(/group name|name/i)); + await expect(nameInput).toBeVisible(); + } + } else { + // Should either hide create button or show sign-in prompt + const createGroupButton = page.getByRole("button", { + name: /create group|new group/i, + }); + const signInPrompt = page.getByText(/sign in|login/i); + + // Either no create button visible or sign in prompt shown + const hasAuthenticationHandling = + !(await createGroupButton.isVisible()) || + (await signInPrompt.isVisible()); + expect(hasAuthenticationHandling).toBe(true); + } + }); + + test("should display existing groups list", async ({ page }) => { + await testHelpers.navigateTo("/groups"); + + // Look for groups list or cards + const groupsList = page + .locator('[data-testid*="groups-list"]') + .or(page.locator('[data-testid*="group-card"]')) + .or(page.locator(".group-card, .group-item")); + + // If groups exist, they should be displayed + if (await groupsList.first().isVisible()) { + await expect(groupsList.first()).toBeVisible(); + } else { + // Should show empty state + const emptyState = page + .getByText(/no groups|empty|create your first/i) + .or(page.locator('[data-testid*="empty"]')); + + if (await emptyState.isVisible()) { + await expect(emptyState).toBeVisible(); + } + } + }); + + test("should navigate to group detail page", async ({ page }) => { + await testHelpers.navigateTo("/groups"); + + // Look for group cards or links + const groupLinks = page + .locator('[data-testid*="group-card"] a') + .or(page.locator('a[href*="/groups/"]')) + .or(page.getByRole("link").filter({ hasText: /group/i })); + + if (await groupLinks.first().isVisible()) { + await groupLinks.first().click(); + + // Should navigate to group detail page + await expect(page).toHaveURL(/\/groups\/[^/]+/); + + // Should show group detail content + const groupDetail = page + .locator('[data-testid*="group-detail"]') + .or(page.getByRole("main")); + await expect(groupDetail).toBeVisible(); + } + }); + + test("should handle group invitations", async ({ page }) => { + // Test invite link functionality + // This would typically be tested with a known invite link + + // Check if there are invite-related elements on groups page + const inviteElements = page + .getByText(/invite|share/i) + .or(page.locator('[data-testid*="invite"]')); + + if (await inviteElements.first().isVisible()) { + await expect(inviteElements.first()).toBeVisible(); + } + }); + + test("should show group management options for group creators", async ({ + page, + }) => { + await testHelpers.navigateTo("/groups"); + + const isAuth = await testHelpers.isAuthenticated(); + if (!isAuth) { + test.skip("Authentication required for group management test"); + } + + // Look for group cards or navigate to a group detail + const groupLinks = page.locator('a[href*="/groups/"]'); + + if (await groupLinks.first().isVisible()) { + await groupLinks.first().click(); + + // Look for management options (edit, delete, manage members) + const managementOptions = page + .getByRole("button", { name: /edit|delete|manage|settings/i }) + .or(page.locator('[data-testid*="manage"]')); + + // If user is group creator, should see management options + if (await managementOptions.first().isVisible()) { + await expect(managementOptions.first()).toBeVisible(); + } + } + }); + + test("should display group members", async ({ page }) => { + await testHelpers.navigateTo("/groups"); + + const groupLinks = page.locator('a[href*="/groups/"]'); + + if (await groupLinks.first().isVisible()) { + await groupLinks.first().click(); + + // Should show group members section + const membersSection = page + .getByText(/members/i) + .or(page.locator('[data-testid*="members"]')); + + if (await membersSection.isVisible()) { + await expect(membersSection).toBeVisible(); + + // Should show at least the current user or creator + const membersList = page + .locator('[data-testid*="member"]') + .or(page.locator(".member-card, .member-item")); + + if (await membersList.first().isVisible()) { + await expect(membersList.first()).toBeVisible(); + } + } + } + }); + + test("should allow joining group via invite link", async ({ page }) => { + // This test would need a valid invite link to test properly + // For now, we'll test the invite landing page structure + + const invitePattern = /\/groups\/.*invite/; + + // Try to visit a mock invite URL structure + await testHelpers.navigateTo("/groups/test-group?invite=123"); + + // Should either show invite acceptance page or redirect appropriately + const inviteContent = page + .getByText(/join|invite|accept/i) + .or(page.locator('[data-testid*="invite"]')); + + // This might show 404 or invite page depending on implementation + const pageContent = page.getByRole("main"); + await expect(pageContent).toBeVisible(); + }); +}); diff --git a/tests/e2e/schedule.spec.ts b/tests/e2e/schedule.spec.ts new file mode 100644 index 0000000..4b5267b --- /dev/null +++ b/tests/e2e/schedule.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from "@playwright/test"; +import { TestHelpers } from "../utils/test-helpers"; + +test.describe("Schedule", () => { + let testHelpers: TestHelpers; + + test.beforeEach(async ({ page }) => { + testHelpers = new TestHelpers(page); + }); + + test("should display schedule page", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Should show schedule content + const scheduleContent = page + .getByRole("main") + .or(page.locator('[data-testid*="schedule"]')); + + await expect(scheduleContent).toBeVisible(); + + // Should have schedule-related heading + const heading = page.getByRole("heading").first(); + if (await heading.isVisible()) { + await expect(heading).toBeVisible(); + } + }); + + test("should show sets/artists in schedule view", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for schedule items (sets, artists, time slots) + const scheduleItems = page + .locator('[data-testid*="schedule-item"]') + .or(page.locator('[data-testid*="set"]')) + .or(page.locator(".schedule-item, .set-item, .artist-block")); + + // If schedule has content, items should be visible + if (await scheduleItems.first().isVisible()) { + await expect(scheduleItems.first()).toBeVisible(); + } else { + // Should show empty state if no schedule + const emptyState = page + .getByText(/no schedule|empty|coming soon/i) + .or(page.locator('[data-testid*="empty"]')); + + if (await emptyState.isVisible()) { + await expect(emptyState).toBeVisible(); + } + } + }); + + test("should display time information", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for time-related elements + const timeElements = page + .locator('[data-testid*="time"]') + .or(page.locator(".time, .schedule-time")) + .or(page.getByText(/\d{1,2}:\d{2}|\d{1,2}(am|pm)/i)); + + if (await timeElements.first().isVisible()) { + await expect(timeElements.first()).toBeVisible(); + + // Time should be in recognizable format + const timeText = await timeElements.first().textContent(); + expect(timeText).toMatch(/\d{1,2}[:\s]?\d{2}|am|pm|\d{1,2}h/i); + } + }); + + test("should show stage information", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for stage labels or indicators + const stageElements = page + .locator('[data-testid*="stage"]') + .or(page.locator(".stage-label, .stage-name")) + .or(page.getByText(/stage|main|tent|arena/i)); + + if (await stageElements.first().isVisible()) { + await expect(stageElements.first()).toBeVisible(); + } + }); + + test("should allow navigation between days", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for day navigation controls + const dayNavigation = page + .locator('[data-testid*="day"]') + .or( + page.getByRole("button", { name: /day|today|tomorrow|previous|next/i }), + ) + .or(page.locator(".day-selector, .date-navigation")); + + if (await dayNavigation.first().isVisible()) { + await expect(dayNavigation.first()).toBeVisible(); + + // Try clicking day navigation + await dayNavigation.first().click(); + await testHelpers.waitForPageLoad(); + + // Schedule should still be visible after navigation + const scheduleContent = page.getByRole("main"); + await expect(scheduleContent).toBeVisible(); + } + }); + + test("should provide timeline view functionality", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for timeline-specific elements + const timelineElements = page + .locator('[data-testid*="timeline"]') + .or(page.locator(".timeline, .time-scale")) + .or(page.locator('[aria-label*="timeline"]')); + + if (await timelineElements.first().isVisible()) { + await expect(timelineElements.first()).toBeVisible(); + } + }); + + test("should allow clicking on sets to navigate to details", async ({ + page, + }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for clickable set items + const setLinks = page + .locator('a[href*="/sets/"]') + .or(page.locator('[data-testid*="set"] a')) + .or(page.locator(".schedule-item a, .set-item a")); + + if (await setLinks.first().isVisible()) { + await setLinks.first().click(); + + // Should navigate to set detail page + await expect(page).toHaveURL(/\/(sets|artists)\/[^/]+/); + + // Should show set/artist detail content + const detailContent = page.getByRole("main"); + await expect(detailContent).toBeVisible(); + } + }); + + test("should show current time indicator if applicable", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for current time indicator (would be visible during festival) + const currentTimeIndicator = page + .locator('[data-testid*="current-time"]') + .or(page.locator(".current-time, .now-indicator")) + .or(page.locator('[aria-label*="current time"]')); + + // This might not be visible if not during festival time + if (await currentTimeIndicator.isVisible()) { + await expect(currentTimeIndicator).toBeVisible(); + } + }); + + test("should support different view modes", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Look for view mode toggles (list, grid, timeline, etc.) + const viewToggle = page + .getByRole("button", { name: /view|mode|list|grid|timeline/i }) + .or(page.locator('[data-testid*="view"]')) + .or(page.locator(".view-toggle, .mode-selector")); + + if (await viewToggle.first().isVisible()) { + await expect(viewToggle.first()).toBeVisible(); + + // Try switching view mode + await viewToggle.first().click(); + await testHelpers.waitForPageLoad(); + + // Schedule should still be visible after view change + const scheduleContent = page.getRole("main"); + await expect(scheduleContent).toBeVisible(); + } + }); + + test("should handle mobile responsiveness", async ({ page }) => { + await testHelpers.navigateTo("/schedule"); + + // Set mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + + // Schedule should still be accessible on mobile + const scheduleContent = page.getByRole("main"); + await expect(scheduleContent).toBeVisible(); + + // Mobile-specific navigation might be present + const mobileNav = page + .locator('[data-testid*="mobile"]') + .or(page.locator(".mobile-nav, .hamburger-menu")); + + if (await mobileNav.isVisible()) { + await expect(mobileNav).toBeVisible(); + } + + // Reset viewport + await page.setViewportSize({ width: 1280, height: 720 }); + }); +}); diff --git a/tests/e2e/sets.spec.ts b/tests/e2e/sets.spec.ts new file mode 100644 index 0000000..7e562df --- /dev/null +++ b/tests/e2e/sets.spec.ts @@ -0,0 +1,247 @@ +import { test, expect } from "@playwright/test"; +import { TestHelpers } from "../utils/test-helpers"; + +test.describe("Sets", () => { + let testHelpers: TestHelpers; + + test.beforeEach(async ({ page }) => { + testHelpers = new TestHelpers(page); + }); + + test("should display set detail page", async ({ page }) => { + // Navigate to a test set URL + await testHelpers.navigateTo("/sets/test-set"); + + // Should show set content or 404 + const setContent = page.getByRole("main"); + await expect(setContent).toBeVisible(); + + // If set exists, should show set information + const setInfo = page + .locator('[data-testid*="set"]') + .or(page.getByRole("heading")); + + if (await setInfo.isVisible()) { + await expect(setInfo).toBeVisible(); + } + }); + + test("should show set voting buttons when authenticated", async ({ + page, + }) => { + await testHelpers.navigateTo("/sets/test-set"); + + const isAuth = await testHelpers.isAuthenticated(); + + if (isAuth) { + // Look for voting buttons + const voteButtons = page + .locator('[data-testid*="vote"]') + .or(page.getByRole("button", { name: /must go|interested|won't go/i })); + + if (await voteButtons.first().isVisible()) { + await expect(voteButtons.first()).toBeVisible(); + } + } + }); + + test("should display set information", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for set metadata + const setName = page.getByRole("heading").first(); + if (await setName.isVisible()) { + await expect(setName).toBeVisible(); + } + + // Look for time information + const timeInfo = page.getByText(/\d{1,2}:\d{2}|am|pm/i); + if (await timeInfo.isVisible()) { + await expect(timeInfo).toBeVisible(); + } + + // Look for stage information + const stageInfo = page.getByText(/stage|venue/i); + if (await stageInfo.isVisible()) { + await expect(stageInfo).toBeVisible(); + } + }); + + test("should show artists in multi-artist sets", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for artist information + const artistInfo = page + .locator('[data-testid*="artist"]') + .or(page.locator(".artist-card, .artist-item")); + + if (await artistInfo.first().isVisible()) { + await expect(artistInfo.first()).toBeVisible(); + + // Should show artist names + const artistName = artistInfo.first().getByRole("heading"); + if (await artistName.isVisible()) { + await expect(artistName).toBeVisible(); + } + } + }); + + test("should display set image", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for set image + const setImage = page + .locator('[data-testid*="set-image"]') + .or(page.locator('img[alt*="set"], img[alt*="artist"]')); + + if (await setImage.isVisible()) { + await expect(setImage).toBeVisible(); + + // Image should have alt text + const altText = await setImage.getAttribute("alt"); + expect(altText).toBeTruthy(); + } + }); + + test("should show set notes section", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for notes section + const notesSection = page + .locator('[data-testid*="notes"]') + .or(page.getByText(/notes|comments/i)); + + if (await notesSection.isVisible()) { + await expect(notesSection).toBeVisible(); + } + }); + + test("should allow adding notes when authenticated", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + const isAuth = await testHelpers.isAuthenticated(); + if (!isAuth) { + test.skip("Authentication required for notes test"); + } + + // Look for add note functionality + const addNoteButton = page + .getByRole("button", { name: /add note|new note/i }) + .or(page.locator('[data-testid*="add-note"]')); + + const noteInput = page + .getByPlaceholder(/note|comment/i) + .or(page.getByLabel(/note/i)); + + if ((await addNoteButton.isVisible()) || (await noteInput.isVisible())) { + if (await noteInput.isVisible()) { + await expect(noteInput).toBeVisible(); + + // Try adding a note + await noteInput.fill("Test note"); + + // Look for submit button + const submitButton = page.getByRole("button", { + name: /save|submit|add/i, + }); + if (await submitButton.isVisible()) { + await submitButton.click(); + await testHelpers.waitForPageLoad(); + } + } + } + }); + + test("should show genre information", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for genre badges or tags + const genreInfo = page + .locator('[data-testid*="genre"]') + .or(page.locator(".genre-badge, .tag")) + .or(page.getByText(/rock|pop|electronic|jazz|hip hop/i)); + + if (await genreInfo.first().isVisible()) { + await expect(genreInfo.first()).toBeVisible(); + } + }); + + test("should handle social platform links", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for social media links + const socialLinks = page + .locator('a[href*="spotify"], a[href*="youtube"], a[href*="soundcloud"]') + .or(page.locator('[data-testid*="social"]')); + + if (await socialLinks.first().isVisible()) { + await expect(socialLinks.first()).toBeVisible(); + + // Links should open in new tab + const target = await socialLinks.first().getAttribute("target"); + expect(target).toBe("_blank"); + } + }); + + test("should show vote counts and statistics", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for vote statistics + const voteStats = page + .locator('[data-testid*="vote-count"]') + .or(page.locator(".vote-stats, .statistics")); + + if (await voteStats.first().isVisible()) { + await expect(voteStats.first()).toBeVisible(); + + // Should contain numbers + const statsText = await voteStats.first().textContent(); + expect(statsText).toMatch(/\d+/); + } + }); + + test("should handle invalid set URLs gracefully", async ({ page }) => { + await testHelpers.navigateTo("/sets/non-existent-set"); + + // Should show 404 or not found message + const notFoundContent = page + .getByText(/not found|404|doesn't exist/i) + .or(page.locator('[data-testid*="not-found"]')); + + // Some content should be visible (either error or valid content) + const pageContent = page.getByRole("main"); + await expect(pageContent).toBeVisible(); + }); + + test("should support navigation back to main view", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for back navigation + const backButton = page + .getByRole("button", { name: /back|return/i }) + .or(page.locator('[data-testid*="back"]')) + .or(page.getByRole("link", { name: /back|home/i })); + + if (await backButton.isVisible()) { + await expect(backButton).toBeVisible(); + + await backButton.click(); + + // Should navigate away from set detail + await expect(page).not.toHaveURL(/\/sets\/[^/]+$/); + } + }); + + test("should show related sets or recommendations", async ({ page }) => { + await testHelpers.navigateTo("/sets/test-set"); + + // Look for related or recommended sets + const relatedSets = page + .locator('[data-testid*="related"], [data-testid*="recommend"]') + .or(page.getByText(/related|similar|you might like/i)); + + if (await relatedSets.isVisible()) { + await expect(relatedSets).toBeVisible(); + } + }); +}); diff --git a/tests/e2e/voting.spec.ts b/tests/e2e/voting.spec.ts new file mode 100644 index 0000000..ccd66f1 --- /dev/null +++ b/tests/e2e/voting.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from "@playwright/test"; +import { TestHelpers } from "../utils/test-helpers"; + +test.describe("Voting", () => { + let testHelpers: TestHelpers; + + test.beforeEach(async ({ page }) => { + testHelpers = new TestHelpers(page); + await testHelpers.navigateTo("/"); + }); + + test("should allow user to vote on sets/artists when authenticated", async ({ + page, + }) => { + // Skip test if user is not authenticated + const isAuth = await testHelpers.isAuthenticated(); + if (!isAuth) { + // Try to sign in first + const signInButton = page + .getByRole("button", { name: /sign in/i }) + .or(page.getByRole("link", { name: /sign in/i })); + + if (await signInButton.isVisible()) { + await signInButton.click(); + // For now, just skip if auth dialog appears (would need real test credentials) + const authDialog = page.getByRole("dialog"); + if (await authDialog.isVisible()) { + test.skip("Authentication required for voting test"); + } + } else { + test.skip("Cannot find sign in button"); + } + } + + // Look for voting buttons on sets/artists + const voteButtons = page + .locator('[data-testid*="vote"]') + .or(page.locator('button[aria-label*="vote"]')) + .or(page.getByRole("button", { name: /must go|interested|won't go/i })); + + if (await voteButtons.first().isVisible()) { + const initialCount = await voteButtons.count(); + expect(initialCount).toBeGreaterThan(0); + + // Click on a voting button + await voteButtons.first().click(); + + // Should show some feedback (like updated vote count or visual state change) + await testHelpers.waitForPageLoad(); + + // Verify the vote was recorded (button might change state/color) + const updatedButton = voteButtons.first(); + await expect(updatedButton).toBeVisible(); + } + }); + + test("should show vote counts on sets/artists", async ({ page }) => { + // Look for vote count indicators + const voteCountElements = page + .locator('[data-testid*="vote-count"]') + .or(page.locator(".vote-count")) + .or(page.locator('[aria-label*="votes"]')); + + // If vote counts are displayed, they should be visible + if (await voteCountElements.first().isVisible()) { + await expect(voteCountElements.first()).toBeVisible(); + + // Vote counts should contain numbers + const countText = await voteCountElements.first().textContent(); + expect(countText).toMatch(/\d+/); + } + }); + + test("should handle voting without authentication gracefully", async ({ + page, + }) => { + // If not authenticated, voting should either: + // 1. Show sign-in prompt + // 2. Be disabled + // 3. Redirect to auth + + const isAuth = await testHelpers.isAuthenticated(); + if (!isAuth) { + const voteButtons = page + .locator('[data-testid*="vote"]') + .or(page.getByRole("button", { name: /must go|interested|won't go/i })); + + if (await voteButtons.first().isVisible()) { + await voteButtons.first().click(); + + // Should either show auth dialog or sign in button should appear + const authDialog = page.getByRole("dialog"); + const signInButton = page.getByRole("button", { name: /sign in/i }); + + const hasAuthResponse = await Promise.race([ + authDialog.isVisible(), + signInButton.isVisible(), + ]); + + expect(hasAuthResponse).toBe(true); + } + } + }); + + test("should allow changing votes", async ({ page }) => { + const isAuth = await testHelpers.isAuthenticated(); + if (!isAuth) { + test.skip("Authentication required for vote changing test"); + } + + // Look for voting buttons for the same item + const voteButtonGroup = page + .locator('[data-testid*="voting-buttons"]') + .first(); + + if (await voteButtonGroup.isVisible()) { + const individualVoteButtons = voteButtonGroup.locator("button"); + const buttonCount = await individualVoteButtons.count(); + + if (buttonCount >= 2) { + // Click first vote button + await individualVoteButtons.nth(0).click(); + await testHelpers.waitForPageLoad(); + + // Click different vote button + await individualVoteButtons.nth(1).click(); + await testHelpers.waitForPageLoad(); + + // Should handle the vote change without errors + await expect(individualVoteButtons.nth(1)).toBeVisible(); + } + } + }); + + test("should show voting perspective selector if available", async ({ + page, + }) => { + // Look for perspective selector (personal vs group voting) + const perspectiveSelector = page + .locator('[data-testid*="perspective"]') + .or(page.getByLabel(/perspective|view/i)) + .or(page.locator('select[name*="perspective"]')); + + if (await perspectiveSelector.isVisible()) { + await expect(perspectiveSelector).toBeVisible(); + + // Should have multiple options + const options = perspectiveSelector.locator("option"); + if ((await options.count()) > 0) { + expect(await options.count()).toBeGreaterThan(1); + } + } + }); +}); diff --git a/tests/utils/test-helpers.ts b/tests/utils/test-helpers.ts index 7e2a2ee..7404e3a 100644 --- a/tests/utils/test-helpers.ts +++ b/tests/utils/test-helpers.ts @@ -14,10 +14,7 @@ export class TestHelpers { await this.page.goto("/"); // Click sign in button - const signInButton = this.page - .getByRole("button", { name: /sign in/i }) - .or(this.page.getByRole("link", { name: /sign in/i })) - .or(this.page.getByText(/sign in/i)); + const signInButton = this.page.getByRole("button", { name: /sign in/i }); await signInButton.click(); @@ -29,12 +26,8 @@ export class TestHelpers { const emailInput = this.page .getByLabel(/email/i) .or(this.page.getByPlaceholder(/email/i)); - const passwordInput = this.page - .getByLabel(/password/i) - .or(this.page.getByPlaceholder(/password/i)); await emailInput.fill(email); - await passwordInput.fill(password); // Submit form const submitButton = this.page