// native-applink.mjs: the browser's half of TestNativeBrowserAppLink. // // The web's "Join in the app" (2026-10-02): a person signed in on the web // hands themselves to the Calls app. The Go test plays the app — it holds // the verifier and calls callscore exactly as the app does — and drives // this script one command at a time over stdin, one JSON line back per // command, so each step's answer can decide the next: // // {"cmd":"context","cookie":{name,value}|null,"js":true} a browser // {"cmd":"goto","path":"/m/x/join"} → {"url","appHref","body"} // {"cmd":"click-app"} → {"url","app"} (the offer page) // {"cmd":"guest","name":"Dee"} → {"url"} (guest form) // {"cmd":"approve","code":"ABCD-1234"} → {"url","text"} (on /native/complete) // {"cmd":"quit"} // // Every command answers {"error": "..."} instead when it cannot do what // it says, and the test fails with that. import { chromium } from 'playwright'; import { createInterface } from 'node:readline'; const lines = createInterface({ input: process.stdin }); const input = lines[Symbol.asyncIterator](); const origin = JSON.parse((await input.next()).value).origin; const browser = await chromium.launch({ headless: true }); let context = null; let page = null; const reply = (o) => process.stdout.write(JSON.stringify(o) + '\n'); async function appHref() { const a = page.locator('a[href^="oficina-calls:"], a:has-text("Join in the app")').first(); return (await a.count()) ? a.getAttribute('href') : ''; } try { for (;;) { const next = await input.next(); if (next.done) break; const c = JSON.parse(next.value); try { switch (c.cmd) { case 'context': { if (context) await context.close(); context = await browser.newContext({ javaScriptEnabled: c.js !== false }); if (c.cookie) await context.addCookies([{ name: c.cookie.name, value: c.cookie.value, url: origin }]); page = await context.newPage(); reply({ ok: true }); break; } case 'goto': { await page.goto(origin + c.path, { waitUntil: 'domcontentloaded' }); reply({ url: page.url(), appHref: await appHref(), body: (await page.locator('body').innerText()).slice(0, 2000) }); break; } case 'click-app': { await Promise.all([ // Not "load": with script the page opens oficina-calls:// // straight away, which a browser with no Calls app never // finishes, and that is the case here. page.waitForURL('**/native/offer?o=*', { waitUntil: 'commit' }), page.getByRole('link', { name: 'Join in the app', exact: true }).click(), ]); const app = await page.locator('#open-app').getAttribute('href', { timeout: 10000 }); reply({ url: page.url(), app }); break; } case 'guest': { await page.locator('input[name="name"]').fill(c.name); await Promise.all([ page.waitForURL((u) => !u.pathname.startsWith('/j/')), page.locator('form[action$="/guest"] button[type="submit"]').click(), ]); reply({ url: page.url() }); break; } case 'approve': { // The waiting page turns into the confirmation by itself — by // script, or by its own refresh with none — once the app has // asked. Nothing here navigates it. await page.waitForURL('**/native/authorize?id=*', { timeout: 20000, waitUntil: 'domcontentloaded' }); const text = await page.locator('body').innerText(); if (!text.includes(c.code)) throw new Error('the confirmation does not show the app\'s code ' + c.code + ': ' + text.slice(0, 400)); const button = page.getByRole('button', { name: /^(Sign in to Calls app|Continue as guest)$/ }); await Promise.all([page.waitForURL('**/native/complete'), button.click()]); reply({ url: page.url(), text: (await page.locator('body').innerText()).slice(0, 400) }); break; } case 'quit': reply({ ok: true }); throw new Error('quit'); default: reply({ error: 'unknown command ' + c.cmd }); } } catch (e) { if (e.message === 'quit') throw e; reply({ error: String(e && e.message || e) }); } } } catch (e) { if (e.message !== 'quit') throw e; } finally { await browser.close(); lines.close(); process.stdin.destroy(); }