Podcast voices: an iPhone note on where the other voices live
Safari hands web pages one voice per language, whatever the iPhone has installed, so the picker cannot fix that on iOS. It now carries the way out instead: Accessibility -> Spoken Content -> Voices -> your language, download an Enhanced/Premium voice and make it preferred. The speaker button wears an amber dot while the note is waiting, and the voice-less banner appends the same guidance. Detection covers iPadOS too (maxTouchPoints, since it reports as a Mac). The suite fakes an iPhone user agent, a single Daniel voice and pins the dot and the note's actual steps (37 checks). Version 0.7.5, SW v97.
This commit is contained in:
parent
4ec986da12
commit
9882d775e7
7 changed files with 83 additions and 7 deletions
|
|
@ -329,6 +329,45 @@ try {
|
|||
await context.close();
|
||||
}
|
||||
|
||||
/* 4b2. an iPhone gets told where the other voices live -------------- */
|
||||
{
|
||||
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
|
||||
/* Safari's shape: one voice for the language, whatever is installed. */
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "userAgent", {
|
||||
configurable: true,
|
||||
get: () => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
||||
});
|
||||
window.__spoken = [];
|
||||
const voices = [{ name: "Daniel", lang: "en-GB", voiceURI: "daniel", localService: true, default: true }];
|
||||
class FakeUtterance {
|
||||
constructor(text) { this.text = String(text); this.rate = 1; this.lang = "en-US"; this.voice = null; }
|
||||
}
|
||||
Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true, value: FakeUtterance });
|
||||
Object.defineProperty(window, "speechSynthesis", {
|
||||
configurable: true,
|
||||
value: { speak(u) { queueMicrotask(() => u.onstart?.({ name: "start", charIndex: 0 })); }, cancel() {}, getVoices: () => voices, addEventListener() {}, removeEventListener() {}, speaking: false, paused: false },
|
||||
});
|
||||
});
|
||||
await seed(page, { decks: [], theme: "dark", extra: { "freezmo.podcasts.v1": JSON.stringify([EPISODE_RECORD]) } });
|
||||
await page.goto(`${APP}/podcast/pod_local`, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const voiceButton = page.getByRole("button", { name: "Voice" });
|
||||
const badge = await voiceButton.evaluate((element) => Boolean(element.querySelector("span[aria-hidden]")));
|
||||
check("ios: the voice button carries a hint dot", badge);
|
||||
await voiceButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
const note = await page.getByText(/Spoken Content/).count();
|
||||
check("ios: the picker explains where to add voices", note === 1, String(note));
|
||||
const steps = await page.evaluate(() => {
|
||||
const text = [...document.querySelectorAll("p")].map((el) => el.textContent ?? "").find((entry) => entry.includes("Spoken Content")) ?? "";
|
||||
return { settings: text.includes("Settings"), accessibility: text.includes("Accessibility"), premium: text.includes("Enhanced") || text.includes("Premium") };
|
||||
});
|
||||
check("ios: the note names the actual settings path", steps.settings && steps.accessibility && steps.premium, JSON.stringify(steps));
|
||||
await context.close();
|
||||
}
|
||||
|
||||
/* 4c. the transcript follows by line, and yields to a manual scroll --- */
|
||||
{
|
||||
const { context, page } = await makePage(browser, { width: 390, height: 700, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue