e2e: add medium-priority Playwright coverage (#44)
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m29s

Co-authored-by: Shaun Campbell <shaun@campbellwireless.net>
Reviewed-on: #44
Co-authored-by: AI Agent <ai-agent@campbellwireless.net>
Co-committed-by: AI Agent <ai-agent@campbellwireless.net>
This commit was merged in pull request #44.
This commit is contained in:
2026-02-23 00:12:24 +00:00
committed by shaun
parent 224ca77a9d
commit f75465e7f1
6 changed files with 420 additions and 0 deletions

159
e2e/helpers/trip.ts Normal file
View File

@@ -0,0 +1,159 @@
import { expect, type Page } from '@playwright/test';
const NEW_TRIP_URL = '/trips/trips/new';
function escapeRegex(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function uniqueSuffix(): string {
return `${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
export async function createTrip(
page: Page,
values: { name: string; startDate?: string; description?: string }
): Promise<{ tripUrl: string; tripId: string }> {
await page.goto(NEW_TRIP_URL);
await expect(page.getByRole('heading', { name: 'Plan New Trip' })).toBeVisible();
if (values.startDate) {
await page.getByLabel('Start date').fill(values.startDate);
} else {
await page.getByRole('checkbox', { name: "I don't know yet" }).first().check();
}
if (values.description) {
await page.getByLabel('Description').fill(values.description);
}
await page.locator('input[name="name"]').fill(values.name);
await page.getByRole('button', { name: 'Save' }).click();
await expect(page).toHaveURL(/\/trips\/trips\/(?!new$)[^/?#]+$/, { timeout: 15_000 });
const tripUrl = page.url();
const tripId = tripUrl.split('/').pop() ?? '';
return { tripUrl, tripId };
}
export async function openAddToTripMenuItem(page: Page, label: string): Promise<void> {
const addToTripButton = page.getByRole('button', { name: /^Add to trip$/i });
if (await addToTripButton.isVisible().catch(() => false)) {
await addToTripButton.click();
await page.getByRole('button', { name: label, exact: true }).click();
return;
}
// In the welcome state, button names include label + subtitle.
await page
.getByRole('button', { name: new RegExp(`^${escapeRegex(label)}\\b`, 'i') })
.first()
.click();
}
export async function openAddTraveller(page: Page): Promise<void> {
const existingDialog = page.getByRole('dialog', { name: 'Add traveller' });
if (await existingDialog.isVisible().catch(() => false)) return;
const addToTripButton = page.getByRole('button', { name: /^Add to trip$/i });
if (await addToTripButton.isVisible().catch(() => false)) {
await addToTripButton.click();
await page.getByRole('button', { name: 'Travellers', exact: true }).click();
return;
}
await page.getByRole('button', { name: /^Who's travelling\?/ }).click();
}
export async function addTraveller(
page: Page,
values: { firstName: string; lastName: string; email?: string }
): Promise<void> {
await openAddTraveller(page);
const dialog = page.getByRole('dialog', { name: 'Add traveller' });
await expect(dialog).toBeVisible();
const addNewButton = dialog.getByRole('button', { name: 'Add someone new' });
if (await addNewButton.isVisible().catch(() => false)) {
await addNewButton.click();
}
await dialog.getByLabel('First name', { exact: false }).fill(values.firstName);
await dialog.getByLabel('Last name', { exact: false }).fill(values.lastName);
if (values.email) {
await dialog.getByLabel(/Email/).fill(values.email);
}
await dialog.getByRole('button', { name: 'Add traveller' }).click();
await expect(dialog).toBeHidden();
}
export async function addFlight(
page: Page,
values: {
departureDate: string;
airlineCode: string;
flightNumber: string;
departureAirport: string;
arrivalAirport: string;
}
): Promise<void> {
await openAddToTripMenuItem(page, 'Transportation');
const dialog = page.getByRole('dialog', { name: 'Add transportation' });
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: /Flight/ }).click();
const form = dialog.locator('form');
await expect(form.locator('input[name="segments[0][departure_date]"]')).toBeVisible();
await form.locator('input[name="segments[0][departure_date]"]').fill(values.departureDate);
await form.getByPlaceholder('Search airline or enter code').fill(values.airlineCode);
await form.locator('input[name="segments[0][flight_number]"]').fill(values.flightNumber);
await form.getByPlaceholder('Code or search').nth(0).fill(values.departureAirport);
await form.getByPlaceholder('Code or search').nth(1).fill(values.arrivalAirport);
await form.getByRole('button', { name: 'Add transportation' }).click();
await expect(dialog).toBeHidden();
}
export async function addLodging(
page: Page,
values: { name: string; guestNames?: string[] }
): Promise<void> {
await openAddToTripMenuItem(page, 'Lodgings');
const dialog = page.getByRole('dialog', { name: 'Add lodging' });
await expect(dialog).toBeVisible();
await dialog.getByLabel('Name', { exact: false }).fill(values.name);
for (const guestName of values.guestNames ?? []) {
await dialog.getByRole('checkbox', { name: guestName }).check();
}
await dialog.getByRole('button', { name: 'Add lodging' }).click();
await expect(dialog).toBeHidden();
}
export async function addPackingList(
page: Page,
values: { name: string; items: string[] }
): Promise<void> {
await openAddToTripMenuItem(page, 'Packing List');
const dialog = page
.locator('[role="dialog"]')
.filter({ has: page.getByRole('heading', { name: /Packing list/i }) })
.first();
await expect(dialog).toBeVisible();
await dialog.getByLabel('Name', { exact: false }).fill(values.name);
const itemInputs = dialog.getByPlaceholder('Item');
await itemInputs.first().fill(values.items[0]);
for (const item of values.items.slice(1)) {
await dialog.getByRole('button', { name: 'Add item' }).click();
await itemInputs.last().fill(item);
}
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
await expect(dialog).toBeHidden();
}
export async function addActivity(page: Page, values: { name: string }): Promise<void> {
await openAddToTripMenuItem(page, 'Attractions & Activities');
const dialog = page
.locator('[role="dialog"]')
.filter({ has: page.getByRole('heading', { name: /Attraction & activity/i }) })
.first();
await expect(dialog).toBeVisible();
await dialog.getByLabel('Name', { exact: false }).fill(values.name);
await dialog.getByRole('button', { name: 'Add' }).click();
await expect(dialog).toBeHidden();
}