The third segment of the note-powered wizards (Study deck / Homework / Podcast) takes the same intake - photos, documents, pasted text, the share target - and turns it into a spoken episode. - prompts: PODCAST_SYSTEM_PROMPT writes for the ear (plain prose, no markdown, no visual references), chaptered into 3-8 sections around a length target. - /api/podcast mirrors /api/generate's plumbing (rate limit, model gate, provider keys, heartbeats, the 2x deadline, the optional vision pass). - storage: frezmo.podcasts.v1, zod-validated, included in the backup and the encrypted sync payload; a new library tab lists episodes with listen / rename / delete-with-undo, and the wizard switch deep-links in. - player: the transcript is word-wrapped spans and the word under the voice rides a lyric-style highlight that keeps itself centred. Two clocks: exact onboundary events where the browser reports them, a per-word length timer where it does not (iOS Safari); the first boundary retires the timer. Pause = cancel + re-speak from the word, which makes resume, chapter jumps and tap-any-word-to-seek the same operation; the position saves on pause, page-hide and unmount, so reopening resumes. Speed rebuilds the engine and keeps playing; the Aero soundtrack pauses itself when the voice starts; a browser with no voice gets an honest note and a readable transcript. - the script sits on a glass pane (plain paragraphs over Aero's grass failed contrast; the scan now covers the player too). tests/podcast-test.mjs (17th suite) stubs the model and the speech engine and drives the highlight by hand: 26 checks. All suites green, contrast clean. Version 0.7.3, SW v95.
51 lines
1.5 KiB
JavaScript
51 lines
1.5 KiB
JavaScript
/**
|
|
* Runs every Freezmo browser suite in order.
|
|
*
|
|
* npm run test:e2e # everything
|
|
* npm run test:e2e -- phase1 # only suites whose name contains "phase1"
|
|
*
|
|
* Each suite is a standalone script that talks to the running app on :3207
|
|
* (FREEZMO_URL overrides). Suites that need a stub Ollama or extra server
|
|
* instance start their own on their own port and clean up after themselves.
|
|
*/
|
|
import { spawn } from "node:child_process";
|
|
import path from "node:path";
|
|
|
|
const SUITES = [
|
|
"regression",
|
|
"activities-test",
|
|
"aero-music-test",
|
|
"bubbles-test",
|
|
"mobile-test",
|
|
"wizard-test",
|
|
"native-test",
|
|
"i18n-test",
|
|
"freeze-loading-test",
|
|
"phase1-test",
|
|
"phase2-test",
|
|
"phase3-test",
|
|
"podcast-test",
|
|
"security-test",
|
|
"chat-test",
|
|
"providers-test",
|
|
"review-test",
|
|
];
|
|
|
|
const filters = process.argv.slice(2);
|
|
const list = filters.length ? SUITES.filter((name) => filters.some((f) => name.includes(f))) : SUITES;
|
|
|
|
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
const repo = path.resolve(here, "..");
|
|
|
|
let failed = 0;
|
|
for (const suite of list) {
|
|
console.log(`\n===== ${suite} =====`);
|
|
const code = await new Promise((resolve) => {
|
|
const child = spawn(process.execPath, [path.join(here, `${suite}.mjs`)], { cwd: repo, stdio: "inherit" });
|
|
child.on("close", resolve);
|
|
});
|
|
if (code !== 0) failed += 1;
|
|
}
|
|
|
|
console.log(`\n${list.length - failed}/${list.length} suites passed`);
|
|
process.exit(failed ? 1 : 0);
|