Angular + Go + PostgreSQL sample with strict per-tenant isolation. - Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool) - Frontend: Angular 22 SPA (search, checkout, bookings, dashboard) - Database: schema, seed data and demo bookings migrations - Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e), plus a generated evidence report with PDF export - Security: whitebox VAPT report under security/ Secrets (.env, tokens, DB credentials) are git-ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import { expect, Locator, Page } from '@playwright/test';
|
|
|
|
export interface Traveller {
|
|
firstName: string;
|
|
lastName: string;
|
|
dateOfBirth?: string;
|
|
passportNumber?: string;
|
|
}
|
|
|
|
/** The booking form. */
|
|
export class CheckoutPage {
|
|
readonly travellerFieldsets: Locator;
|
|
readonly addTraveller: Locator;
|
|
readonly contactEmail: Locator;
|
|
readonly contactPhone: Locator;
|
|
readonly confirm: Locator;
|
|
readonly total: Locator;
|
|
readonly error: Locator;
|
|
readonly seatsLeft: Locator;
|
|
|
|
constructor(private readonly page: Page) {
|
|
this.travellerFieldsets = page.locator('.pax');
|
|
this.addTraveller = page.getByRole('button', { name: '+ Add traveller' });
|
|
this.contactEmail = page.locator('#contactEmail');
|
|
this.contactPhone = page.locator('#contactPhone');
|
|
this.confirm = page.getByRole('button', { name: /Confirm booking|Confirming/ });
|
|
this.total = page.locator('.fare-row.total strong');
|
|
this.error = page.locator('.alert-error');
|
|
this.seatsLeft = page.locator('.sum-list div', { hasText: 'Seats left' }).locator('dd');
|
|
}
|
|
|
|
async waitUntilReady(): Promise<void> {
|
|
await expect(this.travellerFieldsets.first()).toBeVisible();
|
|
}
|
|
|
|
async totalAmount(): Promise<number> {
|
|
const text = await this.total.innerText();
|
|
return Number(text.replace(/[^0-9.]/g, ''));
|
|
}
|
|
|
|
/** Fills one traveller block, adding a row first if necessary. */
|
|
async fillTraveller(index: number, traveller: Traveller): Promise<void> {
|
|
// Wait for each added row to render before deciding whether another is
|
|
// needed -- re-counting immediately races Angular and adds one too many.
|
|
let count = await this.travellerFieldsets.count();
|
|
while (count <= index) {
|
|
await this.addTraveller.click();
|
|
count += 1;
|
|
await expect(this.travellerFieldsets).toHaveCount(count);
|
|
}
|
|
await this.page.locator(`#first-${index}`).fill(traveller.firstName);
|
|
await this.page.locator(`#last-${index}`).fill(traveller.lastName);
|
|
if (traveller.dateOfBirth) {
|
|
await this.page.locator(`#dob-${index}`).fill(traveller.dateOfBirth);
|
|
}
|
|
if (traveller.passportNumber) {
|
|
await this.page.locator(`#passport-${index}`).fill(traveller.passportNumber);
|
|
}
|
|
}
|
|
|
|
async fillTravellers(travellers: Traveller[]): Promise<void> {
|
|
for (let i = 0; i < travellers.length; i++) {
|
|
await this.fillTraveller(i, travellers[i]);
|
|
}
|
|
}
|
|
|
|
async removeTraveller(index: number): Promise<void> {
|
|
await this.travellerFieldsets.nth(index).getByRole('button', { name: 'Remove' }).click();
|
|
}
|
|
|
|
/** Confirms and waits for the confirmation screen. */
|
|
async confirmBooking(): Promise<void> {
|
|
await this.confirm.click();
|
|
await this.page.waitForURL(/\/bookings\/[0-9a-f-]+/);
|
|
}
|
|
}
|