freezmo/tests/aero-music-test.mjs
Martin Dvorak 1ad199fb56 Aero music: start without a tap on the player
"It should play by default when the Frutiger theme loads in." The embed URL
now carries auto_play=true as a second try alongside the immediate play()
call, and when the browser still refuses (a reload carries no gesture), the
retry rides the next pointerdown/keydown anywhere OUTSIDE the player - the
first tap on anything starts the music. A 700ms throttle keeps a flurry of
taps from skipping through the set, taps on the player's own controls are
left to their handlers (no blip on the x), and an explicit pause or dismiss
is respected: no later gesture undoes it. Looking at the player (expanding
the circle) starts a still-pending soundtrack too.

aero-music-test simulates the refusal (__scAllowPlay) and pins all three
paths: one attempt anyway, the next tap starts it, an explicit pause stays.
28 checks. Version 0.7.1, SW v93.
2026-10-16 05:28:12 +00:00

227 lines
11 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();
}
/* 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();