oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

1// quickadd.mjs — the quick-add box in a real browser, both ways.
2//
3// Driven by TestQuickAddInARealBrowser, which starts the app, signs in,
4// and passes this script the origin and a session cookie. Playwright
5// rather than chromedp: chromedp does not drive in this environment at
6// all (it starts Chromium and parks at about:blank until the Go test
7// times out), and a gate that cannot run is not a gate.
8//
9// Five passes, and the scriptless ones matter most:
10//
11// 1. WITH script — type, and the pane fills in from the server.
12// 2. WITHOUT script — the same box still opens, still submits, and
13// still creates the event.
14// 3. WITH script — Enter adds it, Shift+Enter does not.
15// 4. WITHOUT script — the Enter hint is absent, because the shortcut
16// does not exist there and a page must not promise one.
17// 5. WITH script — a date range reads as a run of days, and the pane
18// names the last one.
19//
20// The scriptless passes are the only way to prove the floor is real.
21// Every other test in this repo asks Go what Go produced; these ask a
22// browser with JavaScript switched off whether the feature still works.
23import { chromium } from 'playwright';
24
25const [origin, cookieName, cookieValue] = process.argv.slice(2);
26if (!origin || !cookieName || !cookieValue) {
27 console.error('usage: quickadd.mjs <origin> <cookieName> <cookieValue>');
28 process.exit(2);
29}
30
31const failures = [];
32function check(ok, what) {
33 if (ok) return;
34 failures.push(what);
35 console.error('FAIL: ' + what);
36}
37
38const SENTENCE = 'Board meeting with doc@tito.io tomorrow at 3pm in McCloskeys';
39
40const browser = await chromium.launch();
41
42async function contextWith(javaScriptEnabled) {
43 const ctx = await browser.newContext({ javaScriptEnabled });
44 await ctx.addCookies([
45 { name: cookieName, value: cookieValue, url: origin, httpOnly: true },
46 ]);
47 return ctx;
48}
49
50// ── 1. With script: the pane fills in as you type ────────────────────
51{
52 const ctx = await contextWith(true);
53 const page = await ctx.newPage();
54
55 const errors = [];
56 page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
57 page.on('pageerror', (e) => errors.push(String(e)));
58
59 await page.goto(origin + '/today', { waitUntil: 'networkidle' });
60
61 const box = page.locator('[cal-quick]');
62 check(await box.count() === 1, 'the quick add box is on the day page');
63
64 // Closed, the form must not be reachable — a popover that is merely
65 // transparent is still in the tab order.
66 check(!(await page.locator('[cal-quick-text]').isVisible()),
67 'the textarea is hidden until the box is opened');
68
69 await page.locator('[cal-quick] > summary').click();
70 check(await page.locator('[cal-quick-text]').isVisible(),
71 'clicking the box opens it');
72
73 // The crossing from computed value to PAINTED output: the popover has
74 // to have real size and sit inside the viewport. A panel positioned
75 // off the side of the page passes every string assertion in the Go
76 // suite and is invisible to a person.
77 const rect = await page.locator('[cal-quick-form]').boundingBox();
78 check(rect !== null && rect.width > 200 && rect.height > 80,
79 'the popover has real painted size (got ' + JSON.stringify(rect) + ')');
80 const vw = page.viewportSize().width;
81 check(rect !== null && rect.x >= 0 && rect.x + rect.width <= vw + 1,
82 'the popover sits inside the viewport (got ' + JSON.stringify(rect) + ')');
83
84 await page.locator('[cal-quick-text]').fill(SENTENCE);
85
86 // The pane is filled by the SERVER. Waiting for its text is waiting
87 // for the round trip, which is the thing under test.
88 await page.locator('[cal-quick-pane]').getByText('McCloskeys').waitFor({ timeout: 5000 });
89
90 const pane = await page.locator('[cal-quick-pane]').innerText();
91 check(pane.includes('Board meeting'), 'the pane shows the title it parsed');
92 check(pane.includes('McCloskeys'), 'the pane shows the place');
93 check(pane.includes('doc@tito.io'), 'the pane shows the attendee');
94 check(pane.includes('15:00'), 'the pane resolved 3pm to 15:00, not 03:00');
95 check(/not emailed/i.test(pane), 'the pane says nobody is emailed');
96
97 check(errors.length === 0, 'no console errors (' + errors.join('; ') + ')');
98 await ctx.close();
99}
100
101// ── 2. Without script: the same box still works ──────────────────────
102{
103 const ctx = await contextWith(false);
104 const page = await ctx.newPage();
105 await page.goto(origin + '/today');
106
107 // <details> is native, so it opens with no JavaScript at all.
108 await page.locator('[cal-quick] > summary').click();
109 check(await page.locator('[cal-quick-text]').isVisible(),
110 'the box opens with JavaScript disabled');
111
112 await page.locator('[cal-quick-text]').fill('Scriptless lunch tomorrow at 1pm in Fumbally');
113 await Promise.all([
114 page.waitForNavigation(),
115 page.locator('[cal-quick-form] button[type=submit]').click(),
116 ]);
117
118 check(/\/events\/\d+$/.test(page.url()),
119 'submitting with no script lands on the new event (got ' + page.url() + ')');
120 const body = await page.locator('body').innerText();
121 check(body.includes('Scriptless lunch'), 'the scriptless event kept its title');
122 check(body.includes('Fumbally'), 'the scriptless event kept its place');
123 await ctx.close();
124}
125
126// ── 3. Enter adds it; Shift+Enter does not ──────────────────────────
127{
128 const ctx = await contextWith(true);
129 const page = await ctx.newPage();
130 await page.goto(origin + '/today', { waitUntil: 'networkidle' });
131 await page.locator('[cal-quick] > summary').click();
132
133 const box = page.locator('[cal-quick-text]');
134
135 // Shift+Enter is a newline, so it must NOT create anything. Asserted
136 // as "did not land on an event" rather than "still on /today": /today
137 // redirects to /day/<date>, so the page was never on /today to begin
138 // with and the tighter-looking assertion was just wrong.
139 //
140 // Checked BEFORE the submitting case — once Enter navigates there is no
141 // page left to test this on.
142 await box.fill('Draft lunch');
143 await box.press('Shift+Enter');
144 await page.waitForTimeout(300);
145 check(!/\/events\/\d+$/.test(page.url()),
146 'Shift+Enter created nothing (got ' + page.url() + ')');
147 check((await box.inputValue()).includes('\n'),
148 'Shift+Enter inserts a newline rather than submitting');
149
150 // The hint only appears when this file is running, because without it
151 // Enter does nothing.
152 check(await page.locator('[cal-quick-key]').isVisible(),
153 'the Enter hint is shown when script is on');
154
155 await box.fill('Keyboard standup tomorrow at 8am in the Loft');
156 await Promise.all([
157 page.waitForNavigation(),
158 box.press('Enter'),
159 ]);
160
161 check(/\/events\/\d+$/.test(page.url()),
162 'Enter creates the event (got ' + page.url() + ')');
163 const body = await page.locator('body').innerText();
164 check(body.includes('Keyboard standup'), 'the event Enter created kept its title');
165 check(body.includes('Loft'), 'the event Enter created kept its place');
166 await ctx.close();
167}
168
169// ── 4. The Enter hint is absent with no script ──────────────────────
170{
171 const ctx = await contextWith(false);
172 const page = await ctx.newPage();
173 await page.goto(origin + '/today');
174 await page.locator('[cal-quick] > summary').click();
175 check(!(await page.locator('[cal-quick-key]').isVisible()),
176 'the Enter hint stays hidden with JavaScript off — the shortcut does not work there');
177 await ctx.close();
178}
179
180// ── 5. A date range is a run of days ────────────────────────────────
181{
182 const ctx = await contextWith(true);
183 const page = await ctx.newPage();
184 await page.goto(origin + '/today', { waitUntil: 'networkidle' });
185 await page.locator('[cal-quick] > summary').click();
186
187 await page.locator('[cal-quick-text]').fill('london tues-thurs');
188 await page.waitForFunction(
189 () => /Tuesday/.test(document.querySelector('[cal-quick-pane]')?.innerText || ''),
190 null, { timeout: 5000 },
191 );
192
193 const pane = await page.locator('[cal-quick-pane]').innerText();
194 check(/Tuesday/.test(pane) && /Thursday/.test(pane),
195 'the pane names both ends of the run:\n' + pane);
196 check(/all day/i.test(pane),
197 'the pane says a run with no time is all day:\n' + pane);
198 // The stranded word: the whole reason this pass exists.
199 check(!/thurs\b/i.test(pane.split('\n')[0] || ''),
200 'the end of the range was left in the title:\n' + pane);
201 await ctx.close();
202}
203
204await browser.close();
205
206if (failures.length) {
207 console.error('\n' + failures.length + ' browser check(s) failed');
208 process.exit(1);
209}
210console.log('quick add: all browser checks passed');
211