/** * 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: "player" }), ); } /* 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();