Skip to content

Refresh the wiki's screenshots from the running app, and get the last Settings row out from under the navigation bar - #155

Merged
parawanderer merged 9 commits into
mainfrom
feat/wiki-captures-and-settings-padding
Aug 23, 2026
Merged

Refresh the wiki's screenshots from the running app, and get the last Settings row out from under the navigation bar#155
parawanderer merged 9 commits into
mainfrom
feat/wiki-captures-and-settings-padding

Conversation

@parawanderer

Copy link
Copy Markdown
Owner

The wiki's images came from several phones over several years, and nothing recorded how any of them was taken. This makes them reproducible, and fixes the things that turned up while doing it.

The fix worth reviewing

The last row of Settings sat under the navigation bar. The theme makes it transparent, so the scrolling area runs underneath, and the debug switch is last with nothing below it.

Measured on a Pixel 9 emulator, scrolled all the way down: the switch ended at 2350 with the bar starting at 2361. Eleven pixels, on gesture navigation. Three-button navigation asks for around 126px instead of 63, and those eleven pixels become a switch nobody can tap.

The scroll area now reserves the measured inset, and clipToPadding="false" makes that space scrollable rather than a strip that clips the rows passing through it. No new dimension — the child already leaves 20dp under its content, which is the visual gap; what was missing was the bar's own space.

The test asserts the reserved space as well as the geometry, and the reserved space is the half that bites. A purely geometric assertion is green with the fix removed, because of those eleven pixels — it would only fail on a device this suite never runs on. Checked both ways: passes with the fix, fails without it with reserves 0px, needs 63px.

A real bug in a shared fixture

AMapWithTagsOnIt.putItBack had guard.restore() last in a plain sequence, so a test that failed hard enough to upset the scenario close or the Python double took the settings and the stored session down with it. The next class then met somebody signed in who should not have been, and failed with NoActivityResumedException — nowhere near anything mentioning a session. It is in a finally now. This affects every test using that fixture, not only the new ones.

The captures themselves

Three classes covering 21 screens: the iCloud import route, the export flow, signing in, Settings and its dialogs, and turning on debug data to get a log out.

They are skipped unless asked for. Each ends in a Shot and asserts almost nothing, so a green run of them says nothing about the app — and they added about twenty minutes to a suite people are meant to run constantly. Same opt-in mechanism as the Anisette live tests:

ANDROID_SERIAL=emulator-5554 ./gradlew :app:connectedDebugAndroidTest \
  -Pandroid.testInstrumentationRunnerArguments.captureScreenshots=true \
  -Pandroid.testInstrumentationRunnerArguments.class=dev.wander.android.opentagviewer.ui.WikiScreenshotsTest

Everything on them is fabricated except the map: the session, the tags, the locations, the phone numbers and the address. The pin is the Eiffel Tower and the geocoder answers "a made-up street, in a made-up town".

blur.py takes bands out of a screenshot by height fraction, for the AMap key dialog — it prints the build's own package name and signing fingerprint, which is exactly what AMap's console asks you to paste, and not something to put on a public page.

Two things measured rather than guessed, now in CONTRIBUTING

  • The map captures are slow, and it is the teardown. ~50s per test, of which ~45s is putItBack; setup is about two seconds. Recorded because the real Google map, the dialog waits and the tile sleep all look guilty and none of them is — I chased all three before measuring.
  • A PR stacked on another PR runs no checks at all. Every workflow filters pull_request: branches: ["main"], and that matches the base. The checks section is absent rather than red or pending, which reads as "nothing to run here" instead of "nothing ran". Re-read the account when somebody would notice, and explain the device once #153 sat like that.

Verified

  • JVM suite green; translation check green (301 strings, 9 locales)
  • The padding test run both with and without the fix, confirming it can fail
  • All 21 captures produced on a windowed Pixel 9 emulator
  • Confirmed the capture classes now skip by default: that class went from six minutes to 14 seconds

Not verified: any physical device, and any navigation mode other than this emulator's gesture bar — the three-button figure above is Android's documented size, not something I measured here.

🤖 Generated with Claude Code

parawanderer and others added 6 commits August 23, 2026 22:50
Ten screens the wiki needs - the iCloud flow from picking whose passcode you have
through to the device this app registers, and the three steps of sharing a tag.
Written as a test so regenerating them after a UI change is one command rather
than ten by hand, which is the difference between screenshots that stay true and
screenshots nobody dares touch.

It asserts only enough to know it photographed the right screen. That is not
laziness: a picture of the wrong page is worse than none, because nobody checks a
screenshot against the code afterwards. What these screens actually do is covered
by FetchFromICloudErrorsTest, TheDeviceNoteIsShownOnceTest and the export tests.

Everything on them is fabricated - serials, tag names and the bundle code all come
from fakes. They are going on a public page, so that matters more than usual.

**It needs a device with a display**, and says so. Shot.ofTheScreen photographs
the compositor, which is the only way to catch the code dialog and the page behind
it in one frame, and that returns black on the headless managed device. Since Shot
now refuses to write an all-one-colour frame, a headless run leaves no files rather
than ten black ones.

FakeICloudService gains failUnlockWith, because none of its factories produced a
rejected passcode and the wrong-code state is one of the screens worth documenting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the wiki

Three more flows the wiki documents with images from another phone and another
year: signing in, changing the map provider, and getting a log out of the app.

The map ones need the real Google map, so they cannot run on the managed device
- no Play Services there, and the capture would be of a blank rectangle. They
run against a windowed emulator instead, and say so.

Two things worth naming:

forgetEverything() now deletes every owned beacon rather than the two this
class wrote. Deleting by id is right for a test; a capture needs a screen
nobody else has touched, and an abandoned run left tags behind that made the
next one die with AmbiguousViewMatcherException on two views called "Bike".

The AMap key dialog prints the package name and signing fingerprint of the
build that photographed it, because that is what AMap's console asks you to
paste. blur.py takes those out by height fraction, pixelating before blurring
so the characters are gone rather than soft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every workflow filters on `pull_request: branches: ["main"]`, and that matches
the base of the PR rather than the branch the work is on. A PR opened against
another PR's branch therefore runs nothing.

Worth writing down because of how it presents: the checks section is absent,
not red and not pending, which reads as "nothing to run here" rather than
"nothing ran". #153 sat like that and looked fine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The theme makes the navigation bar transparent, so the scrolling area runs
underneath it. The debug switch is the last row, and it came to rest behind the
gesture pill.

Measured on a Pixel 9 emulator: scrolled all the way down, the switch ended at
2350 with the bar starting at 2361. Eleven pixels, on gesture navigation. Three
button navigation asks for around 126px instead of 63, and those eleven pixels
become a switch nobody can tap.

The scroll area now reserves the measured inset, and clipToPadding="false"
makes that space scrollable rather than a strip that clips the rows passing
through it. No new dimension: the child already leaves 20dp under its content,
which is the visual gap - what was missing was the bar's own space.

The test asserts the reserved space as well as the geometry, and the reserved
space is the half that bites. A purely geometric assertion is green with the
fix removed, because of those eleven pixels - it would only fail on a device
this suite never runs on. Checked both ways: passes with the fix, fails without
it with "reserves 0px, needs 63px".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the blur

putItBack had guard.restore() last in a plain sequence, so a test that failed
hard enough to upset the scenario close or the Python double took the settings
and the stored session with it. The next class then met somebody signed in who
should not have been, and failed with NoActivityResumedException - nowhere near
anything that mentions a session. It is in a finally now.

The capture classes get the fixes that came out of running them: the Anisette
dialog is waited for by id rather than by a label that moved into helper text
when the dialog grew a mode dropdown, the debug switch is scrolled to before
being asserted on screen rather than after, the 2FA row is matched on the phone
number inside "SMS (+44 ...)" rather than equal to it, and the signed-out class
signs out itself instead of inheriting it from whatever ran before.

blur.py takes --x, because a band across the full width of a screenshot of a
dialog also blurs the dimmed page behind it and leaves a pale rectangle running
off both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
They are a documentation tool, not tests: each ends in a screenshot and asserts
almost nothing, so a green run of them says nothing about the app. Left
switched on they added about twenty minutes to a suite people are meant to run
constantly, which is how a suite stops being run. Same opt-in mechanism as the
Anisette live tests.

Skipped rather than deleted, because the point of them is that the images can
be made again. The wiki's previous set came from several phones over several
years and nothing recorded how any of it was taken.

The map class is slow, and while documenting that I measured where it goes:
about 50s per test, of which ~45s is AMapWithTagsOnIt.putItBack. Setup is two
seconds. Written down in CONTRIBUTING because the map, the dialog waits and the
tile sleep all look responsible and none of them is - I chased all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fail

The gate was in @before, and an assumption failure there skips the test body
but runs @after regardless - which released Intents that were never
initialised. Every capture reported FAILED with "init() must be called prior to
using this method", which is a worse outcome than leaving them switched on:
skipped work that reports as broken.

From @BeforeClass the class is passed over whole and neither hook runs.

Checked both directions on a device: the three classes skip with exit 0 in
eleven seconds, and with captureScreenshots=true the signing-in class still
produces all four of its screens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The geometric assertion measured the switch with getLocationOnScreen and the
navigation bar with decor.getHeight() - a screen position against a height.
Those coincide only when the decor view starts at y=0, which it does on the
Pixel 9 and does not on the managed aosp-atd device, so CI reported a 54px
overlap on a device whose inset is zero and which therefore has no navigation
bar to overlap with.

Also written down: on aosp-atd the systemBars bottom inset is 0, so both
assertions in this test are vacuously true on the emulator CI runs. It earns
its keep on a device that has a navigation bar. A green CI run is not evidence
this screen is fine, and the comment now says so.

Checked on both: green on aosp-atd, green on a Pixel 9, and still red on the
Pixel 9 with the fix reverted - "reserves 0px, needs 63px".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…at all

@BeforeClass passed the classes over in silence, and the instrumentation then
reported fewer results than the APK declares tests - so AGP called the whole
run aborted: "Expected 640 tests, received 623". The 17 missing were exactly
the contents of these three classes. Zero failures, a red build, and it
presented as "could not connect to TCP port 5554", which reads as the emulator
dying and is documented in AGENTS.md as exactly that. The emulator's own log
said EXIT CODE 0.

So the assumption goes back in @before, where each test is reported as skipped
and counted, and each @after returns early rather than undoing work that was
never done - which is what made @before untenable the first time.

Verified with a full managed-device run: 640 tests reported, 22 skipped (5
Anisette, 17 captures), no abort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@parawanderer
parawanderer merged commit ecda754 into main Aug 23, 2026
3 checks passed
@parawanderer
parawanderer deleted the feat/wiki-captures-and-settings-padding branch August 23, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant