Frutiger Aero: the theme now plays its soundtrack
Selecting Aero starts a random track from the Frutiger Aero playlist (Bigblamboii's set on SoundCloud); every following track is drawn at random too, so the set shuffles itself. The player is the credit bar from the mock: a 44px glass circle with a note by default, expanding into pause + the track's title + "SoundCloud - artist" (both reported by the widget). - Only Aero ever touches SoundCloud: the embed and the widget API are created while the theme is on and the listener has not turned it off; light/dark request nothing (pinned by a suite check). - The off choice persists (freezmo.aero-music.v1); re-selecting Aero in Settings is fresh consent (applyTheme clears it). - Autoplay rides the theme-selection gesture; after a reload the circle comes up paused (browsers require a gesture) and one tap starts it. - Volume 45; the iframe is 1px and inert, only the audio engine; the credit area collapses back to the circle without stopping the music. - Geometry: clears the phone tab bar, lifts above the chat composer and the wizard bar, steps aside under immersive overlays and while the cookie notice shares the corner. 44px targets on phones. - Cookie policy gained the third-party-embed paragraph it was owed. tests/aero-music-test.mjs (16th suite, stubbed widget API, 21 checks). All suites green, contrast scan clean. Version 0.7.0, SW v92.
This commit is contained in:
parent
71c56ad06f
commit
a4c85ed77a
14 changed files with 570 additions and 4 deletions
|
|
@ -28,6 +28,7 @@ prints a per-suite summary at the end.
|
|||
| Suite | Covers |
|
||||
| --- | --- |
|
||||
| `regression.mjs` | Library, deck card + menu, study feed to results, homework entry, desktop shell + settings/health panel. |
|
||||
| `aero-music-test.mjs` | Frutiger Aero's soundtrack player (the SoundCloud widget API is stubbed): nothing loads in light/dark, a random playlist track starts in Aero, the circle expands into the credit bar, pause/collapse/close wiring, the off choice persisting and Aero re-selecting re-arming it, phone geometry over the tab bar and the chat composer, and the control stepping aside under an immersive overlay. |
|
||||
| `bubbles-test.mjs` | Ice-scene bubbles: stacking/layout contract, the open band (a plain clip, no mask) and a 41-phase sweep that no visible orb or bubble ever touches its edges, keyframes only transform+opacity, rise/sway/scale/fade phases, per-breakpoint counts (5/3/2), phone pacing, reduced motion, parking (hidden tab, unfocused window, dialog scrim), and the cost contract (static blobs, no blend-mode grain, no blur on static glass, no hidden blur under Aero's bake). |
|
||||
| `mobile-test.mjs` | Five-tab bar at 360/390 × EN/CS × dark/light × reduced motion (labels, targets, no hamburger/drawer), the library ⋯ sheet (Import JSON / New folder), the settings sheet (touch targets, key flow, drag-to-close, backdrop), axe contrast audits. |
|
||||
| `wizard-test.mjs` | Deck 3-step flow and homework flow, flat action bar, in-page mode switch, the loading lock, abort-on-leave. |
|
||||
|
|
|
|||
164
tests/aero-music-test.mjs
Normal file
164
tests/aero-music-test.mjs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* 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; 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: 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();
|
||||
}
|
||||
|
||||
/* 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();
|
||||
|
|
@ -14,6 +14,7 @@ import path from "node:path";
|
|||
const SUITES = [
|
||||
"regression",
|
||||
"activities-test",
|
||||
"aero-music-test",
|
||||
"bubbles-test",
|
||||
"mobile-test",
|
||||
"wizard-test",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue