From 3a63ec8dcc49b0186d4a75fb8d5bb2ec914e12b6 Mon Sep 17 00:00:00 2001 From: Robert van Diest Date: Thu, 30 Apr 2026 16:35:28 +0200 Subject: [PATCH] test(e2e): update Playwright suite for Respellion UI --- tests/e2e/playwright.config.ts | 2 +- tests/e2e/tests/admin.spec.ts | 44 +++++++++--------- tests/e2e/tests/auth.spec.ts | 26 +++++------ tests/e2e/tests/helpers.ts | 11 +++-- tests/e2e/tests/planner.spec.ts | 80 +++++++++++++++------------------ 5 files changed, 81 insertions(+), 82 deletions(-) diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 2ace249..d46b3c6 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ retries: 0, reporter: [['list'], ['html', { open: 'never' }]], use: { - baseURL: process.env.BASE_URL ?? 'http://localhost', + baseURL: process.env.BASE_URL ?? 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', headless: !!process.env.CI, diff --git a/tests/e2e/tests/admin.spec.ts b/tests/e2e/tests/admin.spec.ts index 11b47f3..fcef95b 100644 --- a/tests/e2e/tests/admin.spec.ts +++ b/tests/e2e/tests/admin.spec.ts @@ -6,41 +6,43 @@ test.describe('Admin portal', () => { await loginAsAdmin(page); }); - test('admin user sees the Admin portal button in the header', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Admin portal' })).toBeVisible(); + test('admin user sees the Admin button in the header', async ({ page }) => { + await expect(page.getByRole('button', { name: 'Admin' })).toBeVisible(); }); test('navigates to the admin portal', async ({ page }) => { - await page.getByRole('button', { name: 'Admin portal' }).click(); + await page.getByRole('button', { name: 'Admin' }).click(); await page.waitForURL('/admin'); - await expect(page.getByRole('heading', { name: 'Admin Portal' })).toBeVisible(); - await expect(page.getByText('Manage user accounts')).toBeVisible(); + await expect(page.getByRole('heading', { name: /Who can book/i })).toBeVisible(); + await expect(page.getByText(/Users.*The Hague HQ/i)).toBeVisible(); }); - test('admin portal lists the admin account under Approved accounts', async ({ page }) => { - await page.goto('/admin'); + test('admin portal lists the admin account', async ({ page }) => { + await page.getByRole('button', { name: 'Admin' }).click(); + await page.waitForURL('/admin'); - await expect(page.getByText('Approved accounts')).toBeVisible(); - // Target the name paragraph inside the admin's list item - const adminRow = page.locator('li').filter({ hasText: 'admin@randall.local' }); - await expect(adminRow.getByRole('paragraph').filter({ hasText: /^Admin$/ })).toBeVisible(); + await expect(page.getByText('admin@randall.local')).toBeVisible(); + const adminRow = page.locator('[data-testid="user-row"]').filter({ hasText: 'admin@randall.local' }); + await expect(adminRow.locator('[data-testid="role-badge"]').filter({ hasText: /^admin$/i })).toBeVisible(); }); - test('admin account has the Admin badge and no Make admin button', async ({ page }) => { - await page.goto('/admin'); + test('admin account has the admin role badge and no make-admin button', async ({ page }) => { + await page.getByRole('button', { name: 'Admin' }).click(); + await page.waitForURL('/admin'); - const adminRow = page.locator('li').filter({ hasText: 'admin@randall.local' }); - // The badge is a with class bg-amber-100 - await expect(adminRow.locator('span').filter({ hasText: /^Admin$/ })).toBeVisible(); - await expect(adminRow.getByRole('button', { name: 'Make admin' })).not.toBeVisible(); + const adminRow = page.locator('[data-testid="user-row"]').filter({ hasText: 'admin@randall.local' }); + await expect(adminRow.locator('[data-testid="role-badge"]').filter({ hasText: /^admin$/i })).toBeVisible(); + // The make-admin action button is not rendered for users who are already admin + await expect(adminRow.getByRole('button', { name: 'Admin' })).not.toBeVisible(); }); - test('Back to planner link returns to the planner', async ({ page }) => { - await page.goto('/admin'); - await page.getByRole('button', { name: '← Back to planner' }).click(); + test('wordmark navigates back to the planner', async ({ page }) => { + await page.getByRole('button', { name: 'Admin' }).click(); + await page.waitForURL('/admin'); + await page.locator('span', { hasText: 'randall' }).first().click(); await page.waitForURL('/'); - await expect(page.getByText('Reserve your workspace up to 2 weeks ahead')).toBeVisible(); + await expect(page.getByRole('heading', { name: /Where to sit/i })).toBeVisible(); }); }); diff --git a/tests/e2e/tests/auth.spec.ts b/tests/e2e/tests/auth.spec.ts index f2392bc..737b73f 100644 --- a/tests/e2e/tests/auth.spec.ts +++ b/tests/e2e/tests/auth.spec.ts @@ -7,22 +7,22 @@ test.describe('Authentication', () => { }); test('shows the sign-in form on initial load', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Office Planner' })).toBeVisible(); - await expect(page.getByPlaceholder('jane@company.com')).toBeVisible(); + await expect(page.getByRole('heading', { name: /Find your desk/i })).toBeVisible(); + await expect(page.getByPlaceholder('you@randall.local')).toBeVisible(); await expect(page.getByPlaceholder('••••••••')).toBeVisible(); - await expect(page.locator('button[type="submit"]')).toHaveText('Sign in'); + await expect(page.locator('button[type="submit"]')).toHaveText('Sign in →'); }); test('signs in with valid credentials and reaches the planner', async ({ page }) => { - await page.getByPlaceholder('jane@company.com').fill(ADMIN_EMAIL); + await page.getByPlaceholder('you@randall.local').fill(ADMIN_EMAIL); await page.getByPlaceholder('••••••••').fill(ADMIN_PASSWORD); await page.locator('button[type="submit"]').click(); - await expect(page.getByText('Reserve your workspace up to 2 weeks ahead')).toBeVisible(); + await expect(page.getByRole('heading', { name: /Where to sit/i })).toBeVisible(); }); test('shows an error for an unknown email', async ({ page }) => { - await page.getByPlaceholder('jane@company.com').fill('nobody@example.com'); + await page.getByPlaceholder('you@randall.local').fill('nobody@example.com'); await page.getByPlaceholder('••••••••').fill('anything'); await page.locator('button[type="submit"]').click(); @@ -30,7 +30,7 @@ test.describe('Authentication', () => { }); test('shows an error for a wrong password', async ({ page }) => { - await page.getByPlaceholder('jane@company.com').fill(ADMIN_EMAIL); + await page.getByPlaceholder('you@randall.local').fill(ADMIN_EMAIL); await page.getByPlaceholder('••••••••').fill('wrongpassword'); await page.locator('button[type="submit"]').click(); @@ -39,25 +39,25 @@ test.describe('Authentication', () => { test('registers a new account and shows pending approval message', async ({ page }) => { // Switch to register mode - await page.locator('button').filter({ hasText: 'Create account' }).first().click(); + await page.locator('button').filter({ hasText: 'Register' }).first().click(); - await page.getByPlaceholder('Jane Smith').fill('Test User'); - await page.getByPlaceholder('jane@company.com').fill(`testuser+${Date.now()}@example.com`); + await page.getByPlaceholder('you@randall.local').fill(`testuser+${Date.now()}@example.com`); await page.getByPlaceholder('••••••••').fill('Test@1234'); + await page.getByPlaceholder('Jane Smith').fill('Test User'); await page.locator('button[type="submit"]').click(); - await expect(page.getByText('Account pending approval')).toBeVisible(); + await expect(page.getByText('Almost there.')).toBeVisible(); await expect(page.getByText(/administrator will review/i)).toBeVisible(); }); test('signs out and returns to the sign-in form', async ({ page }) => { - await page.getByPlaceholder('jane@company.com').fill(ADMIN_EMAIL); + await page.getByPlaceholder('you@randall.local').fill(ADMIN_EMAIL); await page.getByPlaceholder('••••••••').fill(ADMIN_PASSWORD); await page.locator('button[type="submit"]').click(); await page.waitForURL('/'); await page.getByRole('button', { name: 'Sign out' }).click(); - await expect(page.locator('button[type="submit"]')).toHaveText('Sign in'); + await expect(page.locator('button[type="submit"]')).toHaveText('Sign in →'); }); }); diff --git a/tests/e2e/tests/helpers.ts b/tests/e2e/tests/helpers.ts index 7bed4d7..20a1160 100644 --- a/tests/e2e/tests/helpers.ts +++ b/tests/e2e/tests/helpers.ts @@ -6,20 +6,25 @@ export const ADMIN_NAME = 'Admin'; export async function loginAs(page: Page, email: string, password: string) { await page.goto('/'); - await page.getByPlaceholder('jane@company.com').fill(email); + await page.getByPlaceholder('you@randall.local').fill(email); await page.getByPlaceholder('••••••••').fill(password); await page.locator('button[type="submit"]').click(); await page.waitForURL('/'); - await page.getByText('Reserve your workspace up to 2 weeks ahead').waitFor(); + await page.locator('h1').waitFor(); } export async function loginAsAdmin(page: Page) { await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD); } -/** Returns today's date offset by `days` in yyyy-MM-dd format */ +/** Returns today's date offset by `days` in yyyy-MM-dd format. */ export function offsetDate(days: number): string { const d = new Date(); d.setDate(d.getDate() + days); return d.toISOString().split('T')[0]; } + +/** Clicks the day-strip cell for today + `days` offset. */ +export async function selectDate(page: Page, days: number) { + await page.locator(`[data-date="${offsetDate(days)}"]`).click(); +} diff --git a/tests/e2e/tests/planner.spec.ts b/tests/e2e/tests/planner.spec.ts index 2438aa7..ca880f6 100644 --- a/tests/e2e/tests/planner.spec.ts +++ b/tests/e2e/tests/planner.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { loginAsAdmin, offsetDate } from './helpers'; +import { loginAsAdmin, offsetDate, selectDate } from './helpers'; test.describe('Planner', () => { test.beforeEach(async ({ page }) => { @@ -8,20 +8,24 @@ test.describe('Planner', () => { test('displays both pods with 8 desks each', async ({ page }) => { // Wait for floor plan to load — at least one free desk must be visible - await expect(page.getByRole('button', { name: 'D1 Free' })).toBeVisible(); + await expect(page.getByRole('button', { name: /^D1 free$/i })).toBeVisible(); // All 16 desk buttons contain a desk label (D1–D16) in their text content - const allDesks = page.getByRole('button').filter({ hasText: /D\d+/ }); + const allDesks = page.getByRole('button').filter({ hasText: /^D\d+/ }); await expect(allDesks).toHaveCount(16); }); - test('date picker is constrained to today and 14 days ahead', async ({ page }) => { - const input = page.locator('input[type="date"]'); - const today = offsetDate(0); - const max = offsetDate(14); + test('date strip shows 14 days and respects boundaries', async ({ page }) => { + // 14 day-strip cells are rendered (one per bookable day) + await expect(page.locator('[data-date]')).toHaveCount(14); - await expect(input).toHaveAttribute('min', today); - await expect(input).toHaveAttribute('max', max); + // Today and the last bookable day are both present + await expect(page.locator(`[data-date="${offsetDate(0)}"]`)).toBeVisible(); + await expect(page.locator(`[data-date="${offsetDate(13)}"]`)).toBeVisible(); + + // Clicking the last day disables the forward arrow + await page.locator(`[data-date="${offsetDate(13)}"]`).click(); + await expect(page.getByRole('button', { name: '→' })).toBeDisabled(); }); test('previous-day button is disabled when on today', async ({ page }) => { @@ -29,72 +33,60 @@ test.describe('Planner', () => { }); test('next-day button is disabled when on the maximum date', async ({ page }) => { - const input = page.locator('input[type="date"]'); - await input.fill(offsetDate(14)); - await input.dispatchEvent('change'); + await page.locator(`[data-date="${offsetDate(13)}"]`).click(); await expect(page.getByRole('button', { name: '→' })).toBeDisabled(); }); - test('reserves a desk and shows it as Mine', async ({ page }) => { - const targetDate = offsetDate(7); - const input = page.locator('input[type="date"]'); - await input.fill(targetDate); - await input.dispatchEvent('change'); + test('reserves a desk and shows it as yours', async ({ page }) => { + await selectDate(page, 7); - await page.getByRole('button', { name: 'D1 Free' }).click(); - await expect(page.getByRole('heading', { name: 'Reserve desk' })).toBeVisible(); + await page.getByRole('button', { name: /^D1 free$/i }).click(); + await expect(page.getByRole('dialog').getByText('Reserve desk')).toBeVisible(); await page.getByRole('button', { name: 'Confirm' }).click(); - await expect(page.getByRole('button', { name: /^D1\s+Mine/ })).toBeVisible(); + await expect(page.getByRole('button', { name: /^D1 yours$/i })).toBeVisible(); // Clean up - await page.getByRole('button', { name: /^D1\s+Mine/ }).click(); + await page.getByRole('button', { name: /^D1 yours$/i }).click(); await page.getByRole('button', { name: 'Cancel reservation' }).click(); }); test('reserved desk appears in My reservations', async ({ page }) => { - const targetDate = offsetDate(8); - const input = page.locator('input[type="date"]'); - await input.fill(targetDate); - await input.dispatchEvent('change'); + await selectDate(page, 8); - await page.getByRole('button', { name: 'D2 Free' }).click(); + await page.getByRole('button', { name: /D2.*free/i }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); - await expect(page.getByRole('heading', { name: 'My reservations' })).toBeVisible(); - await expect(page.locator('section').filter({ hasText: 'My reservations' }).getByText('D2')).toBeVisible(); + await expect(page.getByText('My reservations')).toBeVisible(); + await expect(page.locator('li').filter({ hasText: 'D2' })).toBeVisible(); // Clean up - await page.getByRole('button', { name: /^D2\s+Mine/ }).click(); + await page.getByRole('button', { name: /D2.*yours/i }).click(); await page.getByRole('button', { name: 'Cancel reservation' }).click(); }); - test('cancels a reservation and desk returns to Free', async ({ page }) => { - const targetDate = offsetDate(9); - const input = page.locator('input[type="date"]'); - await input.fill(targetDate); - await input.dispatchEvent('change'); + test('cancels a reservation and desk returns to free', async ({ page }) => { + await selectDate(page, 9); // Reserve - await page.getByRole('button', { name: 'D3 Free' }).click(); + await page.getByRole('button', { name: /D3.*free/i }).click(); await page.getByRole('button', { name: 'Confirm' }).click(); - await expect(page.getByRole('button', { name: /^D3\s+Mine/ })).toBeVisible(); + await expect(page.getByRole('button', { name: /D3.*yours/i })).toBeVisible(); // Cancel - await page.getByRole('button', { name: /^D3\s+Mine/ }).click(); - await expect(page.getByRole('heading', { name: 'Cancel reservation' })).toBeVisible(); + await page.getByRole('button', { name: /D3.*yours/i }).click(); + await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: 'Cancel reservation' }).click(); - await expect(page.getByRole('button', { name: 'D3 Free' })).toBeVisible(); + await expect(page.getByRole('button', { name: /D3.*free/i })).toBeVisible(); }); test('dismisses the reservation modal when clicking Cancel', async ({ page }) => { - await page.getByRole('button', { name: 'D4 Free' }).click(); - await expect(page.getByRole('heading', { name: 'Reserve desk' })).toBeVisible(); + await page.getByRole('button', { name: /D4.*free/i }).click(); + await expect(page.getByRole('dialog').getByText('Reserve desk')).toBeVisible(); - // Scope to the modal overlay to avoid matching Cancel buttons in My reservations - await page.locator('.fixed.inset-0').getByRole('button', { name: 'Cancel' }).click(); - await expect(page.getByRole('heading', { name: 'Reserve desk' })).not.toBeVisible(); + await page.getByRole('dialog').getByRole('button', { name: 'Cancel' }).click(); + await expect(page.getByRole('dialog')).not.toBeVisible(); }); });