test(e2e): update Playwright suite for Respellion UI

This commit is contained in:
2026-04-30 16:35:28 +02:00
parent 74a05253e7
commit 3a63ec8dcc
5 changed files with 81 additions and 82 deletions

View File

@@ -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,

View File

@@ -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 <span> 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();
});
});

View File

@@ -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');
});
});

View File

@@ -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();
}

View File

@@ -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 (D1D16) 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();
});
});