diff --git a/.detoxrc.js b/.detoxrc.js index 2734968db439..af93cbe633c1 100644 --- a/.detoxrc.js +++ b/.detoxrc.js @@ -22,6 +22,7 @@ module.exports = { $0: 'jest', config: 'e2e/jest.e2e.config.js', }, + detached: true, jest: { setupTimeout: 220000, teardownTimeout: 60000, // Increase teardown timeout from default 30s to 60s diff --git a/.github/scripts/e2e-create-test-report.mjs b/.github/scripts/e2e-create-json-test-report.mjs similarity index 97% rename from .github/scripts/e2e-create-test-report.mjs rename to .github/scripts/e2e-create-json-test-report.mjs index 10424247fa8d..decfd69ef507 100644 --- a/.github/scripts/e2e-create-test-report.mjs +++ b/.github/scripts/e2e-create-json-test-report.mjs @@ -3,9 +3,12 @@ import path from 'path'; import xml2js from 'xml2js'; import https from 'https'; +// Converts JUnit XML test reports into a structured JSON report with GitHub job metadata +// and retry tracking. These JSON reports are later on analyzed by the Flaky tests bot daily. + const env = { - TEST_RESULTS_PATH: process.env.TEST_RESULTS_PATH || 'android-merged-test-report', - TEST_RUNS_PATH: process.env.TEST_RUNS_PATH || 'test/test-results/test-runs.json', + TEST_RESULTS_PATH: process.env.TEST_RESULTS_PATH || 'e2e-smoke-android-all-test-artifacts', + TEST_RUNS_PATH: process.env.TEST_RUNS_PATH || 'test/test-results/json-test-report.json', RUN_ID: process.env.RUN_ID ? parseInt(process.env.RUN_ID) : Date.now(), PR_NUMBER: process.env.PR_NUMBER ? parseInt(process.env.PR_NUMBER) : 0, GITHUB_ACTIONS: process.env.GITHUB_ACTIONS === 'true', diff --git a/.github/scripts/e2e-merge-detox-junit-reports.mjs b/.github/scripts/e2e-merge-detox-junit-reports.mjs new file mode 100644 index 000000000000..24e7faea1fd8 --- /dev/null +++ b/.github/scripts/e2e-merge-detox-junit-reports.mjs @@ -0,0 +1,276 @@ +#!/usr/bin/env node + +/** + * Merges multiple Detox junit XML reports into a single deduplicated report. + * + * This is useful when Detox retries failed tests, creating multiple XML + * files per test run, and we want the final report to reflect the actual + * outcome after all retries. + */ + +import { readdir, readFile, writeFile } from 'fs/promises'; +import { join } from 'path'; +import xml2js from 'xml2js'; + +const env = { + REPORTS_DIR: process.env.E2E_REPORTS_DIR || './e2e/reports', + OUTPUT_FILE: process.env.E2E_OUTPUT_FILE || 'junit.xml', +}; + +const xmlParser = new xml2js.Parser(); +const xmlBuilder = new xml2js.Builder({ + xmldec: { version: '1.0', encoding: 'UTF-8' }, + renderOpts: { pretty: true, indent: ' ' }, +}); + +/** + * Extract timestamp from filename (e.g., junit-2025-10-17T12-18-43-667Z.xml) + * @param {string} filename - The filename to extract timestamp from + * @returns {string} Sortable timestamp string + */ +function extractTimestamp(filename) { + const match = filename.match(/junit-(.+)\.xml$/); + return match ? match[1] : ''; +} + +/** + * Find all junit XML files in the reports directory + * @returns {Promise} Sorted array of XML filenames + */ +async function findJUnitXmlFiles() { + try { + const files = await readdir(env.REPORTS_DIR); + const xmlFiles = files + .filter(f => f.startsWith('junit-') && f.endsWith('.xml')) + .sort((a, b) => extractTimestamp(a).localeCompare(extractTimestamp(b))); + + return xmlFiles; + } catch (error) { + if (error.code === 'ENOENT') { + console.warn(`āš ļø Reports directory not found: ${env.REPORTS_DIR}`); + return []; + } + throw error; + } +} + +/** + * Parse a junit XML file + * @param {string} filePath - Path to the XML file + * @returns {Promise} Parsed XML data + */ +async function parseJUnitXML(filePath) { + const xmlContent = await readFile(filePath, 'utf-8'); + return await xmlParser.parseStringPromise(xmlContent); +} + +/** + * Parse all XML files and combine test cases, keeping only the LATEST result for each test + * @returns {Promise} Merged report data or null if no files found + */ +async function parseAndMergeReports() { + console.log('šŸ” Scanning for junit XML files...'); + + const xmlFiles = await findJUnitXmlFiles(); + + if (xmlFiles.length === 0) { + console.log('āš ļø No junit XML files found to merge'); + return null; + } + + console.log(`šŸ“ Found ${xmlFiles.length} report file(s):`); + xmlFiles.forEach(f => console.log(` - ${f}`)); + + // Map to store LATEST result for each test case + // Key: "suiteName::testcaseName::classname" + // Value: { testcase, suiteName, suiteAttrs, timestamp } + const testCaseMap = new Map(); + const suitePropertiesMap = new Map(); + + // Process each XML file in chronological order (earliest to latest) + for (const xmlFile of xmlFiles) { + const filePath = join(env.REPORTS_DIR, xmlFile); + const timestamp = extractTimestamp(xmlFile); + + console.log(`\nšŸ“„ Processing: ${xmlFile}`); + + try { + const parsed = await parseJUnitXML(filePath); + + if (!parsed.testsuites || !parsed.testsuites.testsuite) { + console.log(' āš ļø No test suites found, skipping'); + continue; + } + + const testsuites = Array.isArray(parsed.testsuites.testsuite) + ? parsed.testsuites.testsuite + : [parsed.testsuites.testsuite]; + + for (const testsuite of testsuites) { + const suiteName = testsuite.$.name; + + if (!testsuite.testcase) { + console.log(` āš ļø Suite "${suiteName}" has no test cases`); + continue; + } + + const testcases = Array.isArray(testsuite.testcase) + ? testsuite.testcase + : [testsuite.testcase]; + + console.log(` šŸ“¦ Suite: "${suiteName}" (${testcases.length} test(s))`); + + for (const testcase of testcases) { + const testName = testcase.$.name; + const className = testcase.$.classname || suiteName; + const key = `${suiteName}::${testName}::${className}`; + + const isFailure = !!testcase.failure; + const isError = !!testcase.error; + const isSkipped = !!testcase.skipped; + + // Check if we've seen this test before + const existing = testCaseMap.get(key); + const isRetry = !!existing; + + // Always keep the LATEST result (overwrite previous) + testCaseMap.set(key, { + testcase, + suiteName, + suiteAttrs: testsuite.$, + timestamp, + }); + + const statusIcon = isFailure ? 'āŒ' : isError ? 'āš ļø' : isSkipped ? '⊘' : 'āœ…'; + const retryLabel = isRetry ? ' (retry)' : ''; + console.log(` ${statusIcon} ${testName}${retryLabel}`); + } + + // Track properties (use latest) + if (testsuite.properties) { + suitePropertiesMap.set(suiteName, testsuite.properties); + } + } + } catch (error) { + console.warn(`āš ļø Failed to process ${xmlFile}: ${error.message}`); + continue; + } + } + + console.log('\nšŸ”„ Building deduplicated report with latest results...'); + console.log(` Total unique test cases: ${testCaseMap.size}`); + + // Group test cases by suite name + const suiteMap = new Map(); + + for (const [key, { testcase, suiteName, suiteAttrs }] of testCaseMap) { + if (!suiteMap.has(suiteName)) { + suiteMap.set(suiteName, { + attrs: suiteAttrs, + testcases: [], + properties: suitePropertiesMap.get(suiteName), + }); + } + suiteMap.get(suiteName).testcases.push(testcase); + } + + // Build test suites with recalculated counts + const finalTestSuites = []; + let totalTests = 0; + let totalFailures = 0; + let totalErrors = 0; + let totalSkipped = 0; + let totalTime = 0; + + for (const [suiteName, { attrs, testcases, properties }] of suiteMap) { + // Recalculate suite statistics based on deduplicated test cases + let suiteFailures = 0; + let suiteErrors = 0; + let suiteSkipped = 0; + let suiteTime = 0; + + for (const testcase of testcases) { + if (testcase.failure) suiteFailures++; + if (testcase.error) suiteErrors++; + if (testcase.skipped) suiteSkipped++; + suiteTime += parseFloat(testcase.$.time || '0'); + } + + const suite = { + $: { + ...attrs, + name: suiteName, + tests: testcases.length, + failures: suiteFailures, + errors: suiteErrors, + skipped: suiteSkipped, + time: suiteTime.toFixed(3), + }, + testcase: testcases, + }; + + if (properties) { + suite.properties = properties; + } + + finalTestSuites.push(suite); + + totalTests += testcases.length; + totalFailures += suiteFailures; + totalErrors += suiteErrors; + totalSkipped += suiteSkipped; + totalTime += suiteTime; + } + + // Build final XML structure + const mergedReport = { + testsuites: { + $: { + name: 'jest tests', + tests: totalTests, + failures: totalFailures, + errors: totalErrors, + time: totalTime.toFixed(3), + }, + testsuite: finalTestSuites, + }, + }; + + console.log('\nšŸ“Š Final Report Summary (after deduplication):'); + console.log(` Total Test Suites: ${finalTestSuites.length}`); + console.log(` Total Tests: ${totalTests}`); + console.log(` Failures: ${totalFailures}`); + console.log(` Errors: ${totalErrors}`); + console.log(` Skipped: ${totalSkipped}`); + console.log(` Total Time: ${totalTime.toFixed(3)}s`); + + return mergedReport; +} + + +async function main() { + console.log('šŸš€ Starting JUnit report merge...\n'); + + const mergedReport = await parseAndMergeReports(); + + if (!mergedReport) { + console.log('\nāš ļø No reports to merge, skipping output file creation'); + process.exit(0); + } + + // Convert to XML + const xml = xmlBuilder.buildObject(mergedReport); + + // Write merged report + const outputPath = join(env.REPORTS_DIR, env.OUTPUT_FILE); + await writeFile(outputPath, xml, 'utf-8'); + + console.log(`\nāœ… Merged report written to: ${outputPath}`); + console.log('šŸŽ‰ Merge complete!\n'); +} + +main().catch((error) => { + console.error('\nāŒ Error merging XML reports:', error); + process.exit(1); +}); + diff --git a/.github/scripts/e2e-split-tags-shards.mjs b/.github/scripts/e2e-split-tags-shards.mjs index 0f479109d368..971df95e038a 100644 --- a/.github/scripts/e2e-split-tags-shards.mjs +++ b/.github/scripts/e2e-split-tags-shards.mjs @@ -217,7 +217,7 @@ function computeRetryFilePath(originalPath, retryIndex) { } /** - * Create two retry copies of a given spec if not already present + * Create retry copies of a given spec if not already present * @param {*} originalPath - The original path to the spec file */ function duplicateSpecFile(originalPath) { @@ -225,7 +225,7 @@ function duplicateSpecFile(originalPath) { const srcPath = path.resolve(originalPath); if (!fs.existsSync(srcPath)) return; const content = fs.readFileSync(srcPath); - for (let i = 1; i <= 2; i += 1) { + for (let i = 1; i <= 1; i += 1) { const retryRel = computeRetryFilePath(originalPath, i); if (!retryRel) continue; const retryAbs = path.resolve(retryRel); @@ -304,7 +304,7 @@ function applyFlakinessDetection(splitFiles) { return splitFiles; } - // Build expanded list: base + retry-1 + retry-2 for duplicated files + // Build expanded list: base + retry files for duplicated files const expanded = []; for (const file of splitFiles) { const normalized = normalizePathForCompare(file); @@ -313,9 +313,7 @@ function applyFlakinessDetection(splitFiles) { expanded.push(file); // Add retry files const retry1 = computeRetryFilePath(normalized, 1); - const retry2 = computeRetryFilePath(normalized, 2); if (retry1) expanded.push(retry1); - if (retry2) expanded.push(retry2); } else { // Not changed, add as-is expanded.push(file); @@ -348,7 +346,7 @@ async function main() { // 3) Flaky test detector mechanism in PRs (test retries) // - Only duplicates changed files that are in this shard's split - // - Creates base + retry-1 + retry-2 for flakiness detection + // - Creates base + retry files for flakiness detection const shouldSkipFlakinessGate = await shouldSkipFlakinessDetection(); if (!shouldSkipFlakinessGate) { runFiles = applyFlakinessDetection(splitFiles); diff --git a/.github/workflows/run-e2e-regression-tests-android.yml b/.github/workflows/run-e2e-regression-tests-android.yml index e29ba1dac336..45e771205bcd 100644 --- a/.github/workflows/run-e2e-regression-tests-android.yml +++ b/.github/workflows/run-e2e-regression-tests-android.yml @@ -2,7 +2,7 @@ name: Android E2E Regression Tests on: schedule: - - cron: '0 */3 * * *' # Every 3 hours + - cron: '0 */3 * * *' # Every 3 hours workflow_dispatch: inputs: notify_on_pass: @@ -41,7 +41,6 @@ jobs: total_splits: 4 secrets: inherit - regression-confirmations-redesigned-android: name: 'Confirmations Redesigned Regression (Android) - ${{ matrix.split }}' needs: [build-android-apks] @@ -153,7 +152,7 @@ jobs: split_number: ${{ matrix.split }} total_splits: 2 secrets: inherit - + regression-ux-android: name: 'UX Regression (Android) - ${{ matrix.split }}' needs: [build-android-apks] @@ -189,30 +188,30 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Download all test results + + - name: Download shards test artifacts (XMLs + Screenshots) uses: actions/download-artifact@v4 continue-on-error: true with: - path: all-test-results/ - pattern: '*-android-*-test-results' + path: all-test-artifacts/ + pattern: '*-android-*-test-*' - name: Post Test Report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 with: name: 'Android E2E Regression Test Results' - path: 'all-test-results/**/*.xml' + path: 'all-test-artifacts/**/junit.xml' reporter: 'jest-junit' fail-on-error: false list-suites: 'failed' list-tests: 'failed' - - name: Upload merged report + - name: Upload all test artifacts (XMLs + Screenshots) uses: actions/upload-artifact@v4 continue-on-error: true with: - name: android-merged-test-report - path: all-test-results/**/*.xml + name: e2e-regression-android-all-test-artifacts + path: all-test-artifacts/ - name: Generate Test Summary id: summary @@ -257,4 +256,4 @@ jobs: with: webhook: ${{ secrets.REGRESSION_E2E_SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook - payload-file-path: /tmp/slack-payload.json \ No newline at end of file + payload-file-path: /tmp/slack-payload.json diff --git a/.github/workflows/run-e2e-regression-tests-ios.yml b/.github/workflows/run-e2e-regression-tests-ios.yml index 4a77cc8f7e34..29343a469b7e 100644 --- a/.github/workflows/run-e2e-regression-tests-ios.yml +++ b/.github/workflows/run-e2e-regression-tests-ios.yml @@ -136,7 +136,7 @@ jobs: split_number: ${{ matrix.split }} total_splits: 2 secrets: inherit - + regression-ux-ios: name: 'UX Regression (iOS) - ${{ matrix.split }}' needs: [build-ios-apps] @@ -171,29 +171,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Download all test results + - name: Download shards test artifacts (XMLs + Screenshots) uses: actions/download-artifact@v4 continue-on-error: true with: - path: all-test-results/ - pattern: '*-ios-*-test-results' + path: all-test-artifacts/ + pattern: '*-ios-*-test-*' - name: Post Test Report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 with: name: 'iOS E2E Regression Test Results' - path: 'all-test-results/**/*.xml' + path: 'all-test-artifacts/**/junit.xml' reporter: 'jest-junit' fail-on-error: false list-suites: 'failed' list-tests: 'failed' - - name: Upload merged report + - name: Upload all test artifacts (XMLs + Screenshots) uses: actions/upload-artifact@v4 continue-on-error: true with: - name: ios-merged-test-report - path: all-test-results/**/*.xml + name: e2e-regression-ios-all-test-artifacts + path: all-test-artifacts/ - name: Generate Test Summary id: summary @@ -236,4 +236,4 @@ jobs: with: webhook: ${{ secrets.REGRESSION_E2E_SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook - payload-file-path: /tmp/slack-payload.json \ No newline at end of file + payload-file-path: /tmp/slack-payload.json diff --git a/.github/workflows/run-e2e-smoke-tests-android-flask.yml b/.github/workflows/run-e2e-smoke-tests-android-flask.yml index caf90ce45b1a..6b196f3e27ed 100644 --- a/.github/workflows/run-e2e-smoke-tests-android-flask.yml +++ b/.github/workflows/run-e2e-smoke-tests-android-flask.yml @@ -14,7 +14,7 @@ concurrency: jobs: needs_e2e_build: - name: "Detect Mobile Build Changes" + name: 'Detect Mobile Build Changes' # Execution rules: # - schedule: always run # - merge_group: never run @@ -23,7 +23,7 @@ jobs: uses: ./.github/workflows/needs-e2e-build.yml build-android-flask-apps: - name: "Build Android Flask Apps" + name: 'Build Android Flask Apps' # Execution rules: # - schedule: always run # - merge_group: never run @@ -50,7 +50,7 @@ jobs: with: test-suite-name: flask-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "FlaskBuildTests" + test_suite_tag: 'FlaskBuildTests' split_number: ${{ matrix.split }} total_splits: 3 build_type: 'flask' @@ -63,7 +63,7 @@ jobs: if: ${{ !cancelled() && needs.flask-android-smoke.result != 'skipped' }} needs: - flask-android-smoke - + steps: - name: Checkout uses: actions/checkout@v4 @@ -73,56 +73,25 @@ jobs: with: node-version-file: '.nvmrc' - - name: Download all test results + - name: Download shards test artifacts (XMLs + Screenshots) uses: actions/download-artifact@v4 + continue-on-error: true with: - path: all-test-results/ - pattern: "*-android-*-test-results" + path: all-test-artifacts/ + pattern: '*-android-*-test-*' - name: Post Test Report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 with: - name: "Android E2E Smoke Test Results" - path: "all-test-results/**/*.xml" - reporter: "jest-junit" + name: 'Android Flask E2E Smoke Test Results' + path: 'all-test-artifacts/**/junit.xml' + reporter: 'jest-junit' fail-on-error: false - list-suites: "failed" - list-tests: "failed" - - - name: Create mobile test report - id: create-json-report - continue-on-error: true - run: | - # Create a temporary directory for xml2js to avoid conflicts - mkdir -p temp-deps && cd temp-deps - npm init -y - npm install xml2js@0.5.0 --no-audit --no-fund - - # Copy node_modules to workspace - cp -r node_modules ${{ github.workspace }}/ - - # Run the mobile test report generator - cd ${{ github.workspace }} - TEST_RESULTS_PATH=all-test-results \ - TEST_RUNS_PATH=test/test-results/test-runs-android.json \ - RUN_ID=${{ github.run_id }} \ - PR_NUMBER=${{ github.event.pull_request.number || '0' }} \ - GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ - node .github/scripts/e2e-create-test-report.mjs - - # Clean up temporary node_modules - rm -rf node_modules + list-suites: 'failed' + list-tests: 'failed' - - name: Upload test runs JSON - if: steps.create-json-report.outcome == 'success' + - name: Upload all test artifacts (XMLs + Screenshots) uses: actions/upload-artifact@v4 with: - name: test-e2e-android-report - path: test/test-results/test-runs-android.json - - - name: Upload merged XML report - uses: actions/upload-artifact@v4 - with: - name: android-merged-test-report - path: all-test-results/**/*.xml - + name: e2e-smoke-android-all-test-artifacts + path: all-test-artifacts/ diff --git a/.github/workflows/run-e2e-smoke-tests-android.yml b/.github/workflows/run-e2e-smoke-tests-android.yml index 4f40e8fc4622..6e2f360f17ec 100644 --- a/.github/workflows/run-e2e-smoke-tests-android.yml +++ b/.github/workflows/run-e2e-smoke-tests-android.yml @@ -23,7 +23,7 @@ jobs: with: test-suite-name: trade-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "SmokeTrade" + test_suite_tag: 'SmokeTrade' split_number: ${{ matrix.split }} total_splits: 1 changed_files: ${{ inputs.changed_files }} @@ -38,7 +38,7 @@ jobs: with: test-suite-name: wallet-platform-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "SmokeWalletPlatform" + test_suite_tag: 'SmokeWalletPlatform' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -53,7 +53,7 @@ jobs: with: test-suite-name: identity-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "SmokeIdentity" + test_suite_tag: 'SmokeIdentity' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -68,7 +68,7 @@ jobs: with: test-suite-name: accounts-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "SmokeAccounts" + test_suite_tag: 'SmokeAccounts' split_number: ${{ matrix.split }} total_splits: 1 changed_files: ${{ inputs.changed_files }} @@ -83,7 +83,7 @@ jobs: with: test-suite-name: network-abstraction-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "SmokeNetworkAbstractions" + test_suite_tag: 'SmokeNetworkAbstractions' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -98,26 +98,26 @@ jobs: with: test-suite-name: network-expansion-android-smoke-${{ matrix.split }} platform: android - test_suite_tag: "SmokeNetworkExpansion" + test_suite_tag: 'SmokeNetworkExpansion' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} secrets: inherit confirmations-redesigned-android-smoke: - strategy: - matrix: - split: [1, 2, 3] - fail-fast: false - uses: ./.github/workflows/run-e2e-workflow.yml - with: - test-suite-name: confirmations-redesigned-android-smoke-${{ matrix.split }} - platform: android - test_suite_tag: "SmokeConfirmationsRedesigned" - split_number: ${{ matrix.split }} - total_splits: 3 - changed_files: ${{ inputs.changed_files }} - secrets: inherit + strategy: + matrix: + split: [1, 2, 3] + fail-fast: false + uses: ./.github/workflows/run-e2e-workflow.yml + with: + test-suite-name: confirmations-redesigned-android-smoke-${{ matrix.split }} + platform: android + test_suite_tag: 'SmokeConfirmationsRedesigned' + split_number: ${{ matrix.split }} + total_splits: 3 + changed_files: ${{ inputs.changed_files }} + secrets: inherit report-android-smoke-tests: name: Report Android Smoke Tests @@ -141,23 +141,30 @@ jobs: with: node-version-file: '.nvmrc' - - name: Download all test results + - name: Download shards test artifacts (XMLs + Screenshots) uses: actions/download-artifact@v4 + continue-on-error: true with: - path: all-test-results/ - pattern: "*-android-*-test-results" + path: all-test-artifacts/ + pattern: '*-android-*-test-*' - name: Post Test Report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 with: - name: "Android E2E Smoke Test Results" - path: "all-test-results/**/*.xml" - reporter: "jest-junit" + name: 'Android E2E Smoke Test Results' + path: 'all-test-artifacts/**/junit.xml' + reporter: 'jest-junit' fail-on-error: false - list-suites: "failed" - list-tests: "failed" + list-suites: 'failed' + list-tests: 'failed' - - name: Create mobile test report + - name: Upload all test artifacts (XMLs + Screenshots) + uses: actions/upload-artifact@v4 + with: + name: e2e-smoke-android-all-test-artifacts + path: all-test-artifacts/ + + - name: Create mobile JSON test report id: create-json-report continue-on-error: true run: | @@ -165,31 +172,25 @@ jobs: mkdir -p temp-deps && cd temp-deps npm init -y npm install xml2js@0.5.0 --no-audit --no-fund - + # Copy node_modules to workspace cp -r node_modules ${{ github.workspace }}/ - + # Run the mobile test report generator cd ${{ github.workspace }} - TEST_RESULTS_PATH=all-test-results \ - TEST_RUNS_PATH=test/test-results/test-runs-android.json \ + TEST_RESULTS_PATH=all-test-artifacts \ + TEST_RUNS_PATH=test/test-results/json-test-report \ RUN_ID=${{ github.run_id }} \ PR_NUMBER=${{ github.event.pull_request.number || '0' }} \ GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ - node .github/scripts/e2e-create-test-report.mjs - + node .github/scripts/e2e-create-json-test-report.mjs + # Clean up temporary node_modules rm -rf node_modules - - name: Upload test runs JSON + - name: Upload JSON test report if: steps.create-json-report.outcome == 'success' uses: actions/upload-artifact@v4 with: - name: test-e2e-android-report - path: test/test-results/test-runs-android.json - - - name: Upload merged XML report - uses: actions/upload-artifact@v4 - with: - name: android-merged-test-report - path: all-test-results/**/*.xml + name: e2e-android-json-report + path: test/test-results/json-test-report diff --git a/.github/workflows/run-e2e-smoke-tests-ios.yml b/.github/workflows/run-e2e-smoke-tests-ios.yml index 495632446e24..e3ef7478442d 100644 --- a/.github/workflows/run-e2e-smoke-tests-ios.yml +++ b/.github/workflows/run-e2e-smoke-tests-ios.yml @@ -15,19 +15,19 @@ permissions: jobs: confirmations-redesigned-ios-smoke: - strategy: - matrix: - split: [1, 2, 3] - fail-fast: false - uses: ./.github/workflows/run-e2e-workflow.yml - with: - test-suite-name: confirmations-redesigned-ios-smoke-${{ matrix.split }} - platform: ios - test_suite_tag: "SmokeConfirmationsRedesigned" - split_number: ${{ matrix.split }} - total_splits: 3 - changed_files: ${{ inputs.changed_files }} - secrets: inherit + strategy: + matrix: + split: [1, 2, 3] + fail-fast: false + uses: ./.github/workflows/run-e2e-workflow.yml + with: + test-suite-name: confirmations-redesigned-ios-smoke-${{ matrix.split }} + platform: ios + test_suite_tag: 'SmokeConfirmationsRedesigned' + split_number: ${{ matrix.split }} + total_splits: 3 + changed_files: ${{ inputs.changed_files }} + secrets: inherit trade-ios-smoke: strategy: @@ -38,7 +38,7 @@ jobs: with: test-suite-name: trade-ios-smoke-${{ matrix.split }} platform: ios - test_suite_tag: "SmokeTrade" + test_suite_tag: 'SmokeTrade' split_number: ${{ matrix.split }} total_splits: 1 changed_files: ${{ inputs.changed_files }} @@ -53,7 +53,7 @@ jobs: with: test-suite-name: wallet-platform-ios-smoke-${{ matrix.split }} platform: ios - test_suite_tag: "SmokeWalletPlatform" + test_suite_tag: 'SmokeWalletPlatform' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -68,7 +68,7 @@ jobs: with: test-suite-name: identity-ios-smoke-${{ matrix.split }} platform: ios - test_suite_tag: "SmokeIdentity" + test_suite_tag: 'SmokeIdentity' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -83,7 +83,7 @@ jobs: with: test-suite-name: accounts-ios-smoke-${{ matrix.split }} platform: ios - test_suite_tag: "SmokeAccounts" + test_suite_tag: 'SmokeAccounts' split_number: ${{ matrix.split }} total_splits: 1 changed_files: ${{ inputs.changed_files }} @@ -98,7 +98,7 @@ jobs: with: test-suite-name: network-abstraction-ios-smoke-${{ matrix.split }} platform: ios - test_suite_tag: "SmokeNetworkAbstractions" + test_suite_tag: 'SmokeNetworkAbstractions' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -113,7 +113,7 @@ jobs: with: test-suite-name: network-expansion-ios-smoke-${{ matrix.split }} platform: ios - test_suite_tag: "SmokeNetworkExpansion" + test_suite_tag: 'SmokeNetworkExpansion' split_number: ${{ matrix.split }} total_splits: 2 changed_files: ${{ inputs.changed_files }} @@ -141,23 +141,30 @@ jobs: with: node-version-file: '.nvmrc' - - name: Download all test results + - name: Download shards test artifacts (XMLs + Screenshots) uses: actions/download-artifact@v4 + continue-on-error: true with: - path: all-test-results/ - pattern: "*-ios-*-test-results" + path: all-test-artifacts/ + pattern: '*-ios-*-test-*' - name: Post Test Report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 with: - name: "iOS E2E Smoke Test Results" - path: "all-test-results/**/*.xml" - reporter: "jest-junit" + name: 'iOS E2E Smoke Test Results' + path: 'all-test-artifacts/**/junit.xml' + reporter: 'jest-junit' fail-on-error: false - list-suites: "failed" - list-tests: "failed" + list-suites: 'failed' + list-tests: 'failed' - - name: Create mobile test report + - name: Upload all test artifacts (XMLs + Screenshots) + uses: actions/upload-artifact@v4 + with: + name: e2e-smoke-ios-all-test-artifacts + path: all-test-artifacts/ + + - name: Create mobile JSON test report id: create-json-report continue-on-error: true run: | @@ -165,31 +172,25 @@ jobs: mkdir -p temp-deps && cd temp-deps npm init -y npm install xml2js@0.5.0 --no-audit --no-fund - + # Copy node_modules to workspace cp -r node_modules ${{ github.workspace }}/ - + # Run the mobile test report generator cd ${{ github.workspace }} - TEST_RESULTS_PATH=all-test-results \ - TEST_RUNS_PATH=test/test-results/test-runs-ios.json \ + TEST_RESULTS_PATH=all-test-artifacts \ + TEST_RUNS_PATH=test/test-results/json-test-report.json \ RUN_ID=${{ github.run_id }} \ PR_NUMBER=${{ github.event.pull_request.number || '0' }} \ GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ - node .github/scripts/e2e-create-test-report.mjs - + node .github/scripts/e2e-create-json-test-report.mjs + # Clean up temporary node_modules rm -rf node_modules - - name: Upload test runs JSON + - name: Upload JSON test report if: steps.create-json-report.outcome == 'success' uses: actions/upload-artifact@v4 with: - name: test-e2e-ios-report - path: test/test-results/test-runs-ios.json - - - name: Upload merged XML report - uses: actions/upload-artifact@v4 - with: - name: ios-merged-test-report - path: all-test-results/**/*.xml + name: e2e-ios-json-report + path: test/test-results/json-test-report.json diff --git a/.github/workflows/run-e2e-workflow.yml b/.github/workflows/run-e2e-workflow.yml index 2e83a58633f3..7b32fc3d9cc5 100644 --- a/.github/workflows/run-e2e-workflow.yml +++ b/.github/workflows/run-e2e-workflow.yml @@ -15,7 +15,7 @@ on: required: true type: string test_suite_tag: - description: 'The Cucumber tag expression to use for filtering tests' + description: 'The tests tag expression to use for filtering tests' required: true type: string split_number: @@ -201,18 +201,39 @@ jobs: RUN_ID: ${{ github.run_id }} PR_NUMBER: ${{ github.event.pull_request.number || '' }} - - name: Upload test results + - name: Merge Detox JUnit reports + if: always() + continue-on-error: true + run: | + echo "šŸ“Š Merging Detox JUnit XML reports..." + + # Install xml2js for XML parsing (lightweight, no full yarn install needed) + mkdir -p temp-deps && cd temp-deps + npm init -y > /dev/null 2>&1 + npm install xml2js@0.6.2 --no-audit --no-fund --silent + + # Copy node_modules to workspace for script usage + cp -r node_modules ${{ github.workspace }}/ + cd ${{ github.workspace }} + + # Run merge script + node .github/scripts/e2e-merge-detox-junit-reports.mjs + + # Clean up temporary node_modules + rm -rf node_modules temp-deps + + - name: Upload JUnit XML results if: always() uses: actions/upload-artifact@v4 with: - name: ${{ inputs.test-suite-name }}-test-results + name: ${{ inputs.test-suite-name }}-test-junit-results path: e2e/reports/ retention-days: 7 - - name: Upload screenshots + - name: Upload Screenshots if: failure() uses: actions/upload-artifact@v4 with: - name: ${{ inputs.test-suite-name }}-screenshots + name: ${{ inputs.test-suite-name }}-test-screenshots path: e2e/artifacts/ retention-days: 7 diff --git a/e2e/jest.e2e.config.js b/e2e/jest.e2e.config.js index d93603f6fdd7..413e7b3df9ba 100644 --- a/e2e/jest.e2e.config.js +++ b/e2e/jest.e2e.config.js @@ -21,6 +21,10 @@ module.exports = { { outputDirectory: './e2e/reports', classNameTemplate: '{filepath}', + outputName: (() => { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return `junit-${timestamp}.xml`; + })(), }, ], ],