From c66cf6f0fdce8a2801f7851cb7fd4e083963aa3f Mon Sep 17 00:00:00 2001 From: Radoslaw Nowacki Date: Tue, 15 Sep 2026 14:16:50 +0200 Subject: [PATCH] test(e2e): capture Detox artifacts on Android E2E failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android's Detox config had no `artifacts` section, so every plugin sat at Detox's all-off defaults (log: 'none', screenshot: 'manual'). Both iOS configs opt in via the shared getDetoxArtifactsConfig() helper; Android was the only one that never did. The result: an Android E2E failure produced no logcat, no screenshot and nothing to upload — see run 33749895553, where the upload step logged "No files were found with the provided path: apps/AndroidApp/artifacts". Reuse the shared helper, with video disabled. On iOS Detox records host-side via `simctl io recordVideo` (note the helper's iOS-only video.simulator.codec key); on Android it would be `adb shell screenrecord` writing into the emulator userdata partition that this action already documents as ENOSPC-prone. A unit test locks video off for Android and asserts the shared helper still has it on, so iOS cannot regress. The helper writes to e2e-artifacts/, so the upload path, if-no-files-found and retention-days now match appleapp-road-test, and .gitignore matches AppleApp and RNApp. Co-Authored-By: Claude Opus 5 (1M context) --- .../actions/androidapp-road-test/action.yml | 5 +- apps/AndroidApp/.gitignore | 3 + .../detox-rc-androidapp-emulator-release.cjs | 27 +++++++ ...ox-rc-androidapp-emulator-release.test.cjs | 80 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.test.cjs diff --git a/.github/actions/androidapp-road-test/action.yml b/.github/actions/androidapp-road-test/action.yml index 7369783f..e4f374d8 100644 --- a/.github/actions/androidapp-road-test/action.yml +++ b/.github/actions/androidapp-road-test/action.yml @@ -414,5 +414,6 @@ runs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ inputs.e2e-artifact-name }}-${{ inputs.flavor }}-android - path: apps/AndroidApp/artifacts - if-no-files-found: ignore + path: apps/AndroidApp/e2e-artifacts + if-no-files-found: warn + retention-days: 5 diff --git a/apps/AndroidApp/.gitignore b/apps/AndroidApp/.gitignore index aa724b77..23adb538 100644 --- a/apps/AndroidApp/.gitignore +++ b/apps/AndroidApp/.gitignore @@ -13,3 +13,6 @@ .externalNativeBuild .cxx local.properties + +# Detox E2E artifacts (logcat, screenshots, failure diagnostics) +/e2e-artifacts diff --git a/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.cjs b/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.cjs index 7c4f9c86..a7fa16a1 100644 --- a/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.cjs +++ b/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.cjs @@ -1,6 +1,32 @@ 'use strict'; const { resolveAndroidDetoxDevice } = require('./detox-android-emulator-device.cjs'); +const { getDetoxArtifactsConfig } = require('./detox-artifacts-config.cjs'); + +/** + * Shared Detox artifacts, minus video. + * + * The shared helper enables video for iOS simulators, where Detox records + * host-side via `simctl io recordVideo`. On Android it would record with + * `adb shell screenrecord`, writing into the emulator userdata partition — + * the partition androidapp-road-test already documents as ENOSPC-prone + * ("Do not set disk-size — a large userdata partition fails when the runner + * is low on disk after Gradle/NDK builds"). Logcat, screenshots and the + * UI hierarchy give us what we need without that risk. + * + * @returns {import('detox').DetoxArtifactsConfig} + */ +function buildAndroidArtifactsConfig() { + const shared = getDetoxArtifactsConfig(); + + return { + ...shared, + plugins: { + ...shared.plugins, + video: { enabled: false }, + }, + }; +} /** * Detox Android emulator release config for AndroidApp (native Gradle consumer). @@ -41,6 +67,7 @@ function createAndroidAppEmulatorReleaseDetoxConfig({ setupTimeout: 300000, }, }, + artifacts: buildAndroidArtifactsConfig(), behavior: { cleanup: { // CI owns emulator lifecycle via android-emulator-runner. diff --git a/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.test.cjs b/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.test.cjs new file mode 100644 index 00000000..3eba0b05 --- /dev/null +++ b/apps/brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.test.cjs @@ -0,0 +1,80 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const { + getDetoxArtifactsConfig, +} = require('./detox-artifacts-config.cjs'); +const { + createAndroidAppEmulatorReleaseDetoxConfig, +} = require('./detox-rc-androidapp-emulator-release.cjs'); + +test('writes artifacts where CI uploads from, matching the iOS convention', () => { + const config = createAndroidAppEmulatorReleaseDetoxConfig({ + gradleFlavor: 'expo56', + }); + + assert.equal(config.artifacts.rootDir, 'e2e-artifacts'); + assert.equal(config.artifacts.rootDir, getDetoxArtifactsConfig().rootDir); +}); + +test('captures logcat only for failed tests', () => { + const config = createAndroidAppEmulatorReleaseDetoxConfig({ + gradleFlavor: 'expo56', + }); + + // Detox shorthand: record the log plugin, keep it only for failing tests. + assert.equal(config.artifacts.plugins.log, 'failing'); +}); + +test('captures a screenshot when a test finishes failing', () => { + const config = createAndroidAppEmulatorReleaseDetoxConfig({ + gradleFlavor: 'expo56', + }); + + assert.equal( + config.artifacts.plugins.screenshot.keepOnlyFailedTestsArtifacts, + true + ); + assert.equal(config.artifacts.plugins.screenshot.takeWhen.testDone, true); +}); + +test('keeps video off on Android', () => { + const config = createAndroidAppEmulatorReleaseDetoxConfig({ + gradleFlavor: 'expo56', + }); + + // The shared helper enables video for iOS simulators (simctl, host-side). + // On Android it would be `adb shell screenrecord` writing into the emulator + // userdata partition — the partition the road-test action already documents + // as ENOSPC-prone ("Do not set disk-size ... low on disk after Gradle/NDK + // builds"). Keep it off here; iOS is unaffected. + assert.equal(config.artifacts.plugins.video.enabled, false); + assert.equal(getDetoxArtifactsConfig().plugins.video.enabled, true); +}); + +test('inherits the remaining shared artifact plugins', () => { + const config = createAndroidAppEmulatorReleaseDetoxConfig({ + gradleFlavor: 'expo56', + }); + + assert.equal( + config.artifacts.plugins.uiHierarchy, + getDetoxArtifactsConfig().plugins.uiHierarchy + ); +}); + +test('keeps the existing app and device wiring intact', () => { + const config = createAndroidAppEmulatorReleaseDetoxConfig({ + gradleFlavor: 'expo56', + detoxConfiguration: 'android.emu.release.expo56', + jestConfigPath: 'e2e/jest.config.expo56.cjs', + }); + + assert.equal( + config.apps['android.release'].binaryPath, + 'app/build/outputs/apk/expo56/release/app-expo56-release.apk' + ); + assert.equal(config.apps['android.release'].launchTimeout, 300000); + assert.equal(config.behavior.cleanup.shutdownDevice, false); + assert.ok(config.configurations['android.emu.release.expo56']); +});