Initial commit: TravelHub multi-tenant flight booking platform
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>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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-]+/);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user