Initial commit

This commit is contained in:
Robert van Diest
2026-03-24 20:13:07 +01:00
commit 1301a01d6d
123 changed files with 7642 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test';
import { loginAsAdmin } from './helpers';
test.describe('Admin portal', () => {
test.beforeEach(async ({ page }) => {
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('navigates to the admin portal', async ({ page }) => {
await page.getByRole('button', { name: 'Admin portal' }).click();
await page.waitForURL('/admin');
await expect(page.getByRole('heading', { name: 'Admin Portal' })).toBeVisible();
await expect(page.getByText('Manage user accounts')).toBeVisible();
});
test('admin portal lists the admin account under Approved accounts', async ({ page }) => {
await page.goto('/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();
});
test('admin account has the Admin badge and no Make admin button', async ({ page }) => {
await page.goto('/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();
});
test('Back to planner link returns to the planner', async ({ page }) => {
await page.goto('/admin');
await page.getByRole('button', { name: '← Back to planner' }).click();
await page.waitForURL('/');
await expect(page.getByText('Reserve your workspace up to 2 weeks ahead')).toBeVisible();
});
});

View File

@@ -0,0 +1,63 @@
import { test, expect } from '@playwright/test';
import { ADMIN_EMAIL, ADMIN_PASSWORD } from './helpers';
test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
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.getByPlaceholder('••••••••')).toBeVisible();
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('••••••••').fill(ADMIN_PASSWORD);
await page.locator('button[type="submit"]').click();
await expect(page.getByText('Reserve your workspace up to 2 weeks ahead')).toBeVisible();
});
test('shows an error for an unknown email', async ({ page }) => {
await page.getByPlaceholder('jane@company.com').fill('nobody@example.com');
await page.getByPlaceholder('••••••••').fill('anything');
await page.locator('button[type="submit"]').click();
await expect(page.getByText(/invalid email or password/i)).toBeVisible();
});
test('shows an error for a wrong password', async ({ page }) => {
await page.getByPlaceholder('jane@company.com').fill(ADMIN_EMAIL);
await page.getByPlaceholder('••••••••').fill('wrongpassword');
await page.locator('button[type="submit"]').click();
await expect(page.getByText(/invalid email or password/i)).toBeVisible();
});
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.getByPlaceholder('Jane Smith').fill('Test User');
await page.getByPlaceholder('jane@company.com').fill(`testuser+${Date.now()}@example.com`);
await page.getByPlaceholder('••••••••').fill('Test@1234');
await page.locator('button[type="submit"]').click();
await expect(page.getByText('Account pending approval')).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('••••••••').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');
});
});

View File

@@ -0,0 +1,25 @@
import { type Page } from '@playwright/test';
export const ADMIN_EMAIL = 'admin@randall.local';
export const ADMIN_PASSWORD = 'Admin@123';
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('••••••••').fill(password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('/');
await page.getByText('Reserve your workspace up to 2 weeks ahead').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 */
export function offsetDate(days: number): string {
const d = new Date();
d.setDate(d.getDate() + days);
return d.toISOString().split('T')[0];
}

View File

@@ -0,0 +1,100 @@
import { test, expect } from '@playwright/test';
import { loginAsAdmin, offsetDate } from './helpers';
test.describe('Planner', () => {
test.beforeEach(async ({ page }) => {
await loginAsAdmin(page);
});
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();
// All 16 desk buttons contain a desk label (D1D16) in their text content
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);
await expect(input).toHaveAttribute('min', today);
await expect(input).toHaveAttribute('max', max);
});
test('previous-day button is disabled when on today', async ({ page }) => {
await expect(page.getByRole('button', { name: '←' })).toBeDisabled();
});
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 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');
await page.getByRole('button', { name: 'D1 Free' }).click();
await expect(page.getByRole('heading', { name: 'Reserve desk' })).toBeVisible();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByRole('button', { name: /^D1\s+Mine/ })).toBeVisible();
// Clean up
await page.getByRole('button', { name: /^D1\s+Mine/ }).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 page.getByRole('button', { name: 'D2 Free' }).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();
// Clean up
await page.getByRole('button', { name: /^D2\s+Mine/ }).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');
// Reserve
await page.getByRole('button', { name: 'D3 Free' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByRole('button', { name: /^D3\s+Mine/ })).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: 'Cancel reservation' }).click();
await expect(page.getByRole('button', { name: 'D3 Free' })).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();
// 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();
});
});