oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1// native-applink.mjs: the browser's half of TestNativeBrowserAppLink.
2//
3// The web's "Join in the app" (2026-10-02): a person signed in on the web
4// hands themselves to the Calls app. The Go test plays the app — it holds
5// the verifier and calls callscore exactly as the app does — and drives
6// this script one command at a time over stdin, one JSON line back per
7// command, so each step's answer can decide the next:
8//
9// {"cmd":"context","cookie":{name,value}|null,"js":true} a browser
10// {"cmd":"goto","path":"/m/x/join"} → {"url","appHref","body"}
11// {"cmd":"click-app"} → {"url","app"} (the offer page)
12// {"cmd":"guest","name":"Dee"} → {"url"} (guest form)
13// {"cmd":"approve","code":"ABCD-1234"} → {"url","text"} (on /native/complete)
14// {"cmd":"quit"}
15//
16// Every command answers {"error": "..."} instead when it cannot do what
17// it says, and the test fails with that.
18import { chromium } from 'playwright';
19import { createInterface } from 'node:readline';
20
21const lines = createInterface({ input: process.stdin });
22const input = lines[Symbol.asyncIterator]();
23const origin = JSON.parse((await input.next()).value).origin;
24const browser = await chromium.launch({ headless: true });
25let context = null;
26let page = null;
27const reply = (o) => process.stdout.write(JSON.stringify(o) + '\n');
28
29async function appHref() {
30 const a = page.locator('a[href^="oficina-calls:"], a:has-text("Join in the app")').first();
31 return (await a.count()) ? a.getAttribute('href') : '';
32}
33
34try {
35 for (;;) {
36 const next = await input.next();
37 if (next.done) break;
38 const c = JSON.parse(next.value);
39 try {
40 switch (c.cmd) {
41 case 'context': {
42 if (context) await context.close();
43 context = await browser.newContext({ javaScriptEnabled: c.js !== false });
44 if (c.cookie) await context.addCookies([{ name: c.cookie.name, value: c.cookie.value, url: origin }]);
45 page = await context.newPage();
46 reply({ ok: true });
47 break;
48 }
49 case 'goto': {
50 await page.goto(origin + c.path, { waitUntil: 'domcontentloaded' });
51 reply({ url: page.url(), appHref: await appHref(), body: (await page.locator('body').innerText()).slice(0, 2000) });
52 break;
53 }
54 case 'click-app': {
55 await Promise.all([
56 // Not "load": with script the page opens oficina-calls://
57 // straight away, which a browser with no Calls app never
58 // finishes, and that is the case here.
59 page.waitForURL('**/native/offer?o=*', { waitUntil: 'commit' }),
60 page.getByRole('link', { name: 'Join in the app', exact: true }).click(),
61 ]);
62 const app = await page.locator('#open-app').getAttribute('href', { timeout: 10000 });
63 reply({ url: page.url(), app });
64 break;
65 }
66 case 'guest': {
67 await page.locator('input[name="name"]').fill(c.name);
68 await Promise.all([
69 page.waitForURL((u) => !u.pathname.startsWith('/j/')),
70 page.locator('form[action$="/guest"] button[type="submit"]').click(),
71 ]);
72 reply({ url: page.url() });
73 break;
74 }
75 case 'approve': {
76 // The waiting page turns into the confirmation by itself — by
77 // script, or by its own refresh with none — once the app has
78 // asked. Nothing here navigates it.
79 await page.waitForURL('**/native/authorize?id=*', { timeout: 20000, waitUntil: 'domcontentloaded' });
80 const text = await page.locator('body').innerText();
81 if (!text.includes(c.code)) throw new Error('the confirmation does not show the app\'s code ' + c.code + ': ' + text.slice(0, 400));
82 const button = page.getByRole('button', { name: /^(Sign in to Calls app|Continue as guest)$/ });
83 await Promise.all([page.waitForURL('**/native/complete'), button.click()]);
84 reply({ url: page.url(), text: (await page.locator('body').innerText()).slice(0, 400) });
85 break;
86 }
87 case 'quit':
88 reply({ ok: true });
89 throw new Error('quit');
90 default:
91 reply({ error: 'unknown command ' + c.cmd });
92 }
93 } catch (e) {
94 if (e.message === 'quit') throw e;
95 reply({ error: String(e && e.message || e) });
96 }
97 }
98} catch (e) {
99 if (e.message !== 'quit') throw e;
100} finally {
101 await browser.close();
102 lines.close();
103 process.stdin.destroy();
104}
105