From b6b3c10cb57432d7570b4978790008566cb8bfae Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 14 Aug 2026 12:55:03 +0200 Subject: [PATCH] ci(screenshots): the TV storefronts were the only ones with no automated captures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Google Play's Android TV slot needs 16:9 1920x1080 shots and the App Store needs Apple TV 1920x1080 — neither existed as automation output: - apple.yml screenshots job now runs the tvos leg. The harness supported it all along (tools/screenshots.sh tvos); what the job was missing is the Tier-3 tvOS xcframework slices (nightly + -Zbuild-std, same recipe the distribute job uses on this runner) and an explicit scene list — the gamepad-console scenes are compiled out on tvOS, and an UNKNOWN scene name falls back to a normal app launch, which would silently capture the real empty app. Still best-effort: a tvOS hiccup warns, never reds. - TvScreenshotTest renders the console scenes + the stream HUD at Android TV geometry (w960dp-h540dp-television-xhdpi = native 1920x1080, no resampling), prefixed tv- so the artifact separates the form factors. Verified locally: 6 scenes, all 1920x1080. android-screenshots.yml needs no change — it runs the whole unit-test task and uploads the whole roborazzi output dir. --- .gitea/workflows/apple.yml | 31 +++++++---- .../punktfunk/screenshots/TvScreenshotTest.kt | 55 +++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/TvScreenshotTest.kt diff --git a/.gitea/workflows/apple.yml b/.gitea/workflows/apple.yml index 4bd88fe1..79eefe09 100644 --- a/.gitea/workflows/apple.yml +++ b/.gitea/workflows/apple.yml @@ -676,20 +676,23 @@ jobs: # Skipped on PRs (cost); runs on main pushes + manual dispatch. Needs the build/test job green # first, and is a separate job so a capture hiccup can never red the core signal. # - # Scope = the two REQUIRED iOS sizes (iPhone 6.9" + iPad 13"), captured on the Simulator - # (`simctl io screenshot`, no Screen Recording grant needed). macOS and tvOS are deliberately - # NOT in CI: the self-hosted runner is headless (no window-server session), so the mac window - # capture can't run there; tvOS needs the Tier-3 build-std slice. Generate those two locally on - # a GUI Mac with `clients/apple/tools/screenshots.sh macos tvos`. + # Scope = the two REQUIRED iOS sizes (iPhone 6.9" + iPad 13") + Apple TV (1920×1080), captured + # on the Simulator (`simctl io screenshot`, no Screen Recording grant needed). The tvOS slice is + # Tier-3 (nightly -Zbuild-std, same as the distribute job — slow cold, cached on the self-hosted + # runner). The tvOS scene list is explicit: the gamepad-console scenes are iOS/macOS-only, and an + # unknown scene name falls back to a NORMAL app launch — the capture would silently be of the + # real empty app. macOS stays deliberately NOT in CI: the runner is headless (no window-server + # session), so the mac window capture can't run there — generate it locally on a GUI Mac with + # `clients/apple/tools/screenshots.sh macos`. screenshots: needs: swift if: gitea.event_name != 'pull_request' runs-on: macos-arm64 - timeout-minutes: 75 + timeout-minutes: 90 steps: - uses: actions/checkout@v4 - - name: Rust toolchain + iOS Simulator targets + - name: Rust toolchain + iOS Simulator targets (+ nightly for the tvOS slices) run: | if ! command -v rustup >/dev/null && [ ! -x "$HOME/.cargo/bin/rustup" ]; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ @@ -699,6 +702,10 @@ jobs: dirname "$RUSTUP" >> "$GITHUB_PATH" "$RUSTUP" target add aarch64-apple-darwin x86_64-apple-darwin \ aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios + # tvOS targets are tier-3 (no prebuilt std) — build-xcframework.sh compiles them with + # nightly + -Zbuild-std, so ensure nightly + rust-src are present (see the swift job). + "$RUSTUP" toolchain install nightly --profile minimal + "$RUSTUP" component add rust-src --toolchain nightly # Shared compile cache. The script handles the macOS side (user-prefix install + # GITHUB_PATH, bsdtar globbing) — see scripts/ci/ensure-sccache.sh. @@ -735,10 +742,10 @@ jobs: -mtime +7 -exec rm -rf {} + 2>/dev/null || true fi - - name: Build PunktfunkCore.xcframework (mac + iOS slices) - run: BUILD_IOS=1 bash scripts/build-xcframework.sh + - name: Build PunktfunkCore.xcframework (mac + iOS + tvOS slices) + run: BUILD_IOS=1 BUILD_TVOS=1 bash scripts/build-xcframework.sh - - name: Capture screenshots (iPhone 6.9" + iPad 13"; auto-creates the Simulators) + - name: Capture screenshots (iPhone 6.9" + iPad 13" + Apple TV; auto-creates the Simulators) working-directory: clients/apple env: SETTLE: "8" # Simulators settle slower than a local run @@ -746,6 +753,10 @@ jobs: # Independent invocations: one platform failing skips it, not the other. bash tools/screenshots.sh ios || echo "::warning::iOS (iPhone 6.9\") screenshots skipped" bash tools/screenshots.sh ipad || echo "::warning::iPad 13\" screenshots skipped" + # tvOS shoots only the scenes that exist there — the 06–09 gamepad-console scenes are + # compiled out on tvOS (native focus engine), and an unknown name = a normal app launch. + SCENES="01-stream 02-hosts 05-settings 03-pair" \ + bash tools/screenshots.sh tvos || echo "::warning::Apple TV screenshots skipped" echo "Produced:"; ls -la screenshots || true - name: Shut the Simulators down (leaked booted sims once piled up 846 deep) diff --git a/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/TvScreenshotTest.kt b/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/TvScreenshotTest.kt new file mode 100644 index 00000000..fef490a5 --- /dev/null +++ b/clients/android/app/src/test/kotlin/io/unom/punktfunk/screenshots/TvScreenshotTest.kt @@ -0,0 +1,55 @@ +package io.unom.punktfunk.screenshots + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import com.github.takahirom.roborazzi.captureRoboImage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +/** + * The same Roborazzi harness as ScreenshotTest, at Android TV geometry: 960×540dp in the + * `television` UI mode at xhdpi (2.0×) = 1920×1080 px — the Play Store's 16:9 TV screenshot size, + * captured 1:1 with no resampling. Only the screens that exist on a TV are shot here: the + * gamepad-console shell (what LEANBACK_LAUNCHER opens into) and the in-stream view. Files are + * prefixed `tv-` so the artifact separates the form factors. + */ +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [36], qualifiers = "w960dp-h540dp-television-xhdpi") +class TvScreenshotTest { + @get:Rule + val compose = createAndroidComposeRule() + + private val out = "build/outputs/roborazzi" + + private fun shootRoot(name: String, content: @androidx.compose.runtime.Composable () -> Unit) { + compose.mainClock.autoAdvance = false + compose.setContent { ShotTheme(content) } + compose.mainClock.advanceTimeBy(800) + compose.onRoot().captureRoboImage("$out/tv-$name.png") + } + + @Test + fun stream() = shootRoot("stream") { StreamScene(io.unom.punktfunk.StatsVerbosity.COMPACT) } + + @Test + fun streamDetailed() = + shootRoot("stream-detailed") { StreamScene(io.unom.punktfunk.StatsVerbosity.DETAILED) } + + @Test + fun consoleHome() = shootRoot("console-home") { ConsoleHomeScene() } + + @Test + fun consoleSettings() = shootRoot("console-settings") { ConsoleSettingsScene() } + + @Test + fun consoleControllers() = shootRoot("console-controllers") { ConsoleControllersScene() } + + @Test + fun connectingConsole() = shootRoot("connecting-console") { ConnectConsoleScene() } +} -- 2.54.0