From c276cc84ba07a5c334153a44bdc6da4923b6ffa7 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 3 Oct 2026 15:53:38 +0200 Subject: [PATCH] Give Pro and Lite their own edit screenshot Lite dims pro's tools behind a badge in an edit, but both listings got the pro picture, so the lite listing showed tools that lite does not have. ScreenshotTests now takes the edit a second time as 04-edit-lite. The run is a pro build, so the test locks the strip itself before it taps into the page. Nothing in the app changes. store_screenshots.py --stage takes --app, and puts 04-edit-lite in place of 04-edit for lite. frame-screenshots.py gives it the headline and the place of 04-edit. The Fastfile stages each flavor's own set. This is the same change as OpenDocument.ios#210. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VTmf15E1oFAe5PrtjT2Cz8 --- CLAUDE.md | 2 + .../droid/test/ScreenshotTests.kt | 32 ++++++++++--- fastlane/Fastfile | 19 +++++--- scripts/frame-screenshots.py | 3 ++ scripts/store_screenshots.py | 45 ++++++++++++++----- 5 files changed, 78 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24d48bfea8a9..c379bc1a8bde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -219,6 +219,8 @@ back door to the app. Details: which locale reads which language's documents. Do not copy that table into the test. - The tablet set goes into both tablet slots, because Play falls back to the phone set only where a slot is empty. +- The edit is taken twice. `04-edit-lite` locks the strip from the test, because the run is + a pro build, and `store_screenshots.py --app lite` stages it in place of `04-edit`. - In `release.yml` the listing is not gated on the screenshots. Do not put them back into a plain `needs:`, or a wedged emulator takes the listing text down with it. diff --git a/app/src/androidTest/java/app/opendocument/droid/test/ScreenshotTests.kt b/app/src/androidTest/java/app/opendocument/droid/test/ScreenshotTests.kt index 7b70c9d54f41..6c1c418344a1 100644 --- a/app/src/androidTest/java/app/opendocument/droid/test/ScreenshotTests.kt +++ b/app/src/androidTest/java/app/opendocument/droid/test/ScreenshotTests.kt @@ -43,6 +43,7 @@ import app.opendocument.droid.ui.OpenFileIdling import app.opendocument.droid.ui.activity.DocumentFragment import app.opendocument.droid.ui.activity.MainActivity import app.opendocument.droid.ui.widget.DocumentActions +import app.opendocument.droid.ui.widget.EditingTools import app.opendocument.droid.ui.widget.PageView import java.io.File import java.io.FileOutputStream @@ -62,9 +63,10 @@ import org.junit.runner.RunWith * The pictures the play store shows. * * Six screens, in every locale the listing is written in, on whichever device the runner is - * driving. `scripts/frame-screenshots.py` then frames what this writes and - * `scripts/store_screenshots.py` checks the set and stages it for supply - the names below are the - * names those expect, and their order is the order the store shows them in. + * driving. The edit is taken twice, the second time as lite shows it - see [editing]. + * `scripts/frame-screenshots.py` then frames what this writes and `scripts/store_screenshots.py` + * checks the set and stages it for supply - the names below are the names those expect, and their + * order is the order the store shows them in. * * Which locales those are, and which language's documents each of them reads, comes out of * `screenshot-names.json` beside the samples: `scripts/store_screenshots.py` holds that table and @@ -165,7 +167,8 @@ class ScreenshotTests { landing(locale) searching(locale, folder.getValue("text"), words.getString("search")) - editing(locale, folder.getValue("text")) + editing(locale, folder.getValue("text"), Shot.EDIT) + editing(locale, folder.getValue("text"), Shot.EDIT_LITE) document(locale, Shot.SHEET, folder.getValue("sheet")) document(locale, Shot.PDF, folder.getValue("paper")) document(locale, Shot.OFFICE, folder.getValue("word")) @@ -201,12 +204,16 @@ class ScreenshotTests { /** * The same text document, being edited, with the keyboard up. * + * [Shot.EDIT_LITE] is the edit as lite shows it: pro's tools dimmed behind its badge. The run + * is a pro build, so the test locks the strip itself, before the tap - the selection the tap + * makes then reaches the locked strip as it does in lite. Nothing in the app knows about it. + * * The keyboard needs a real tap: WebKit raises it for a gesture it saw, so an edit staged * entirely in code sets a caret and nothing else. Where the text is depends on the page, so * this works down the page rather than betting the run on one offset - the sample is a page of * A4 and a tap into its margin reaches nothing. */ - private fun editing(locale: String, uri: Uri) { + private fun editing(locale: String, uri: Uri, shot: Shot) { val activity = launchWith(uri) val fragment = documentFragment(activity) @@ -224,6 +231,16 @@ class ScreenshotTests { waitFor(EDIT_TIMEOUT_MS) { isEditable(pageView) }, ) + // after the page turned editable, so nothing of the edit mode's own start lays the + // strip out again over it + if (shot == Shot.EDIT_LITE) { + instrumentation.runOnMainSync { + activity + .findViewById(R.id.editing_tools) + .showFormatting(sheet = false, locked = true) + } + } + Assert.assertTrue( "no tap down the page set a caret, so the keyboard never came up", KEYBOARD_OFFSETS.any { offset -> @@ -239,7 +256,7 @@ class ScreenshotTests { // keyboard halfway up settle() - shoot(locale, Shot.EDIT) + shoot(locale, shot) finish() } @@ -653,6 +670,9 @@ class ScreenshotTests { TEXT("02-text"), SHEET("03-sheet"), EDIT("04-edit"), + + /** Lite's in place of [EDIT]; `store_screenshots.py` stages it for the lite listing. */ + EDIT_LITE("04-edit-lite"), PDF("05-pdf"), OFFICE("06-office"), } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0efd4f19f6fe..6fd4ed5063c3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -124,16 +124,23 @@ end # captured has to have all of it: half a set is worse in the store than the set # already up there, which is what the script checks before it copies anything. # +# Each flavor gets its own set: lite's edit shows pro's tools locked. +# # @return whether there were any, which is also what tells supply to replace what # the store has rather than leave it alone. -def stage_screenshots(staged) +def stage_screenshots(staged, flavor) captured = !Dir.glob(File.join(FRAMED_DIR, "*", "*.png")).empty? unless captured UI.important("no screenshots under #{FRAMED_DIR} - writing the listing text only") return false end - ok = system("python3", STORE_SCREENSHOTS, "--screenshots", FRAMED_DIR, "--stage", staged) + ok = system( + "python3", STORE_SCREENSHOTS, + "--screenshots", FRAMED_DIR, + "--stage", staged, + "--app", flavor.downcase + ) UI.user_error!("the screenshots could not be staged - see above") unless ok true @@ -246,7 +253,7 @@ platform :android do version = require_version(options[:version]) staged = stage_listing(flavor, version) - captured = stage_screenshots(staged) + captured = stage_screenshots(staged, flavor) upload_to_play_store( track: options[:track] || DEFAULT_TRACK, @@ -321,9 +328,9 @@ platform :android do FileUtils.rm_rf(ADDITIONAL_OUTPUT) # The Pro flavor: it links no ad sdk, so no consent form can come up in front - # of a picture. The two apps are the same app, and what differs - the banner - # Lite carries - is not in a screenshot either way, so one set of pictures - # goes to both listings. + # of a picture. The banner Lite carries is in no screenshot. The edit is the + # one screen that differs, so the test takes it a second time with the tools + # locked, and each listing gets its own set. gradle( task: "connectedProDebugAndroidTest", properties: { diff --git a/scripts/frame-screenshots.py b/scripts/frame-screenshots.py index ccc9d2951b58..d0188ba013e5 100755 --- a/scripts/frame-screenshots.py +++ b/scripts/frame-screenshots.py @@ -868,6 +868,9 @@ def main(argv=None): # the capture run writes the device into the name, being the only # thing that knows which emulator it was driving device, name = store.named(path.stem) + # lite's edit is framed as the screen it stands in for: the same + # headline, and the same place on the line that crosses the set + name = next((pro for pro, lite in store.LITE.items() if lite == name), name) if device is None or name not in screens: print(f"{locale}: skipping {path.name}, which no screen is named after", file=sys.stderr) diff --git a/scripts/store_screenshots.py b/scripts/store_screenshots.py index 2cdd4baf57dd..86cd905ef4bd 100755 --- a/scripts/store_screenshots.py +++ b/scripts/store_screenshots.py @@ -10,7 +10,7 @@ # # scripts/store_screenshots.py --languages what to capture # scripts/store_screenshots.py check what was captured -# scripts/store_screenshots.py --stage DIR check it and stage it +# scripts/store_screenshots.py --stage DIR --app A check it and stage A's set # # An underscore in the name, where every other script here has a dash: # `frame-screenshots.py` imports this one, and a dash cannot be imported. @@ -68,6 +68,19 @@ "06-office", ) +# What lite shows in place of a screen of pro's. The edit is the one screen where +# the two apps differ: lite dims pro's tools behind a badge. The run is a pro +# build, and `ScreenshotTests.kt` takes the edit a second time with the tools +# locked. +LITE = { + "04-edit": "04-edit-lite", +} + +APPS = ("pro", "lite") + +# Every picture a run takes. +CAPTURED = SCREENS + tuple(LITE.values()) + # The devices photographed, and the directories supply uploads each one to. Play # keeps a set per form factor and shows the phone one everywhere it has nothing # better, so the tablet set is what makes the listing a tablet listing. @@ -140,7 +153,7 @@ def named(stem): picture is the size of its canvas, and two devices could share one. """ for device in DIRECTORIES: - for screen in SCREENS: + for screen in CAPTURED: if stem == f"{device}-{screen}": return device, screen @@ -187,7 +200,7 @@ def collect(directory): if device is None: problems.append( f"{locale}: {path.name} is not one of " - + ", ".join([f"{d}-{s}" for d in DIRECTORIES for s in SCREENS] + [FEATURE]) + + ", ".join([f"{d}-{s}" for d in DIRECTORIES for s in CAPTURED] + [FEATURE]) ) continue @@ -205,7 +218,7 @@ def collect(directory): pictures.setdefault(device, {})[screen] = path for device in DIRECTORIES: - missing = [screen for screen in SCREENS if screen not in pictures.get(device, {})] + missing = [screen for screen in CAPTURED if screen not in pictures.get(device, {})] if missing: problems.append(f"{locale}: no {device} {', '.join(missing)}") @@ -217,8 +230,8 @@ def collect(directory): return found, features, problems -def stage(found, features, directory): - """Write the pictures into the metadata tree supply uploads. +def stage(found, features, directory, app): + """Write `app`'s pictures into the metadata tree supply uploads. Into the same directory `scripts/store-listing.py` stages the text in, under the `images/` subdirectory supply reads a locale's pictures from - so one @@ -227,7 +240,8 @@ def stage(found, features, directory): The borrowed locales are copied from the English rather than left out: what supply does not upload for a locale, play keeps - which would be whatever was - there before this release. + there before this release. Lite's pictures go under the name of the screen + they stand in for, so they keep its place in the store. """ directory = Path(directory) @@ -241,8 +255,9 @@ def stage(found, features, directory): for name in DIRECTORIES[device]: folder = directory / locale / "images" / name folder.mkdir(parents=True, exist_ok=True) - for screen, path in screens.items(): - shutil.copyfile(path, folder / f"{screen}.png") + for screen in SCREENS: + picture = LITE.get(screen, screen) if app == "lite" else screen + shutil.copyfile(screens[picture], folder / f"{screen}.png") for locale in borrowed(): source = directory / FALLBACK / "images" @@ -282,12 +297,20 @@ def main(argv=None): metavar="DIR", help="also write the screenshots into the supply metadata tree in DIR", ) + parser.add_argument( + "--app", + choices=APPS, + help="whose set --stage writes: lite's edit is not pro's", + ) args = parser.parse_args(argv) if args.languages: print("\n".join(languages())) return 0 + if args.stage and not args.app: + return fail("--stage needs --app, because the two listings differ in the edit") + if not LEAST <= len(SCREENS) <= MOST: return fail(f"play takes {LEAST} to {MOST} screenshots per device, not {len(SCREENS)}") @@ -302,11 +325,11 @@ def main(argv=None): if args.stage: try: - stage(found, features, args.stage) + stage(found, features, args.stage, args.app) except OSError as reason: return fail(str(reason)) print( - f"staged {len(SCREENS)} screenshots per device and a feature graphic for " + f"staged {args.app}'s {len(SCREENS)} screenshots per device and a feature graphic for " f"{len(found) + len(borrowed())} locales in {args.stage}" ) else: