From 224ca77a9d5afcc409f9447680a59c9f2a3505df Mon Sep 17 00:00:00 2001 From: AI Agent Date: Sun, 22 Feb 2026 22:30:44 +0000 Subject: [PATCH] Add high-priority Playwright trips coverage (#42) Co-authored-by: Shaun Campbell Reviewed-on: https://cloud.campbellwireless.net/git/campbellwireless/trips/pulls/42 Co-authored-by: AI Agent Co-committed-by: AI Agent --- e2e/admin-auth-source.test.ts | 73 +++++++++++++++++++++++++++++++ e2e/admin-local-user-flow.test.ts | 49 +++++++++++++++++++++ e2e/auth-callback.test.ts | 25 +++++++++++ e2e/trip-planning.test.ts | 2 +- 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 e2e/admin-auth-source.test.ts create mode 100644 e2e/admin-local-user-flow.test.ts create mode 100644 e2e/auth-callback.test.ts diff --git a/e2e/admin-auth-source.test.ts b/e2e/admin-auth-source.test.ts new file mode 100644 index 0000000..cf9ed95 --- /dev/null +++ b/e2e/admin-auth-source.test.ts @@ -0,0 +1,73 @@ +import { test, expect, type Locator, type Page } from '@playwright/test'; +import { TEST_USERS } from './setup/test-users.js'; + +const LOGIN_URL = '/trips/login'; +const ADMIN_USERS_URL = '/trips/admin/users'; + +async function loginAsLocalUser(page: Page, username: string, password: string): Promise { + await page.goto(LOGIN_URL); + await page.fill('input[name="identifier"]', username); + await page.fill('input[name="password"]', password); + await page.click('button[type="submit"]:has-text("Sign in locally")'); + await page.waitForURL('**/trips/dashboard', { timeout: 15_000 }); +} + +function rowForUser(page: Page, username: string): Locator { + return page.getByRole('cell', { name: username }).locator('..'); +} + +test.beforeEach(async ({ context }) => { + await context.clearCookies(); +}); + +test('admin sees auth source and can set local password', async ({ page }) => { + const suffix = Date.now(); + const username = `e2e_auth_source_${suffix}`; + const fullName = `E2E Auth Source ${suffix}`; + const email = `e2e_auth_source_${suffix}@test.local`; + const password = `InitPass-${suffix}-123`; + + await loginAsLocalUser(page, TEST_USERS.admin.username, TEST_USERS.admin.password); + await page.goto(ADMIN_USERS_URL); + await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible(); + + const regularRow = rowForUser(page, TEST_USERS.regular.username); + await expect(regularRow.getByText('Local', { exact: true })).toBeVisible(); + + const adminRow = rowForUser(page, TEST_USERS.admin.username); + await expect(adminRow.getByText('Local', { exact: true })).toBeVisible(); + + await page.getByRole('button', { name: 'Add User' }).click(); + const addDialog = page.getByRole('dialog', { name: 'Add user' }); + await expect(addDialog).toBeVisible(); + await addDialog.locator('input[type="text"]').nth(0).fill(username); + await addDialog.locator('input[type="text"]').nth(1).fill(fullName); + await addDialog.locator('input[type="email"]').fill(email); + await addDialog.locator('input[type="password"]').nth(0).fill(password); + await addDialog.locator('input[type="password"]').nth(1).fill(password); + await addDialog.getByRole('button', { name: 'Create user' }).click(); + + const disposableUserRow = rowForUser(page, username); + await expect(disposableUserRow.getByText('Local', { exact: true })).toBeVisible(); + await disposableUserRow.getByRole('button', { name: 'Set local password' }).click(); + const dialog = page.getByRole('dialog', { name: 'Set local password' }); + await expect(dialog).toBeVisible(); + + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog.getByText('Password is required')).toBeVisible(); + + await dialog.locator('input[type="password"]').nth(0).fill('short'); + await dialog.locator('input[type="password"]').nth(1).fill('short'); + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog.getByText('Password must be at least 12 characters')).toBeVisible(); + + await dialog.locator('input[type="password"]').nth(0).fill('long-enough-password'); + await dialog.locator('input[type="password"]').nth(1).fill('long-enough-password-mismatch'); + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog.getByText('Passwords do not match')).toBeVisible(); + + await dialog.locator('input[type="password"]').nth(0).fill('local-password-123'); + await dialog.locator('input[type="password"]').nth(1).fill('local-password-123'); + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog).toBeHidden(); +}); diff --git a/e2e/admin-local-user-flow.test.ts b/e2e/admin-local-user-flow.test.ts new file mode 100644 index 0000000..3c7fd8b --- /dev/null +++ b/e2e/admin-local-user-flow.test.ts @@ -0,0 +1,49 @@ +import { test, expect, type Page } from '@playwright/test'; +import { TEST_USERS } from './setup/test-users.js'; + +const LOGIN_URL = '/trips/login'; +const ADMIN_USERS_URL = '/trips/admin/users'; +const DASHBOARD_URL = '/trips/dashboard'; + +async function loginAsLocalUser(page: Page, username: string, password: string): Promise { + await page.goto(LOGIN_URL); + await page.fill('input[name="identifier"]', username); + await page.fill('input[name="password"]', password); + await page.click('button[type="submit"]:has-text("Sign in locally")'); + await page.waitForURL(`**${DASHBOARD_URL}`, { timeout: 15_000 }); +} + +test.beforeEach(async ({ context }) => { + await context.clearCookies(); +}); + +test('admin can create a local user who can sign in', async ({ page }) => { + const suffix = Date.now(); + const username = `e2e_new_user_${suffix}`; + const fullName = `E2E New User ${suffix}`; + const email = `e2e_new_user_${suffix}@test.local`; + const password = `TempPass-${suffix}-123`; + + await loginAsLocalUser(page, TEST_USERS.admin.username, TEST_USERS.admin.password); + await page.goto(ADMIN_USERS_URL); + await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible(); + await page.getByRole('button', { name: 'Add User' }).click(); + + const addDialog = page.getByRole('dialog', { name: 'Add user' }); + await expect(addDialog).toBeVisible(); + await addDialog.locator('input[type="text"]').nth(0).fill(username); + await addDialog.locator('input[type="text"]').nth(1).fill(fullName); + await addDialog.locator('input[type="email"]').fill(email); + await addDialog.locator('input[type="password"]').nth(0).fill(password); + await addDialog.locator('input[type="password"]').nth(1).fill(password); + await addDialog.getByRole('button', { name: 'Create user' }).click(); + + await expect(page.getByRole('cell', { name: username, exact: true })).toBeVisible(); + + await page.context().clearCookies(); + await loginAsLocalUser(page, username, password); + await expect(page).toHaveURL(/\/trips\/dashboard/); + + await page.goto(ADMIN_USERS_URL); + await expect(page).toHaveURL(/\/trips\/dashboard/); +}); diff --git a/e2e/auth-callback.test.ts b/e2e/auth-callback.test.ts new file mode 100644 index 0000000..87302f8 --- /dev/null +++ b/e2e/auth-callback.test.ts @@ -0,0 +1,25 @@ +import { test, expect, type Page } from '@playwright/test'; +import { TEST_USERS } from './setup/test-users.js'; + +const LOGIN_URL = '/trips/login'; +const UPCOMING_URL = '/trips/trips/upcoming'; + +async function submitLocalLogin(page: Page, username: string, password: string): Promise { + await page.fill('input[name="identifier"]', username); + await page.fill('input[name="password"]', password); + await page.click('button[type="submit"]:has-text("Sign in locally")'); +} + +test.beforeEach(async ({ context }) => { + await context.clearCookies(); +}); + +test('redirects back to protected route after local login', async ({ page }) => { + await page.goto(UPCOMING_URL); + await expect(page).toHaveURL(/\/trips\/login/); + + await submitLocalLogin(page, TEST_USERS.regular.username, TEST_USERS.regular.password); + await page.waitForURL(/\/trips\/(trips\/upcoming|dashboard)/, { timeout: 15_000 }); + await expect(page).not.toHaveURL(/\/trips\/auth/); + await expect(page).not.toHaveURL(/\/trips\/login/); +}); diff --git a/e2e/trip-planning.test.ts b/e2e/trip-planning.test.ts index bc2f09f..8cafcef 100644 --- a/e2e/trip-planning.test.ts +++ b/e2e/trip-planning.test.ts @@ -150,7 +150,7 @@ test.beforeEach(async ({ context }) => { await context.clearCookies(); }); -test('regular user can plan a detailed future trip and see it in upcoming', async ({ page }) => { +test('regular user can plan a detailed future trip with no end date', async ({ page }) => { const startDate = formatDate(30); const tripName = `E2E Future Trip ${Date.now()}`; const tripDescription = 'Future trip created in Playwright e2e scenario';