Skip to main content

How to Test Travel Booking Flows (Hotels, Flights, Cancellations)

Short answer

Travel E2E fails when tests pick calendar dates in the past, ignore timezone on check-in midnight, or assert confirmation banners while inventory holds expired. Seed availability per runId, freeze clock/timezone in Playwright, probe booking PNR/status rows, and cover cancellation refunds asynchronously—not just search result card counts.

Part of Testing Guides by industry.

Who this is for

OTA, corporate travel, vacation rental, and airline retail teams shipping hotel/flight search, hold-to-book, multi-room carts, and cancellation flows. Pain points: midnight boundary bugs, hold timers, GDS sandbox limits, and refund webhooks lagging UI.

Why travel booking needs different Arrange/Assert

LayerPitfallE2E fix
Inventory holdsCart expires during checkoutSeed long hold or freeze timer in test env
TimezoneCheck-in date shifts UTCtimezoneId + fixed clock in Playwright
Fare rulesNon-refundable fare in cartSeed fare class metadata; probe refund policy
Parallel CISame room night booked twicerunId on inventory blocks
CancellationUI cancelled; GDS still activeProbe supplier status + refund row
Multi-cityLeg order wrongProbe itinerary segments array

Confirmation emails and PDF tickets are supplements—booking row status is authoritative.

Complexity map

ScenarioEdge caseWhy tests breakApproach
Date picker past datesDefault "today" in CI TZInvalid searchFreeze clock; seed future check-in
Midnight check-inProperty local TZOff-by-one nighttimezoneId: 'America/New_York'
Inventory hold TTL15-min hold expiresSold out at payExtend hold in E2E env; probe hold_id
Double bookingTwo workers same roomOne succeeds one 409Per-run room block seed
Flight + hotel bundlePartial failure rollbackOrphan hotelProbe both legs or neither
Seat mapDynamic seat takenRandom failSeed locked seats for runId
Fare changePrice drift during checkoutStale totalProbe quoted vs charged amount
Cancellation windowFree cancel until 6pmWrong refundSeed policy; probe refund_eligible
Non-refundableCancel blockedUI allows anywayProbe status remains confirmed
Supplier asyncGDS pending → confirmedAssert too earlyexpect.poll booking status
Multi-roomDifferent guest namesWrong assignmentProbe room_guest rows
Currency displayFX vs charge currencyWrong symbolProbe settlement currency
Loyalty pointsPost-book accrual lagMissing points UIPoll loyalty ledger probe
Timezone DSTSpring forward nightInvalid local timeUse UTC instants in seed
Waitlist / overbookUpgrade pathFlaky availabilitySeed deterministic inventory API

Seed pattern (inventory + booking probe)

// app/api/test/seed-travel-inventory/route.ts
export async function POST(req: Request) {
if (process.env.E2E_TEST_MODE !== 'true') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const { runId, propertyId, checkIn, checkOut, rooms = 1 } = await req.json();
const blockId = `hold-${runId}`;
await inventory.createHold({
blockId,
propertyId,
checkIn, // ISO instant in property TZ
checkOut,
rooms,
run_id: runId,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1h for E2E
});
return NextResponse.json({ blockId, runId, checkIn, checkOut });
}
// app/api/test/probe-booking/route.ts
export async function GET(req: Request) {
if (process.env.E2E_TEST_MODE !== 'true') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const runId = new URL(req.url).searchParams.get('runId');
const booking = await db.bookings.findFirst({ where: { run_id: runId } });
return NextResponse.json({
runId,
status: booking?.status ?? 'none',
pnr: booking?.pnr ?? null,
refundStatus: booking?.refund_status ?? null,
});
}

Playwright spec — hotel book and cancel

// @Scenario: travel/hotel-refundable-cancel
import { test, expect } from '../fixtures/run';

test.use({
timezoneId: 'America/Los_Angeles',
locale: 'en-US',
});

test('refundable hotel books then cancels with refund pending', async ({ page, request, runId }) => {
await page.clock.install({ time: new Date('2026-08-01T10:00:00-07:00') });

const checkIn = '2026-08-15';
const checkOut = '2026-08-17';
await request.post('/api/test/seed-travel-inventory', {
data: { runId, propertyId: 'hotel-seed-1', checkIn, checkOut },
});

await page.goto(`/hotels/hotel-seed-1?checkIn=${checkIn}&checkOut=${checkOut}`);
await page.getByRole('button', { name: 'Reserve' }).click();
await page.getByLabel('Guest name').fill(`Guest ${runId}`);
await page.getByRole('button', { name: 'Confirm booking' }).click();

await expect.poll(async () => {
const res = await request.get(`/api/test/probe-booking?runId=${runId}`);
return (await res.json()).status;
}, { timeout: 60_000 }).toBe('confirmed');

await page.getByRole('button', { name: 'Cancel reservation' }).click();
await page.getByRole('button', { name: 'Confirm cancellation' }).click();

await expect.poll(async () => {
const res = await request.get(`/api/test/probe-booking?runId=${runId}`);
const body = await res.json();
return body.status === 'cancelled' && body.refundStatus === 'pending';
}, { timeout: 45_000 }).toBe(true);
});

See date, time, and timezones for clock patterns and Stripe payments when checkout uses card holds.

Flights-specific notes

  • Seed fixed itinerary in sandbox GDS or stub supplier API with runId PNR prefix.
  • Assert segment order and baggage fare basis via probe JSON—not only airport codes in UI.
  • 24h DOT cancel scenarios need separate policy seed from hotel refundable rules.

Hotels and vacation rentals

  • Distinguish instant book vs request to book—probe host approval state.
  • Cleaning fees and tax lines must match probe line_items total.
  • Channel manager sync lag: poll availability probe after seed, similar to Algolia index sync.

TestChimp workflow

Tag specs // @Scenario: travel/hotel-cancel for markdown linkage. /testchimp plan helps author cancellation edge cases (non-refundable, partial refund). /testchimp test keeps date and probe Assert when calendar components refactor. Use TrueCoverage on search → book → cancel funnels.

Anti-patterns

Anti-patternWhy it failsBetter approach
Hard-coded "next Friday" datesBecomes past in CIFrozen clock + ISO seed dates
UI confirmation onlySupplier pendingProbe booking status
Shared sandbox PNRParallel collisionrunId-scoped inventory
Ignore property TZWrong night counttimezoneId in test.use
Skip cancellation specsRevenue leakage on refundsProbe refund_status
Assert email PDFAsync delivery flakeProbe booking row first
Real GDS in every PRCost + flakeStub supplier in CI; nightly real
No hold extensionExpired mid-checkoutE2E env longer TTL

External references

Example scenario

Situation: Traveler books refundable hotel for two nights then cancels before deadline.

Expected outcome: Booking confirmed then cancelled; refund initiated; inventory released for dates.

Why UI-only automation breaks: Cancellation modal shows success while supplier PNR still active—customer charged.

  1. Arrange: Seed inventory hold and refundable rate for runId with fixed check-in dates.
  2. Act: Search property; complete guest details; confirm; cancel from trips page.
  3. Assert: Poll probe-booking status cancelled + refund pending; optional inventory probe shows availability restored.

TestChimp workflow: // @Scenario: links hotel cancel spec to markdown; /testchimp test preserves timezone clock and probe Assert on calendar refactors.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Frequently asked questions

How to pick stable hotel dates in CI?

Freeze Playwright clock and seed check-in/out ISO strings relative to frozen now. Never use bare new Date() in specs without clock.install.

Property timezone vs user browser TZ?

Set timezoneId to property region for check-in midnight cases. Probe stored instants in UTC with property TZ metadata.

How to test inventory hold expiry?

Advance clock past hold TTL in dedicated scenario; expect sold-out or re-hold flow. Probe hold row deleted or expired.

Flight sandbox without real GDS costs?

Stub supplier API returning runId PNR in CI; run one nightly job against sandbox credentials if required.

Cancellation refund async?

Poll probe refund_status pending → completed like webhook patterns. UI refund timeline is supplementary.

Multi-room different policies?

Seed mixed refundable/non-refundable rooms; probe per-room cancellation outcome—not single banner.

Bundle flight+hotel partial failure?

Probe transactional rollback: neither confirmed or compensating cancel on succeeded leg. Critical scenario for markdown plan.

TestChimp with travel flows?

/testchimp plan authors hold/cancel matrices; // @Scenario links specs; TrueCoverage prioritizes book and cancel paths users hit in ExploreChimp.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo