"The music thing button with the Frutiger Aero doesn't work on the podcasts." The podcast page is a full-screen overlay at z-45 and the control sat at z-40: visible through the transparent page, but every tap landed on the overlay. The control now takes a data-raised="player" position on /podcast/[id] - z-index 46, docked to the header's corner (top under the header pill, right mirroring the max-w-3xl geometry) where neither the transport bar nor the voice panel lives - and the transcript gained pt-32 so the episode card clears it. Study/review/exam feeds keep their "steps aside" behaviour. aero-music-test: a podcast section asserts the control exists, that elementFromPoint at its centre hits the button, and that a tap expands the credit bar. 31 checks. Version 0.7.9, SW v101.
268 lines
13 KiB
JavaScript
268 lines
13 KiB
JavaScript
/**
|
|
* Frutiger Aero's soundtrack player: only in Aero, only while enabled; a
|
|
* random track starts, the credit bar expands from the circle, pause/close
|
|
* reach the widget, and the geometry clears the phone tab bar and the chat
|
|
* composer. The SoundCloud widget API is stubbed — the real one needs the
|
|
* network and the live playlist.
|
|
*/
|
|
import { BASE, DECK, launchBrowser, makeChecker, makePage, seed } from "./lib.mjs";
|
|
|
|
const { check, finish } = makeChecker();
|
|
const browser = await launchBrowser();
|
|
|
|
/** A tiny stand-in for https://w.soundcloud.com/player/api.js. */
|
|
const STUB = `(function(){
|
|
var sounds = [
|
|
{ title: "Track One", user: { username: "Bigblamboii" } },
|
|
{ title: "Track Two", user: { username: "Bigblamboii" } },
|
|
{ title: "Track Three", user: { username: "Bigblamboii" } }
|
|
];
|
|
window.__scStub = { skipped: [], plays: 0, pauses: 0, volume: null, current: 0, sounds: sounds, iframeSrc: null };
|
|
function Widget(el) {
|
|
window.__scStub.iframeSrc = el && el.getAttribute ? el.getAttribute("src") : null;
|
|
var handlers = {};
|
|
function emit(event) { (handlers[event] || []).forEach(function (h) { h(); }); }
|
|
var api = {
|
|
bind: function (event, handler) { (handlers[event] = handlers[event] || []).push(handler); },
|
|
getSounds: function (cb) { setTimeout(function () { cb(sounds); }, 10); },
|
|
getCurrentSound: function (cb) { setTimeout(function () { cb(sounds[window.__scStub.current]); }, 0); },
|
|
skip: function (index) { window.__scStub.skipped.push(index); window.__scStub.current = index; },
|
|
play: function () {
|
|
window.__scStub.plays += 1;
|
|
/* A browser that has not seen a gesture yet refuses the start: the
|
|
app must retry from the next real one. */
|
|
if (window.__scAllowPlay === false) return;
|
|
emit("play");
|
|
},
|
|
pause: function () { window.__scStub.pauses += 1; emit("pause"); },
|
|
setVolume: function (value) { window.__scStub.volume = value; }
|
|
};
|
|
setTimeout(function () { emit("ready"); }, 20);
|
|
return api;
|
|
}
|
|
Widget.Events = { READY: "ready", PLAY: "play", PAUSE: "pause", FINISH: "finish" };
|
|
window.SC = { Widget: Widget };
|
|
})();`;
|
|
|
|
async function stubSoundCloud(page) {
|
|
await page.route("https://w.soundcloud.com/player/api.js*", (route) =>
|
|
route.fulfill({ contentType: "application/javascript", body: STUB }),
|
|
);
|
|
await page.route("https://w.soundcloud.com/player/?*", (route) =>
|
|
route.fulfill({ contentType: "text/html", body: "<!doctype html><title>player</title>" }),
|
|
);
|
|
}
|
|
|
|
/* 1. the embed belongs to Aero alone ---------------------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 731 });
|
|
await stubSoundCloud(page);
|
|
const requests = [];
|
|
page.on("request", (request) => {
|
|
if (request.url().includes("soundcloud.com")) requests.push(request.url());
|
|
});
|
|
await seed(page, { theme: "light", decks: [] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1200);
|
|
check("light theme: no player", (await page.locator("[data-testid=aero-music]").count()) === 0);
|
|
check("light theme: nothing is requested from SoundCloud", requests.length === 0, JSON.stringify(requests.slice(0, 2)));
|
|
await context.close();
|
|
}
|
|
|
|
/* 2. Aero: a random track starts, credits expand ---------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 731 });
|
|
await stubSoundCloud(page);
|
|
await seed(page, { theme: "aero", decks: [] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1500);
|
|
|
|
const circle = page.getByRole("button", { name: "Frutiger Aero music" });
|
|
check("aero: the circle is the default state", await circle.isVisible());
|
|
check("aero: no credits while collapsed", (await page.getByText("SoundCloud ·", { exact: false }).count()) === 0);
|
|
|
|
const stub = await page.evaluate(() => window.__scStub);
|
|
const index = stub.skipped[0];
|
|
check("aero: a random track from the playlist is chosen", Number.isInteger(index) && index >= 0 && index < stub.sounds.length, String(index));
|
|
check("aero: playback starts", stub.plays >= 1, String(stub.plays));
|
|
check("aero: the embed points at the playlist", decodeURIComponent(stub.iframeSrc ?? "").includes("soundcloud.com/rustie-gamer/sets/frutiger-aero"));
|
|
check("aero: the embed itself asks to autoplay", (stub.iframeSrc ?? "").includes("auto_play=true"));
|
|
check("aero: background volume", stub.volume === 45, String(stub.volume));
|
|
|
|
await circle.click();
|
|
await page.waitForTimeout(500);
|
|
const bar = await page.evaluate(() => document.querySelector("[data-testid=aero-music]")?.innerText ?? "");
|
|
check("aero: expanding credits the track and the author", bar.includes("Track") && bar.includes("SoundCloud · Bigblamboii"), JSON.stringify(bar));
|
|
|
|
await page.getByRole("button", { name: "Pause the music" }).click();
|
|
await page.waitForTimeout(300);
|
|
check("aero: pause reaches the widget", (await page.evaluate(() => window.__scStub.pauses)) >= 1);
|
|
|
|
/* The credit area collapses back to the circle without stopping. */
|
|
await page.getByRole("button", { name: "Back to the music button" }).click();
|
|
await page.waitForTimeout(400);
|
|
check("aero: the bar collapses back to the circle", await circle.isVisible());
|
|
|
|
await circle.click();
|
|
await page.waitForTimeout(300);
|
|
await page.getByRole("button", { name: "Turn the music off" }).click();
|
|
await page.waitForTimeout(400);
|
|
check("aero: turning it off hides the player", (await page.locator("[data-testid=aero-music]").count()) === 0);
|
|
check("aero: the choice is remembered", (await page.evaluate(() => localStorage.getItem("freezmo.aero-music.v1"))) === "off");
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1200);
|
|
check("aero: it stays off after a reload", (await page.locator("[data-testid=aero-music]").count()) === 0);
|
|
await context.close();
|
|
}
|
|
|
|
/* 2b. a refused autoplay starts from the next gesture ----------------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 731 });
|
|
await stubSoundCloud(page);
|
|
await seed(page, { theme: "aero", decks: [] });
|
|
/* Load with autoplay refused, the way a browser without activation does. */
|
|
await page.addInitScript(() => {
|
|
window.__scAllowPlay = false;
|
|
});
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1500);
|
|
const first = await page.evaluate(() => window.__scStub.plays);
|
|
check("blocked autoplay: one attempt is made anyway", first >= 1, String(first));
|
|
check(
|
|
"blocked autoplay: the circle shows nothing playing yet",
|
|
(await page.getByRole("button", { name: "Frutiger Aero music" }).count()) === 1,
|
|
);
|
|
await page.getByRole("button", { name: "Frutiger Aero music" }).click();
|
|
await page.waitForTimeout(300);
|
|
check("blocked autoplay: play is offered while it waits", (await page.getByRole("button", { name: "Play the music" }).count()) === 1);
|
|
await page.getByRole("button", { name: "Back to the music button" }).click();
|
|
await page.waitForTimeout(300);
|
|
|
|
/* The browser would now allow it: the next tap anywhere starts the music.
|
|
(The retry throttle is 700ms, so give the previous attempt room.) */
|
|
await page.waitForTimeout(900);
|
|
await page.evaluate(() => {
|
|
window.__scAllowPlay = true;
|
|
});
|
|
const before = await page.evaluate(() => window.__scStub.plays);
|
|
await page.evaluate(() => {
|
|
document.body.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
|
|
});
|
|
await page.waitForTimeout(600);
|
|
const after = await page.evaluate(() => window.__scStub.plays);
|
|
check("blocked autoplay: the next tap starts the music", after > before, `${before} -> ${after}`);
|
|
await page.getByRole("button", { name: "Frutiger Aero music" }).click();
|
|
await page.waitForTimeout(400);
|
|
check("after the gesture start the bar offers pause", (await page.getByRole("button", { name: "Pause the music" }).count()) === 1);
|
|
|
|
/* An explicit pause is respected: no gesture restarts it. */
|
|
await page.getByRole("button", { name: "Pause the music" }).click();
|
|
await page.waitForTimeout(300);
|
|
const paused = await page.evaluate(() => window.__scStub.plays);
|
|
await page.evaluate(() => {
|
|
document.body.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
|
|
});
|
|
await page.waitForTimeout(600);
|
|
check(
|
|
"an explicit pause is not undone by later taps",
|
|
(await page.evaluate(() => window.__scStub.plays)) === paused,
|
|
`${paused} -> ${await page.evaluate(() => window.__scStub.plays)}`,
|
|
);
|
|
await context.close();
|
|
}
|
|
|
|
/* 3. picking Aero again is fresh consent ------------------------------ */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 731 });
|
|
await stubSoundCloud(page);
|
|
await seed(page, { theme: "dark", decks: [], extra: { "freezmo.aero-music.v1": "off" } });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1200);
|
|
check("dark theme: no player", (await page.locator("[data-testid=aero-music]").count()) === 0);
|
|
await page.locator("text=Settings").first().click({ force: true });
|
|
await page.waitForTimeout(1200);
|
|
await page.getByRole("button", { name: "Frutiger Aero" }).first().click();
|
|
await page.waitForTimeout(1600);
|
|
check("selecting Aero again brings the music back", (await page.locator("[data-testid=aero-music]").count()) === 1);
|
|
check("selecting Aero again starts playback", ((await page.evaluate(() => window.__scStub?.plays)) ?? 0) >= 1);
|
|
await context.close();
|
|
}
|
|
|
|
/* 4. geometry: phones clear the tab bar and the chat composer --------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 2, isMobile: true, hasTouch: true });
|
|
await stubSoundCloud(page);
|
|
await seed(page, { theme: "aero", decks: [] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1400);
|
|
const circle = await page.locator("[data-testid=aero-music]").boundingBox();
|
|
const tabbar = await page.locator("nav.tabbar").boundingBox();
|
|
check("phone: the circle sits above the tab bar", Boolean(circle && tabbar) && circle.y + circle.height <= tabbar.y + 1, JSON.stringify({ circle, tabbar }));
|
|
check("phone: the circle is a 44px touch target", Boolean(circle) && circle.width >= 43.5 && circle.height >= 43.5, JSON.stringify(circle && { w: Math.round(circle.width), h: Math.round(circle.height) }));
|
|
|
|
await page.goto(`${BASE}/chat`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1600);
|
|
const chatCircle = await page.locator("[data-testid=aero-music]").boundingBox();
|
|
const composer = await page.locator(".chat-composer-band").boundingBox();
|
|
check("phone chat: the circle sits above the composer", Boolean(chatCircle && composer) && chatCircle.y + chatCircle.height <= composer.y + 1, JSON.stringify({ chatCircle, composer }));
|
|
await context.close();
|
|
}
|
|
|
|
/* 4b. the podcast player: the control rides above that overlay -------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
|
|
await stubSoundCloud(page);
|
|
await seed(page, {
|
|
theme: "aero",
|
|
decks: [],
|
|
extra: {
|
|
"freezmo.podcasts.v1": JSON.stringify([
|
|
{
|
|
id: "pod_music",
|
|
title: "While you listen",
|
|
summary: "",
|
|
language: "English",
|
|
sourceText: "x",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
sections: [{ heading: "One", text: "Look up on a clear day." }],
|
|
},
|
|
]),
|
|
},
|
|
});
|
|
await page.goto(`${BASE}/podcast/pod_music`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1500);
|
|
|
|
const circle = page.getByRole("button", { name: "Frutiger Aero music" });
|
|
check("podcast: the music control is there while listening", (await circle.count()) === 1);
|
|
const reachable = await page.evaluate(() => {
|
|
const button = document.querySelector("[data-testid=aero-music] button");
|
|
if (!button) return "missing";
|
|
const box = button.getBoundingClientRect();
|
|
const top = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
|
|
return button.contains(top) || top === button ? "on-top" : `covered by ${top?.className?.toString().slice(0, 40)}`;
|
|
});
|
|
check("podcast: nothing covers it (it rides above the overlay)", reachable === "on-top", reachable);
|
|
await circle.click();
|
|
await page.waitForTimeout(400);
|
|
check("podcast: tapping it expands the credit bar", (await page.getByText("SoundCloud ·", { exact: false }).count()) >= 1);
|
|
await context.close();
|
|
}
|
|
|
|
/* 5. immersive overlays step aside, the music keeps playing ----------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 731 });
|
|
await stubSoundCloud(page);
|
|
await seed(page, { theme: "aero", decks: [DECK] });
|
|
await page.goto(`${BASE}/deck/seed_deck`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(1500);
|
|
const hidden = await page.evaluate(() => {
|
|
const player = document.querySelector("[data-testid=aero-music]");
|
|
return player ? getComputedStyle(player).visibility : "missing";
|
|
});
|
|
check("study feed: the control steps aside (music keeps playing)", hidden === "hidden", hidden);
|
|
await context.close();
|
|
}
|
|
|
|
await browser.close();
|
|
finish();
|