porchlight/tests/e2e/admin.spec.js
2026-02-19 14:31:41 +01:00

238 lines
9.9 KiB
JavaScript

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