Files
yellowjacket/scripts/android-eval.mjs
logan 0bfa2136be
Build & publish Arch package / arch-package (push) Successful in 2m29s
CI / check (push) Successful in 2m26s
Search index maintenance / maintain-index (push) Successful in 7s
CI / e2e (push) Successful in 5m53s
feat(dev): ask the phone instead of looking at it
The device tier could only take a screenshot and read what Go chose to
log, and a screenshot cannot tell a dropped CSS declaration from a
missing asset. This adds the third thing: the page's own answer, from
the engine that is really rendering it.

`make android-screenshot` grabs the screen, `make android-inspect`
forwards the WebView's devtools socket, and `make android-eval EXPR=...`
evaluates in the real page.

Four details are load-bearing. Only a `debuggable` build opens that
socket, so the debug build type takes `applicationIdSuffix ".dev"` and
installs *beside* the release app -- the two carry different signing
certificates, and Android's only remedy for a changed certificate is an
uninstall, which takes the user's library with it. Playwright cannot
drive a WebView (`connectOverCDP` calls `Browser.setDownloadBehavior`,
which it answers "Browser context management is not supported"), so the
eval is raw CDP over Node's built-in WebSocket. The socket name carries
the pid, so it is resolved per launch rather than written down. And
`exec-out`, not `shell`, for the screenshot: a pty translates LF and
corrupts the PNG.

What it immediately established is why it was worth having. The phone
renders in Chrome 113 at 424x439 CSS px -- two years behind every
browser the other tiers use, with no Popover API and no relaxed CSS
nesting -- so a spec passing at that viewport says nothing about the
device, and two conclusions drawn from version numbers alone were wrong.
Both are corrected in NOTES.md and the plan.
2026-08-17 05:13:21 -04:00

98 lines
3.0 KiB
JavaScript

/**
* Evaluate an expression inside the app's WebView on a real device.
*
* The device tier could only ever *look* at the app (a screenshot) or
* read what Go chose to log. This is the third thing: the page's own
* answer, from the engine that is actually rendering it — which is how
* "the icons are missing" stops being a guess about assets and becomes a
* computed style.
*
* Two facts make it work at all. A `debuggable` build calls
* `WebView.setWebContentsDebuggingEnabled(true)`, which opens an abstract
* unix socket per process (`webview_devtools_remote_<pid>`); `make
* android-inspect` forwards it to localhost. And **Playwright cannot use
* it** — `connectOverCDP` immediately calls `Browser.setDownloadBehavior`,
* which a WebView answers with "Browser context management is not
* supported", so the connection dies before the first evaluate. Raw CDP
* over Node's built-in WebSocket is a dozen lines and has no such
* opinion.
*
* Usage: node scripts/android-eval.mjs '<js expression>'
* make android-eval EXPR='...'
*
* The expression is evaluated with `awaitPromise`, so an async probe is
* fine. Return a string (`JSON.stringify(...)`) for anything structured:
* `returnByValue` will not serialise a DOM node.
*/
const PORT = process.env.YJ_ANDROID_CDP_PORT ?? '9222';
const expression = process.argv[2];
if (!expression) {
console.error("usage: node scripts/android-eval.mjs '<js expression>'");
process.exit(2);
}
const endpoint = `http://localhost:${PORT}/json`;
let targets;
try {
targets = await (await fetch(endpoint)).json();
} catch (err) {
console.error(
`android-eval: nothing on :${PORT} (${err.message})\n` +
" run 'make android-inspect' first, and check the phone is " +
'awake -- wireless adb drops when the screen sleeps',
);
process.exit(1);
}
const page = targets.find((t) => t.type === 'page');
if (!page) {
console.error('android-eval: no page target; is the app in the foreground?');
process.exit(1);
}
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
ws.onopen = resolve;
ws.onerror = () => reject(new Error('websocket refused'));
});
const answer = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('evaluate timed out')), 20_000);
ws.onmessage = (m) => {
const msg = JSON.parse(m.data);
if (msg.id !== 1) return;
clearTimeout(timer);
resolve(msg.result);
};
ws.send(
JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: { expression, awaitPromise: true, returnByValue: true },
}),
);
});
ws.close();
if (answer.exceptionDetails) {
console.error(
'android-eval: threw:',
answer.exceptionDetails.exception?.description ??
answer.exceptionDetails.text,
);
process.exit(1);
}
const value = answer.result?.value;
console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));