android: the phone turned sideways — the player beside itself, the chrome cut down

The phone UI was drawn portrait-first and had never been looked at in
landscape. The player sheet was the casualty: its cover is a square sized by
width, so at 915×411 it came out 879px tall and the title, the scrubber, the
transport and the face buttons all sat below the bottom of a sheet that does
not scroll — a corner of the artwork and no way to pause. Sideways the sheet
is two columns now, the cover sized by height beside the controls, with a
lyrics or queue face taking the cover's column; the nav and the mini bar come
down to 48 and 60dp, the artist hero becomes the banner the desktop's hero has
always been rather than 300px of a 411px screen, the record head puts the
cover beside the title, and the shell reads the side insets the cutout and the
gesture pill take when the phone is on its side. The walk turns the phone over
and back on its own: `{ view: [w, h] }` is a step kind now, and the steps
assert what used to be off the screen.
This commit is contained in:
KeelrLeek committed 2026-09-14 00:14:23 -05:00
1 parent aff29aba5c
commit 9df162360e
5 files changed
+228 -3

No files matched your search

+92
View File
@@ -68,6 +68,11 @@ window.__settle = function () {
/* what the page shows, read back after every still */
const PROBE = `(function () {
var e = document.querySelector('.screen');
/* geometry, for the orientation steps: box() is the rect or null, and
inside() asks whether every pixel of it is within the viewport —
which, sideways, is the whole question */
function box(sel) { var x = document.querySelector(sel); return x ? x.getBoundingClientRect() : null; }
function inside(b) { return !!b && b.height > 0 && b.top >= -1 && b.bottom <= innerHeight + 1 && b.left >= -1 && b.right <= innerWidth + 1; }
return {
screen: e ? e.className : '',
title: (document.querySelector('.gb-t') || {}).textContent || '',
@@ -183,6 +188,27 @@ const PROBE = `(function () {
halfSub: ((document.querySelector('.halfsheet .sheet-head .m') || {}).textContent || '').trim(),
halfSw: [].filter.call(document.querySelectorAll('.halfsheet .row'), function (r) { return !!r.querySelector('.sw.on'); }).map(function (r) { return ((r.querySelector('.t') || {}).textContent || '').trim(); }).join('|'),
contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),
vp: innerWidth + 'x' + innerHeight,
/* the sheet's controls: any one of them off the viewport is the landscape bug */
npOut: [].filter.call(document.querySelectorAll('.sheet.up .np-frame, .sheet.up .np-meta, .sheet.up .np-scrub, .sheet.up .np-transport, .sheet.up .np-foot'), function (x) { var b = x.getBoundingClientRect(); return b.height > 0 && !inside(b); }).length,
/* stacked = the portrait column; beside = the cover left of the controls; face = a face has the column and the thumbnail is gone */
npCols: (function () { var a = box('.sheet.up .np-frame'), b = box('.sheet.up .np-transport'); if (!a || !b) return ''; if (!a.height) return 'face'; return a.bottom <= b.top + 1 ? 'stacked' : 'beside'; })(),
faceH: Math.round((box('.sheet.up .np-face') || { height: 0 }).height),
/* WHICH face: the queue's own, up whether or not anything is after this one */
qFace: !!document.querySelector('.sheet.up .q-face'),
/* a half sheet: how tall it is allowed to be, that it is all on screen, and that its body still reaches its last row */
halfH: Math.round((box('.halfsheet.up') || { height: 0 }).height),
halfIn: inside(box('.halfsheet.up')),
hsScroll: (function () { var x = document.querySelector('.halfsheet.up .hs-body'); return x ? getComputedStyle(x).overflowY : ''; })(),
hsTail: (function () { var x = document.querySelector('.halfsheet.up .hs-body'); return x ? x.scrollHeight - x.clientHeight : -1; })(),
heroH: Math.round((box('.screen .hero') || { height: 0 }).height),
/* what a page offers first: Play on an artist hero, Play on a record head */
actsIn: inside(box('.screen .hero-acts') || box('.screen .rec-acts')),
recCols: (function () { var c = box('.screen .rec-cover'), m = box('.screen .rec-meta'); return c && m ? (c.bottom <= m.top + 1 ? 'stacked' : 'beside') : ''; })(),
/* the glass bar above plus the floating bars below, as a share of the screen */
chromePct: (function () { var g = box('.gbar'), low = box('.mini') || box('.nav'); return Math.round((((g ? g.height : 0) + (low ? innerHeight - low.top : 0)) / innerHeight) * 100); })(),
miniLeft: Math.round((box('.mini') || { left: -1 }).left),
screenPadL: (function () { var x = document.querySelector('.screen'); return x ? Math.round(parseFloat(getComputedStyle(x).paddingLeft)) : -1; })(),
};
})()`;
@@ -542,6 +568,38 @@ const MOCK_STEPS = [
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "home-more", wait: 700 },
{ do: `__click('.halfsheet .row', 'Sleep timer')`, name: "sleep-sheet", wait: 700 },
{ do: `__click('.halfsheet .chip', '30 min')`, name: "sleep-set", wait: 700 },
/* ── the phone turned over. A resize, so the app keeps playing and keeps
its stack; what changes is the layout, and every one of these asserts
something that was clipped or unreachable at 915x411 before. ────── */
{ view: [915, 411], name: "landscape", wait: 900 },
{ do: `document.querySelector('.mini').click()`, name: "landscape-sheet", wait: 1200 },
{ do: `document.querySelectorAll('.fbtn')[1].click()`, name: "landscape-lyrics", wait: 1200 },
{ do: `document.querySelectorAll('.fbtn')[2].click()`, name: "landscape-queue", wait: 900 },
{ do: `document.querySelectorAll('.fbtn')[0].click()`, wait: 500 },
{ do: `__ss.closeSheet()`, wait: 800 },
/* a record sideways: the cover beside the title, Play in view without a
scroll, and a long sheet over it — the track's, 473px of rows and
facts — capped taller than portrait's 76% and still scrolling to its
last row. (Home's own overflow sheet is 212px tall at either cap and
would have proved nothing.) */
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 800 },
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 500 },
{ do: `document.querySelectorAll('.grid2 .release')[2].click()`, name: "landscape-album", wait: 1100 },
{ do: `document.querySelector('.trk .r').click()`, name: "landscape-half", wait: 900 },
{ do: `document.querySelector('.scrim').click()`, wait: 700 },
/* and the artist: the hero a banner rather than 300px of a 411px screen,
so it stops covering its own Play button with the mini bar */
{ do: `document.querySelector('.rec-artist').click()`, name: "landscape-artist", wait: 1400 },
{ do: `document.querySelectorAll('.nav button')[0].click()`, wait: 800 },
/* the side insets, faked: sideways the cutout and the gesture pill come
in from the edges, and env() cannot be emulated here — the plumbing can */
{ do: `document.documentElement.style.setProperty('--inset-left', '28px'); document.documentElement.style.setProperty('--inset-right', '28px'); true`, name: "landscape-insets", wait: 700 },
{ do: `document.documentElement.style.removeProperty('--inset-left'); document.documentElement.style.removeProperty('--inset-right'); true`, wait: 500 },
/* and back, which is also the override reset: portrait must be exactly
what it was before any of this */
{ view: [411, 914], wait: 800 },
{ do: `document.querySelector('.mini').click()`, name: "portrait-again", wait: 1200 },
{ do: `__ss.closeSheet()`, wait: 600 },
];
const MOCK_EXPECT = {
tracks: (i) => i.title.startsWith("Your library"),
@@ -588,6 +646,25 @@ const MOCK_EXPECT = {
"search-recent-pick": (i) => i.title.startsWith("Search") && (i.songs >= 4 || !!i.top),
"sleep-sheet": (i) => i.half.includes("up"),
"sleep-set": (i) => !i.half.includes("up"),
/* sideways: the chrome budget, then every surface that was clipped. The
before-value of each is in the rig's now.json — 46% of the height as
chrome, a 879px cover with the transport 687px below the fold, a
lyrics face collapsed to nothing, a 300px hero, and the record's own
Play button off the bottom of a 411px screen. */
landscape: (i) => i.vp === "915x411" && i.mini && i.chromePct <= 42 && /^(Good |Still)/.test(i.title),
"landscape-sheet": (i) => i.vp === "915x411" && i.sheet.includes("up") && i.npCols === "beside" && i.npOut === 0,
"landscape-lyrics": (i) => i.sheet.includes("up") && i.npCols === "face" && i.faceH >= 240 && i.npOut === 0,
/* the queue face by name, not by its rows: the track this walk is on is
the LAST queueable row of Late night drive (the missing and the
unresolved ones never reach the queue), so there is nothing up next
and `qrows` is 0 here — the assertion is that the queue's own face has
the cover's column, not that something is after this song */
"landscape-queue": (i) => i.sheet.includes("up") && i.npCols === "face" && i.qFace && i.faceH >= 240 && i.npOut === 0,
"landscape-album": (i) => i.screen.includes("tr-push") && i.recCols === "beside" && i.actsIn,
"landscape-half": (i) => i.half.includes("up") && i.halfIn && i.halfH >= 340 && i.halfH <= 380 && i.hsScroll === "auto" && i.hsTail > 0,
"landscape-artist": (i) => i.heroH > 0 && i.heroH <= 200 && i.actsIn,
"landscape-insets": (i) => i.miniLeft >= 34 && i.screenPadL >= 26,
"portrait-again": (i) => i.vp === "411x914" && i.sheet.includes("up") && i.npCols === "stacked" && i.npOut === 0 && i.chromePct <= 24,
home: (i) => /^(Good |Still)/.test(i.title),
"hold-sheet": (i) => i.half.includes("up") && /Re-fetch the cover/.test(i.halfText),
"cover-refetched": (i) => !i.half.includes("up") && /Cover re-fetched/.test(i.notice),
@@ -1088,6 +1165,21 @@ async function main() {
if (s.adb) {
try { adb(...s.adb); } catch (e) { problems.push("ADB " + (s.name || s.adb.join(" ")) + ": " + e.message); }
}
/* a mock step can turn the phone over: the viewport, through the
same override the walk boots with. A real device's orientation
is not the harness's to set, so a view step on the phone walk
is a failure rather than a no-op. send() resolves with the
whole reply and does not reject on a protocol error, so a
refused override has to be read out of it. */
if (s.view) {
if (PHONE) problems.push("VIEW " + (s.name || s.view.join("x")) + ": the phone walk must not set the viewport");
else {
try {
const r = await cdp.send("Emulation.setDeviceMetricsOverride", { width: s.view[0], height: s.view[1], deviceScaleFactor: 2, mobile: true });
if (r.error) throw new Error(r.error.message || JSON.stringify(r.error));
} catch (e) { problems.push("VIEW " + (s.name || s.view.join("x")) + ": " + e.message); }
}
}
if (s.do) {
try {
/* the helpers live in the page; a reload step wipes them */