// booking.mjs — a stranger books, in a real browser, with NO SCRIPT. // // Driven by TestBookingInARealBrowser, which starts the app, publishes a // page, and calls this three times: // // hold JS off, no cookie: pick a day, // a slot, type a name and an // address, read "Check your mail". // confirm JS off, no cookie: open the // mailed link, see ONE button, // press it, see Reschedule. // pane // JS on, signed in: type an // availability sentence and watch // the pane fill in from the server. // // Go holds the mail recorder, which is why the hold and the confirm are // separate invocations: only the Go side can read the link out of the // mail that the hold sent. import { chromium } from 'playwright'; const [mode, origin, a, b] = process.argv.slice(2); if (!mode || !origin) { console.error('usage: booking.mjs hold|confirm|pane ...'); process.exit(2); } const failures = []; function check(ok, what) { if (ok) return; failures.push(what); console.error('FAIL: ' + what); } const browser = await chromium.launch(); async function contextWith(javaScriptEnabled, cookie) { const ctx = await browser.newContext({ javaScriptEnabled }); if (cookie) { await ctx.addCookies([{ name: cookie.name, value: cookie.value, url: origin, httpOnly: true }]); } return ctx; } if (mode === 'hold') { const ctx = await contextWith(false); const page = await ctx.newPage(); await page.goto(origin + a); check(await page.locator('[cal-book-days] a').count() > 0, 'the page lists days with free slots'); await page.locator('[cal-book-days] a').first().click(); const slots = page.locator('[cal-book-slot]'); check(await slots.count() > 0, 'the day page lists slots'); // The slot's painted size: a button the size of nothing is a button // nobody can press. const rect = await slots.first().boundingBox(); check(rect !== null && rect.width > 30 && rect.height > 20, 'a slot button has real painted size (got ' + JSON.stringify(rect) + ')'); await slots.first().click(); check(await page.locator('[cal-book-details]').isVisible(), 'picking a slot shows the details form'); await page.fill('#name', 'Doc'); await page.fill('#address', 'doc@tito.io'); await page.fill('#note', 'From a real browser, with no script.'); await Promise.all([ page.waitForNavigation(), page.locator('[cal-book-details] button[type=submit]').click(), ]); const body = await page.locator('body').innerText(); check(body.includes('Check your mail'), 'the hold lands on "Check your mail"'); check(body.includes('doc@tito.io'), 'the page echoes the address the mail went to'); await ctx.close(); } if (mode === 'confirm') { const ctx = await contextWith(false); const page = await ctx.newPage(); await page.goto(origin + a); // The page's own forms, not the shell's theme switcher: exactly one // POST to /confirm, and no other way to change anything. const buttons = page.locator('form[action$="/confirm"] button[type=submit]'); check(await buttons.count() === 1, 'the mailed link shows exactly one confirm button'); check((await buttons.first().innerText()).includes('Confirm'), 'and it says Confirm'); check(await page.locator('form[action*="/book/m/"]').count() === 1, 'the hold page has no other booking form — nothing else can change on a GET'); await Promise.all([page.waitForNavigation(), buttons.first().click()]); const body = await page.locator('body').innerText(); check(body.includes('Reschedule'), 'pressing Confirm lands on the manage page'); check(body.includes('Add to calendar'), 'the manage page offers the ICS'); await ctx.close(); } if (mode === 'pane') { const ctx = await contextWith(true, { name: a, value: b }); const page = await ctx.newPage(); const errors = []; page.on('pageerror', (e) => errors.push(String(e))); await page.goto(origin + '/settings/booking/new', { waitUntil: 'networkidle' }); await page.fill('[cal-book-text]', 'tues 12-2, 5-7'); await page.locator('[cal-book-pane]').getByText('Tuesday 12:00').waitFor({ timeout: 5000 }); const pane = await page.locator('[cal-book-pane]').innerText(); check(pane.includes('Tuesday 12:00–14:00, 17:00–19:00'), 'the pane describes both windows (got ' + pane + ')'); check(errors.length === 0, 'no page errors (' + errors.join('; ') + ')'); await ctx.close(); } await browser.close(); if (failures.length > 0) { console.error(failures.length + ' check(s) failed'); process.exit(1); } console.log('booking ' + mode + ': all checks passed');