oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1// booking.mjs — a stranger books, in a real browser, with NO SCRIPT.
2//
3// Driven by TestBookingInARealBrowser, which starts the app, publishes a
4// page, and calls this three times:
5//
6// hold <origin> <publicPath> JS off, no cookie: pick a day,
7// a slot, type a name and an
8// address, read "Check your mail".
9// confirm <origin> <manageLink> JS off, no cookie: open the
10// mailed link, see ONE button,
11// press it, see Reschedule.
12// pane <origin> <cookieName> <cookieValue>
13// JS on, signed in: type an
14// availability sentence and watch
15// the pane fill in from the server.
16//
17// Go holds the mail recorder, which is why the hold and the confirm are
18// separate invocations: only the Go side can read the link out of the
19// mail that the hold sent.
20import { chromium } from 'playwright';
21
22const [mode, origin, a, b] = process.argv.slice(2);
23if (!mode || !origin) {
24 console.error('usage: booking.mjs hold|confirm|pane <origin> ...');
25 process.exit(2);
26}
27
28const failures = [];
29function check(ok, what) {
30 if (ok) return;
31 failures.push(what);
32 console.error('FAIL: ' + what);
33}
34
35const browser = await chromium.launch();
36
37async function contextWith(javaScriptEnabled, cookie) {
38 const ctx = await browser.newContext({ javaScriptEnabled });
39 if (cookie) {
40 await ctx.addCookies([{ name: cookie.name, value: cookie.value, url: origin, httpOnly: true }]);
41 }
42 return ctx;
43}
44
45if (mode === 'hold') {
46 const ctx = await contextWith(false);
47 const page = await ctx.newPage();
48 await page.goto(origin + a);
49 check(await page.locator('[cal-book-days] a').count() > 0, 'the page lists days with free slots');
50
51 await page.locator('[cal-book-days] a').first().click();
52 const slots = page.locator('[cal-book-slot]');
53 check(await slots.count() > 0, 'the day page lists slots');
54
55 // The slot's painted size: a button the size of nothing is a button
56 // nobody can press.
57 const rect = await slots.first().boundingBox();
58 check(rect !== null && rect.width > 30 && rect.height > 20,
59 'a slot button has real painted size (got ' + JSON.stringify(rect) + ')');
60
61 await slots.first().click();
62 check(await page.locator('[cal-book-details]').isVisible(), 'picking a slot shows the details form');
63
64 await page.fill('#name', 'Doc');
65 await page.fill('#address', 'doc@tito.io');
66 await page.fill('#note', 'From a real browser, with no script.');
67 await Promise.all([
68 page.waitForNavigation(),
69 page.locator('[cal-book-details] button[type=submit]').click(),
70 ]);
71 const body = await page.locator('body').innerText();
72 check(body.includes('Check your mail'), 'the hold lands on "Check your mail"');
73 check(body.includes('doc@tito.io'), 'the page echoes the address the mail went to');
74 await ctx.close();
75}
76
77if (mode === 'confirm') {
78 const ctx = await contextWith(false);
79 const page = await ctx.newPage();
80 await page.goto(origin + a);
81 // The page's own forms, not the shell's theme switcher: exactly one
82 // POST to /confirm, and no other way to change anything.
83 const buttons = page.locator('form[action$="/confirm"] button[type=submit]');
84 check(await buttons.count() === 1, 'the mailed link shows exactly one confirm button');
85 check((await buttons.first().innerText()).includes('Confirm'), 'and it says Confirm');
86 check(await page.locator('form[action*="/book/m/"]').count() === 1,
87 'the hold page has no other booking form — nothing else can change on a GET');
88
89 await Promise.all([page.waitForNavigation(), buttons.first().click()]);
90 const body = await page.locator('body').innerText();
91 check(body.includes('Reschedule'), 'pressing Confirm lands on the manage page');
92 check(body.includes('Add to calendar'), 'the manage page offers the ICS');
93 await ctx.close();
94}
95
96if (mode === 'pane') {
97 const ctx = await contextWith(true, { name: a, value: b });
98 const page = await ctx.newPage();
99 const errors = [];
100 page.on('pageerror', (e) => errors.push(String(e)));
101 await page.goto(origin + '/settings/booking/new', { waitUntil: 'networkidle' });
102 await page.fill('[cal-book-text]', 'tues 12-2, 5-7');
103 await page.locator('[cal-book-pane]').getByText('Tuesday 12:00').waitFor({ timeout: 5000 });
104 const pane = await page.locator('[cal-book-pane]').innerText();
105 check(pane.includes('Tuesday 12:00–14:00, 17:00–19:00'), 'the pane describes both windows (got ' + pane + ')');
106 check(errors.length === 0, 'no page errors (' + errors.join('; ') + ')');
107 await ctx.close();
108}
109
110await browser.close();
111if (failures.length > 0) {
112 console.error(failures.length + ' check(s) failed');
113 process.exit(1);
114}
115console.log('booking ' + mode + ': all checks passed');
116