mattermost-mobile-upstream/detox/utils/failed-jest-specs.js
yasser khan f3af9dbe99
Fix/detox hittest cluster (#10023)
fix(e2e): stabilize Detox and Maestro test suite

Restore main features, and harden the E2E harness against flaky UI, network, retry, synchronization, and classification-lock failures.

Stabilize channel creation, saved messages, bookmarks, search, deep links, keyboard dismissal, and post verification.
Bound network, transport, health-check, synchronization, and lock retries by elapsed time.
Fix iOS and Android Maestro/Detox races and flaky interaction flows.
Restore Session Attributes and deep-link fixes lost during merge.
Improve test isolation, global-setting cleanup, and Jest/TSIO reporting.
Enforce Maestro flow-header validation in local and CI checks.
2026-08-27 08:58:03 +05:30

199 lines
5.3 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console -- CI utility script */
/**
* Android CI retry helper: list specs to re-run after a failed shard, and merge
* attempt-1 + attempt-2 Jest JSON so TSIO still sees the full shard.
*/
const fs = require('fs');
const path = require('path');
const {parseArgs} = require('./cli-args');
/**
* Canonical key so repo-relative matrix paths and Jest's absolute `name` match.
*
* @param {string} filePath
* @returns {string}
*/
function specKey(filePath) {
const n = String(filePath || '').replace(/\\/g, '/');
const detoxIdx = n.indexOf('detox/e2e/test/');
if (detoxIdx >= 0) {
return n.slice(detoxIdx);
}
const e2eIdx = n.indexOf('e2e/test/');
if (e2eIdx >= 0) {
return n.slice(e2eIdx);
}
return n.replace(/^\.\//, '');
}
/**
* @param {object} suite
* @returns {string}
*/
function suitePath(suite) {
return suite?.testFilePath || suite?.name || '';
}
/**
* @param {object} suite
* @returns {boolean}
*/
function suiteFailed(suite) {
if (!suite || typeof suite !== 'object') {
return true;
}
if (suite.status === 'failed') {
return true;
}
if (typeof suite.numFailingTests === 'number' && suite.numFailingTests > 0) {
return true;
}
const cases = suite.assertionResults || suite.testResults || [];
return cases.some((c) => c && c.status === 'failed');
}
/**
* Specs from the shard that failed or never appeared in the Jest report.
* Missing report → retry the whole shard (crash / timeout before JSON).
*
* @param {string} resultsPath
* @param {string[]} shardSpecs
* @returns {string[]}
*/
function listRetrySpecs(resultsPath, shardSpecs) {
const specs = (shardSpecs || []).filter(Boolean);
if (!resultsPath || !fs.existsSync(resultsPath)) {
return specs;
}
let report;
try {
report = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
} catch {
return specs;
}
const suites = Array.isArray(report.testResults) ? report.testResults : [];
if (suites.length === 0) {
return specs;
}
const byKey = new Map();
for (const suite of suites) {
const key = specKey(suitePath(suite));
if (key) {
byKey.set(key, suite);
}
}
const retry = [];
const seen = new Set();
for (const spec of specs) {
const key = specKey(spec);
seen.add(key);
const suite = byKey.get(key);
if (!suite || suiteFailed(suite)) {
retry.push(spec);
}
}
// Failed suites not in the original matrix (shouldn't happen) still retry.
for (const [key, suite] of byKey) {
if (!seen.has(key) && suiteFailed(suite)) {
retry.push(suitePath(suite));
}
}
return retry;
}
/**
* Later Jest JSON wins for the same spec. Keeps CLI shape (`name` + suites)
* so merge-jest-results-for-tsio can ingest the result.
*
* @param {string[]} inputPaths
* @returns {object}
*/
function mergeJestResultsPreferLater(inputPaths) {
const byKey = new Map();
for (const inputPath of inputPaths) {
if (!inputPath || !fs.existsSync(inputPath)) {
continue;
}
let report;
try {
report = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
} catch {
continue;
}
const suites = Array.isArray(report.testResults) ? report.testResults : [];
for (const suite of suites) {
const key = specKey(suitePath(suite));
if (key) {
byKey.set(key, suite);
}
}
}
const testResults = [...byKey.values()];
let numFailedTests = 0;
let numPassedTests = 0;
for (const suite of testResults) {
const cases = suite.assertionResults || suite.testResults || [];
for (const c of cases) {
if (c?.status === 'failed') {
numFailedTests += 1;
} else if (c?.status === 'passed') {
numPassedTests += 1;
}
}
}
return {
success: numFailedTests === 0 && testResults.length > 0,
numFailedTests,
numPassedTests,
testResults,
};
}
function main() {
const args = parseArgs(process.argv);
if (args.output) {
const inputs = [args.attempt1, args.attempt2].filter(Boolean);
if (inputs.length === 0) {
console.error('failed-jest-specs: --attempt1 and/or --attempt2 required with --output');
process.exit(1);
}
const merged = mergeJestResultsPreferLater(inputs);
fs.mkdirSync(path.dirname(args.output), {recursive: true});
const tmp = `${args.output}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(merged));
fs.renameSync(tmp, args.output);
console.log(`Merged ${inputs.length} attempt JSON(s) -> ${merged.testResults.length} suite(s) -> ${args.output}`);
return;
}
const shardSpecs = (args['shard-specs'] || '').split(/\s+/).filter(Boolean);
const retry = listRetrySpecs(args.results, shardSpecs);
for (const spec of retry) {
console.log(spec);
}
}
if (require.main === module) {
main();
}
module.exports = {
specKey,
suiteFailed,
listRetrySpecs,
mergeJestResultsPreferLater,
};