Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f31331c83b | ||
|
|
6a22601af7 | ||
|
|
ce9951b93a | ||
|
|
99a45401c7 |
@@ -524,6 +524,61 @@ Four things about it, each of which costs an hour if met cold:
|
|||||||
app.yellowjacket.dev android.permission.READ_MEDIA_AUDIO` (and
|
app.yellowjacket.dev android.permission.READ_MEDIA_AUDIO` (and
|
||||||
`POST_NOTIFICATIONS`) ahead of the launch skips it.
|
`POST_NOTIFICATIONS`) ahead of the launch skips it.
|
||||||
|
|
||||||
|
### Getting the app into a state worth measuring
|
||||||
|
|
||||||
|
A fresh install is **not** a neutral starting point, and three things
|
||||||
|
about it will each cost you a measurement.
|
||||||
|
|
||||||
|
**It downloads the real catalog.** `YJ_CORE_INDEX_URL` is stubbed in
|
||||||
|
`dev-headless.sh` and in CI and is *real* here, so the app spends its
|
||||||
|
first minutes fetching ~0.6 GB and `job-band` is **103px of a 439px
|
||||||
|
screen** while it does. Every vertical number taken in that state is
|
||||||
|
wrong -- one #51 measurement had the album art at 0px and it was
|
||||||
|
entirely this.
|
||||||
|
|
||||||
|
`__yj.call("explore.Service.StopIndexBuild", [])` stops it and returns
|
||||||
|
cleanly. **It then starts again within seconds.** So stop it
|
||||||
|
*immediately before* the measurement rather than once at the beginning,
|
||||||
|
and check `jobs.Service.GetJobs` afterwards -- an empty array is the
|
||||||
|
only proof. `jobs.Service.ClearFinishedJobs` tidies the finished rows
|
||||||
|
that otherwise keep the band open.
|
||||||
|
|
||||||
|
**A library added over the bridge does not dismiss the first-run
|
||||||
|
wizard.** `library.Library.AddLibrary` works and scans, but the wizard
|
||||||
|
checks for an existing library once, on mount, and its "Get Started"
|
||||||
|
button gates on a directory chosen *in the wizard* -- so it stays up
|
||||||
|
with a correctly disabled button over everything you are trying to
|
||||||
|
measure. Nothing is broken; reload the page and it is gone. This reads
|
||||||
|
exactly like a tap being swallowed, which is the expensive part.
|
||||||
|
|
||||||
|
**Scoped storage decides where the music can be.** `/sdcard/Music/...`
|
||||||
|
plus `pm grant <pkg> android.permission.READ_MEDIA_AUDIO` works and
|
||||||
|
`AddLibrary` takes the plain path; a push into
|
||||||
|
`/sdcard/Android/data/<pkg>/files/` looks like it worked and then is not
|
||||||
|
there. Some builds additionally want
|
||||||
|
`appops set <pkg> MANAGE_EXTERNAL_STORAGE allow`, and until they have it
|
||||||
|
the app opens the *system* "All files access" screen on launch -- so
|
||||||
|
`dumpsys window | grep mCurrentFocus` naming `com.android.settings` is
|
||||||
|
that, not a crash.
|
||||||
|
|
||||||
|
### A note on quoting `make android-eval`
|
||||||
|
|
||||||
|
`EXPR='...'` is a single-quoted shell word, so anything with a quote or
|
||||||
|
an apostrophe in it -- a file path like `Blazo, 49'ers - ...`, or a
|
||||||
|
snippet containing a string literal -- breaks in a way that reads as a
|
||||||
|
JavaScript error. Put the expression in a file and pass it positionally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ./scripts/android-eval.mjs "$(cat /tmp/probe.js)"
|
||||||
|
```
|
||||||
|
|
||||||
|
That is the same script `make android-eval` wraps, so nothing is lost.
|
||||||
|
Two things worth knowing about it: it does **not** await a promise, so
|
||||||
|
an async call has to park its result (`window.__r = ...`) and be read
|
||||||
|
back in a second eval; and the shim from the section below is lost on
|
||||||
|
every reload and every app restart, along with the devtools socket,
|
||||||
|
whose name carries the pid.
|
||||||
|
|
||||||
### Calling a binding on the device
|
### Calling a binding on the device
|
||||||
|
|
||||||
**The runtime call does not go over HTTP on Android**, and this is worth
|
**The runtime call does not go over HTTP on Android**, and this is worth
|
||||||
|
|||||||
+22
-1
@@ -4867,12 +4867,33 @@ target is 6px tall.
|
|||||||
|
|
||||||
**What the audit did not find is a reachability failure**, which is
|
**What the audit did not find is a reachability failure**, which is
|
||||||
worth recording because it is the promise plan 018 makes. At 424x439,
|
worth recording because it is the promise plan 018 makes. At 424x439,
|
||||||
on all ten primary views plus the queue, `documentElement.scrollWidth`
|
on every view -- the ten primary ones, the queue, `album-details`,
|
||||||
|
`artist-details`, Downloads and Autotag -- `documentElement.scrollWidth`
|
||||||
is 424 against a 424 viewport, no control sits outside a scrollable
|
is 424 against a 424 viewport, no control sits outside a scrollable
|
||||||
ancestor, and a hit test at each control's centre reaches the control.
|
ancestor, and a hit test at each control's centre reaches the control.
|
||||||
The width work of #57, #62, #55 and #59 holds; what was left was
|
The width work of #57, #62, #55 and #59 holds; what was left was
|
||||||
vertical, and it was this screen.
|
vertical, and it was this screen.
|
||||||
|
|
||||||
|
**A number measured on the device is not a number CI can assert.** The
|
||||||
|
spec's floor on the art's height passed here at 114 and failed in CI at
|
||||||
|
**64**, and both are honest: this app is long-lived, so a job staged by
|
||||||
|
an earlier spec is still on screen, and `volume-control` renders in a
|
||||||
|
browser where it does not on Android. Both are chrome above and below
|
||||||
|
the view and both move the leftover. That is the same trap the entry
|
||||||
|
above about staged jobs describes, arriving as a *measurement* rather
|
||||||
|
than as a stuck job. The assertion is the mechanism now -- in a row the
|
||||||
|
art fills the row's height rather than being the leftover -- and the
|
||||||
|
53-to-143 stays on the issue, where it was measured.
|
||||||
|
|
||||||
|
The probe is worth keeping in mind for the next audit, because two of
|
||||||
|
its three checks needed a second pass to mean anything. "Painted
|
||||||
|
outside the viewport" flags a horizontally scrolling carousel -- the
|
||||||
|
home shelves -- so the real question is whether a *scrollable ancestor*
|
||||||
|
can bring the element back. And a hit test at a control's centre flags
|
||||||
|
everything below the fold in a scroll container, so it only says
|
||||||
|
something once the control is on screen. Both first drafts produced
|
||||||
|
long lists of nothing.
|
||||||
|
|
||||||
## Two traps that cost time on the device, both already written down (2026-08-21)
|
## Two traps that cost time on the device, both already written down (2026-08-21)
|
||||||
|
|
||||||
Recorded because both are in `android-tier.md` and I met them anyway.
|
Recorded because both are in `android-tier.md` and I met them anyway.
|
||||||
|
|||||||
@@ -107,11 +107,19 @@ async function openNowPlaying(page: Page): Promise<void> {
|
|||||||
const el = v?.shadowRoot?.querySelector('.art img, .art .placeholder');
|
const el = v?.shadowRoot?.querySelector('.art img, .art .placeholder');
|
||||||
const t = v?.shadowRoot?.querySelector('.transport');
|
const t = v?.shadowRoot?.querySelector('.transport');
|
||||||
|
|
||||||
if (!stack || !el || !t) return false;
|
if (!el || !t) return false;
|
||||||
|
|
||||||
const dir = getComputedStyle(stack).flexDirection;
|
// A build with no `.stack` at all is the one before this change,
|
||||||
|
// and the squareness assertions are still meaningful against it
|
||||||
|
// -- so this waits for the arrangement only where there is one to
|
||||||
|
// wait for. Otherwise reverting the component to check that these
|
||||||
|
// tests bite produces eight timeouts instead of the measurements
|
||||||
|
// that make the case.
|
||||||
|
if (stack) {
|
||||||
|
const dir = getComputedStyle(stack).flexDirection;
|
||||||
|
|
||||||
if (dir !== (row ? 'row' : 'column')) return false;
|
if (dir !== (row ? 'row' : 'column')) return false;
|
||||||
|
}
|
||||||
|
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ async function boxes(page: Page) {
|
|||||||
// Whichever of the two the track has; both carry the sizing.
|
// Whichever of the two the track has; both carry the sizing.
|
||||||
art: rect('.art img') ?? rect('.art .placeholder'),
|
art: rect('.art img') ?? rect('.art .placeholder'),
|
||||||
artBox: rect('.art'),
|
artBox: rect('.art'),
|
||||||
|
stack: rect('.stack'),
|
||||||
meta: rect('.meta'),
|
meta: rect('.meta'),
|
||||||
transport: rect('.transport'),
|
transport: rect('.transport'),
|
||||||
scrollHeight: v.scrollHeight,
|
scrollHeight: v.scrollHeight,
|
||||||
@@ -239,29 +248,36 @@ test.describe('Now Playing survives a short screen', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The reflow is only worth having if it buys something, and the
|
* What the reflow actually does, stated as a mechanism rather than
|
||||||
* honest form of that is a floor on the device's own viewport
|
* as a number: in a row the art is bounded by the row's *height*,
|
||||||
* rather than a comparison with a layout that is no longer there.
|
* so it fills it — where in a column it is the leftover after the
|
||||||
|
* names, which is what made it 53px.
|
||||||
*
|
*
|
||||||
* The first draft compared the art against the column's leftover
|
* **The pixel count is deliberately not asserted here.** Two drafts
|
||||||
* computed from the boxes on screen, and it passed on the build
|
* tried. The first compared the art against the column's leftover
|
||||||
* before this change as well -- the subtraction goes negative when
|
* computed from the boxes on screen and passed on the broken build,
|
||||||
* the names are taller than the art, which is precisely the broken
|
* because the subtraction goes negative when the names are taller
|
||||||
* state. A test that cannot fail on the defect is not evidence.
|
* than the art — precisely the defect. The second put a floor of
|
||||||
|
* 100px on it, passed locally at 114 and **failed in CI at 64**: this
|
||||||
|
* app is long-lived, so a job staged by an earlier spec is still on
|
||||||
|
* screen, and the volume control renders here where it does not on
|
||||||
|
* Android. Both are chrome above and below this view, and both move
|
||||||
|
* the leftover. A test that asserts how much room CI happened to
|
||||||
|
* have is a test about the runner.
|
||||||
*
|
*
|
||||||
* 100 is a floor, not the measurement: the device draws 143 and CI's
|
* The device numbers — 53px to 143px — are on #51, measured there,
|
||||||
* chrome differs by whatever the volume control adds, so pinning the
|
* which is the only tier that can honestly produce them.
|
||||||
* exact number would make this a test about the runner.
|
|
||||||
*/
|
*/
|
||||||
test('gives the art a real size on the reference device', async ({ app }) => {
|
test('fills the row with the art rather than the leftover', async ({ app }) => {
|
||||||
await app.setViewportSize(DEVICE);
|
await app.setViewportSize(DEVICE);
|
||||||
await openNowPlaying(app);
|
await openNowPlaying(app);
|
||||||
|
|
||||||
const b = await boxes(app);
|
const b = await boxes(app);
|
||||||
|
|
||||||
|
expect(b!.stack, 'there is no row to fill').not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
b!.art!.height,
|
Math.abs(b!.artBox!.height - b!.stack!.height),
|
||||||
'the art is still a sliver at the size #51 is about',
|
'the art does not fill the row, so it is still a leftover',
|
||||||
).toBeGreaterThan(100);
|
).toBeLessThanOrEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user