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']); +});