CarPlay and Android Auto cannot render a web view. Both are template
surfaces, and the only audio they will control is the host's own AVPlayer
or ExoPlayer -- so an app that is "the web UI plus CarPlay" is really "the
web UI whose audio engine is native", and the page had no way to give
playback away.
web/src/native.ts replaces the playback surface of the page's media element
with one that forwards to the host and synthesises the events back. Nothing
in player.ts changes: it only ever speaks to the element, so the player bar,
the row buttons, the EQ bars and the keyboard shortcuts keep working as they
did. Video stays in the page, since CarPlay is audio-only and a native video
layer under a web view buys nothing. In a browser none of it installs.
Position and read are the host's to write. player.ts has been bitten before
by a stale position -- one left paused in another tab saved its older place
over where you had got to -- and a backgrounded web view is exactly that
tab: frozen, holding a time from minutes ago, while the host plays on. So
the beacon becomes a request for the host to save its own clock.
tests/native-bridge.js is what holds the two ends together, and it earned
its place immediately: the src setter called removeAttribute('src'), which
the shim's own override turned into a stop() that switched it back off one
line after enabling it. Silent, and only visible in a car. The stub DOM
moved to tests/dom-stub.js so that test and page-smoke share one harness
rather than two copies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
4.2 KiB
JavaScript
87 lines
4.2 KiB
JavaScript
// Builds a page from web/src as build.rs does, runs its script against a stub DOM, and fails
|
|
// on anything thrown: web/index.html, then web/admin.html in a second run of this file.
|
|
//
|
|
// This exists because a ReferenceError at load once blanked the whole UI: a patch
|
|
// anchored on a function that no longer existed, so `prefsModal` was referenced but
|
|
// never defined. `node --check` passes that happily -- it is a parse, not a run --
|
|
// and every server-side test passed too, because the server was fine.
|
|
//
|
|
// node tests/page-smoke.js
|
|
const { makeContext, vm } = require('./dom-stub.js');
|
|
const PAGE = process.argv[2] || 'index.html';
|
|
|
|
const { buildPage } = require('../web/build.mjs');
|
|
// What ships: minified, so an id may have lost its quotes.
|
|
const { html, js: script, script: file } = buildPage(PAGE);
|
|
if (!/<link rel=stylesheet href="?\/app\.css\?v=[0-9a-f]{12}"?>/.test(html) && PAGE !== 'login.html') {
|
|
console.error(`FAIL: ${PAGE} does not load /app.css?v=<hash>`); process.exit(1);
|
|
}
|
|
if (!new RegExp(`<script src="?/${file.replace('.', '\\.')}\\?v=[0-9a-f]{12}"?>`).test(html)) {
|
|
console.error(`FAIL: the page does not load /${file}?v=<hash>`); process.exit(1);
|
|
}
|
|
const { ctx, missing } = makeContext(html, script);
|
|
|
|
try {
|
|
vm.createContext(ctx);
|
|
vm.runInContext(script, ctx, { filename: `${PAGE}<script>`, timeout: 5000 });
|
|
} catch (e) {
|
|
console.error('FAIL: the page script threw while loading\n ' + e.stack.split('\n').slice(0, 3).join('\n '));
|
|
process.exit(1);
|
|
}
|
|
|
|
// The modals are built on demand, so a load-time check never reaches them. Drive the
|
|
// ones that construct markup from live data, which is where a bad field reference hides.
|
|
const feed = {
|
|
id: 'f', url: 'https://x/rss', title: 'A Feed', image: null, folder: null,
|
|
keywords: ['a'], allow_explicit: false, auto_download: true, max_new_per_check: 3,
|
|
schedule: 'every 6h', schedule_mins: 360, every_mins: 360,
|
|
last_checked: 1, next_check: 2, entries: 1, downloaded: 0, unread: 1, last_error: null,
|
|
};
|
|
const drive = PAGE === 'admin.html' ? [
|
|
['drawServer', () => ctx.drawServer()],
|
|
['drawAccounts', () => ctx.drawAccounts()],
|
|
['drawLogView', () => ctx.drawLogView()],
|
|
] : [
|
|
['settingsModal', () => ctx.settingsModal(feed)],
|
|
['settingsModal (no override)', () => ctx.settingsModal({ ...feed, schedule: null, schedule_mins: null })],
|
|
['downloadLatestModal', () => ctx.downloadLatestModal(feed)],
|
|
['removeFeed', () => ctx.removeFeed(feed)],
|
|
['prefsModal', () => ctx.prefsModal()],
|
|
['opmlModal', () => ctx.opmlModal()],
|
|
['selectFeed (directory)', () => ctx.selectFeed(':directory')],
|
|
['selectFeed (popular)', () => ctx.selectFeed(':popular')],
|
|
['selectFeed (currently listening)', () => ctx.selectFeed(':listening')],
|
|
['selectFeed (all subscriptions)', () => ctx.selectFeed(':all')],
|
|
['keysModal', () => ctx.keysModal()],
|
|
// `const S` is not reachable from here: top-level const/let do not become properties
|
|
// of a vm context the way var and function declarations do.
|
|
['renderGroup', () => ctx.renderGroup(feed, [{ ...feed, id: 'child', group: 'f', orphaned: true }])],
|
|
];
|
|
for (const [name, fn] of drive) {
|
|
try {
|
|
const r = fn();
|
|
if (r && typeof r.catch === 'function') r.catch(e => {
|
|
console.error(`FAIL: ${name} rejected: ${e.message}`); process.exit(1);
|
|
});
|
|
} catch (e) {
|
|
console.error(`FAIL: ${name} threw: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// theme.ts keeps the Settings theme controls in step when Settings is open, and looks before it
|
|
// touches them. The admin page has no Settings, so those are the ones it may ask for and not find.
|
|
const OPTIONAL = new Set(['#stheme', '#smode', '#smodefield']);
|
|
missing.splice(0, missing.length, ...missing.filter(sel => !OPTIONAL.has(sel)));
|
|
if (missing.length) {
|
|
console.error('FAIL: handlers wired to elements that do not exist: ' + [...new Set(missing)].join(', '));
|
|
process.exit(1);
|
|
}
|
|
console.log(`OK: ${PAGE}: its script loads clean, every selector it wires at load exists`);
|
|
// The admin page's log arms a poll timer; without an exit the pending interval keeps node alive.
|
|
if (PAGE === 'index.html') {
|
|
const r = require('child_process').spawnSync(process.execPath, [__filename, 'admin.html'], { stdio: 'inherit' });
|
|
process.exit(r.status);
|
|
}
|
|
process.exit(0);
|