// @ts-check const { test, expect } = require('@playwright/test'); const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}'); /** Log in as the admin user and land on /manage/credentials. */ async function loginAsAdmin(page) { await page.goto('/login'); await page.fill('#username', fixtures.admin_username); await page.fill('#password', fixtures.admin_password); await page.click('form[hx-post="/login/password"] button[type="submit"]'); await page.waitForURL('**/manage/credentials', { timeout: 5000 }); } /** Log in as a regular (non-admin) user. */ async function loginAsRegularUser(page) { await page.goto('/login'); await page.fill('#username', fixtures.login_username); await page.fill('#password', fixtures.login_password); await page.click('form[hx-post="/login/password"] button[type="submit"]'); await page.waitForURL('**/manage/credentials', { timeout: 5000 }); } test.describe('Admin pages', () => { // --------------------------------------------------------------- // 1 & 2. Auth guards // --------------------------------------------------------------- test.describe('Auth guard', () => { test('unauthenticated user visiting /admin/users is redirected to /login', async ({ page }) => { await page.goto('/admin/users'); await page.waitForURL('**/login', { timeout: 5000 }); expect(page.url()).toContain('/login'); }); test('non-admin logged-in user gets 403', async ({ page }) => { await loginAsRegularUser(page); const response = await page.goto('/admin/users'); expect(response.status()).toBe(403); }); }); // --------------------------------------------------------------- // 3 & 4 & 5. User list page // --------------------------------------------------------------- test.describe('User list page', () => { test.beforeEach(async ({ page }) => { await loginAsAdmin(page); await page.goto('/admin/users'); }); test('has correct page structure', async ({ page }) => { await expect(page.locator('h1')).toHaveText('Users'); await expect(page.locator('.admin-table thead th')).toHaveCount(6); await expect(page.locator('input[name="q"]')).toBeVisible(); await expect(page.locator('form[hx-post="/admin/invite"]')).toBeVisible(); }); test('table headers are correct', async ({ page }) => { const headers = page.locator('.admin-table thead th'); await expect(headers.nth(0)).toHaveText('Username'); await expect(headers.nth(1)).toHaveText('Name'); await expect(headers.nth(2)).toHaveText('Email'); await expect(headers.nth(3)).toHaveText('Groups'); await expect(headers.nth(4)).toHaveText('Status'); await expect(headers.nth(5)).toHaveText('Created'); }); test('shows seeded users', async ({ page }) => { const tableBody = page.locator('#user-table-body'); await expect(tableBody).toContainText('testuser'); await expect(tableBody).toContainText('adminuser'); }); test('search filters results', async ({ page }) => { const searchInput = page.locator('input[name="q"]'); await searchInput.fill('admin'); // Wait for htmx debounce (300ms) and response await expect(page.locator('#user-table-body')).toContainText('adminuser', { timeout: 5000 }); // Other users should be filtered out await expect(page.locator('#user-table-body')).not.toContainText('testuser', { timeout: 3000 }); }); }); // --------------------------------------------------------------- // 6. User detail page structure // --------------------------------------------------------------- test.describe('User detail page', () => { test.beforeEach(async ({ page }) => { await loginAsAdmin(page); await page.goto('/admin/users'); }); test('clicking a user link shows detail page', async ({ page }) => { await page.click(`a:has-text("adminuser")`); await page.waitForURL('**/admin/users/**', { timeout: 5000 }); await expect(page.locator('h1')).toHaveText('adminuser'); // Profile section await expect(page.locator('h2:has-text("Profile")')).toBeVisible(); await expect(page.locator('#given_name')).toBeVisible(); // Groups section await expect(page.locator('h2:has-text("Groups")')).toBeVisible(); await expect(page.locator('#groups')).toBeVisible(); // Credentials section await expect(page.locator('h2:has-text("Credentials")')).toBeVisible(); // Actions section await expect(page.locator('h2:has-text("Actions")')).toBeVisible(); }); }); // --------------------------------------------------------------- // 7. Profile update // --------------------------------------------------------------- test.describe('Profile update', () => { test('fill in profile fields and save', async ({ page }) => { await loginAsAdmin(page); await page.goto(`/admin/users/${fixtures.admin_userid}`); await page.fill('#given_name', 'Updated'); await page.fill('#family_name', 'Name'); await page.fill('#email', 'updated@example.com'); await page.click('section:has(h2:has-text("Profile")) button[type="submit"]'); const status = page.locator('#profile-status [role="status"]'); await expect(status).toBeVisible({ timeout: 5000 }); await expect(status).toContainText('Profile updated'); }); }); // --------------------------------------------------------------- // 8. Groups update // --------------------------------------------------------------- test.describe('Groups update', () => { test('change groups and save', async ({ page }) => { await loginAsAdmin(page); await page.goto(`/admin/users/${fixtures.admin_userid}`); await page.fill('#groups', 'admin, users, editors'); await page.click('section:has(h2:has-text("Groups")) button[type="submit"]'); const status = page.locator('#groups-status [role="status"]'); await expect(status).toBeVisible({ timeout: 5000 }); await expect(status).toContainText('Groups updated'); }); }); // --------------------------------------------------------------- // 9. Activate/deactivate toggle // --------------------------------------------------------------- test.describe('Activate/deactivate toggle', () => { test('deactivate then activate user', async ({ page }) => { await loginAsAdmin(page); // Use a non-admin user for this test await page.goto(`/admin/users/${fixtures.admin_userid}`); // Click deactivate await page.click('button:has-text("Deactivate user")'); const deactivatedStatus = page.locator('#actions-section [role="status"]'); await expect(deactivatedStatus).toBeVisible({ timeout: 5000 }); await expect(deactivatedStatus).toContainText('User deactivated'); // Now an activate button should appear await expect(page.locator('button:has-text("Activate user")')).toBeVisible(); // Click activate await page.click('button:has-text("Activate user")'); const activatedStatus = page.locator('#actions-section [role="status"]'); await expect(activatedStatus).toBeVisible({ timeout: 5000 }); await expect(activatedStatus).toContainText('User activated'); // Deactivate button should reappear await expect(page.locator('button:has-text("Deactivate user")')).toBeVisible(); }); }); // --------------------------------------------------------------- // 10. Create invite from user list // --------------------------------------------------------------- test.describe('Create invite', () => { test('fill username and submit to get invite URL', async ({ page }) => { await loginAsAdmin(page); await page.goto('/admin/users'); await page.fill('form[hx-post="/admin/invite"] input[name="username"]', 'inviteduser'); await page.click('form[hx-post="/admin/invite"] button[type="submit"]'); const inviteStatus = page.locator('#invite-status'); await expect(inviteStatus.locator('.invite-url')).toBeVisible({ timeout: 5000 }); const inviteUrl = await inviteStatus.locator('.invite-url').textContent(); expect(inviteUrl).toContain('/register/'); }); }); // --------------------------------------------------------------- // 11. Re-invite from user detail // --------------------------------------------------------------- test.describe('Re-invite from user detail', () => { test('generate invite link from user detail page', async ({ page }) => { await loginAsAdmin(page); await page.goto(`/admin/users/${fixtures.admin_userid}`); await page.click('button:has-text("Generate invite link")'); const inviteResult = page.locator('#invite-result'); await expect(inviteResult.locator('.invite-url')).toBeVisible({ timeout: 5000 }); const inviteUrl = await inviteResult.locator('.invite-url').textContent(); expect(inviteUrl).toContain('/register/'); }); }); // --------------------------------------------------------------- // 12. Delete user // --------------------------------------------------------------- test.describe('Delete user', () => { test('delete a user and verify redirect to user list', async ({ page }) => { await loginAsAdmin(page); // Use a disposable user seeded specifically for this test await page.goto(`/admin/users/${fixtures.disposable_userid}`); await expect(page.locator('h1')).toHaveText('disposableuser'); // Set up dialog handler before clicking delete page.on('dialog', async (dialog) => { await dialog.accept(); }); await page.click('button:has-text("Delete user")'); // htmx processes the HX-Redirect header and navigates to /admin/users await page.waitForURL('**/admin/users', { timeout: 5000 }); await expect(page.locator('h1')).toHaveText('Users'); // The deleted user should no longer appear in the list await expect(page.locator('#user-table-body')).not.toContainText('disposableuser'); }); }); });