// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. // CI util unit tests: run with `node --test detox/utils/merge-jest-results-for-tsio.test.js`. const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const {describe, it} = require('node:test'); const { mergeJestResultsForTsio, toTsioDetoxSuite, relativizeDetoxPath, writeMergedJestResultsForTsio, appendUnreportedSpecs, parseExpectedSpecs, parseShards, } = require('./merge-jest-results-for-tsio'); describe('merge-jest-results-for-tsio', () => { it('should keep already-relative paths (any e2e layout) unchanged', () => { assert.equal( relativizeDetoxPath('detox/e2e/test/foo.e2e.ts'), 'detox/e2e/test/foo.e2e.ts', ); assert.equal( relativizeDetoxPath('e2e/detox/test/foo.e2e.ts'), 'e2e/detox/test/foo.e2e.ts', ); assert.equal( relativizeDetoxPath('e2e/maestro/flows/calls/mute.yml'), 'e2e/maestro/flows/calls/mute.yml', ); }); it('should strip absolute paths against an explicit repo root', () => { assert.equal( relativizeDetoxPath( '/home/runner/work/mattermost-mobile/mattermost-mobile/e2e/detox/test/foo.e2e.ts', {repoRoot: '/home/runner/work/mattermost-mobile/mattermost-mobile'}, ), 'e2e/detox/test/foo.e2e.ts', ); assert.equal( relativizeDetoxPath( '/home/runner/work/mattermost-mobile/mattermost-mobile/detox/e2e/test/foo.e2e.ts', {repoRoot: '/home/runner/work/mattermost-mobile/mattermost-mobile'}, ), 'detox/e2e/test/foo.e2e.ts', ); }); it('should fall back to GitHub Actions /work/// stripping', () => { assert.equal( relativizeDetoxPath( '/home/runner/work/mattermost-mobile/mattermost-mobile/e2e/detox/test/foo.e2e.ts', {repoRoot: '/tmp/not-the-root'}, ), 'e2e/detox/test/foo.e2e.ts', ); }); it('should convert Jest CLI suites (name + assertionResults) to TSIO Detox shape', () => { const converted = toTsioDetoxSuite({ name: '/repo/e2e/detox/test/products/agents/agent_mention.e2e.ts', startTime: 1000, assertionResults: [{ ancestorTitles: ['Autocomplete - Agent Mention'], duration: 12, failureMessages: ['boom'], fullName: 'Autocomplete - Agent Mention should show bot', status: 'failed', title: 'should show bot', }], }, {repoRoot: '/repo'}); assert.deepEqual(converted, { testFilePath: 'e2e/detox/test/products/agents/agent_mention.e2e.ts', perfStats: {start: 1000}, testResults: [{ ancestorTitles: ['Autocomplete - Agent Mention'], duration: 12, failureMessages: ['boom'], fullName: 'Autocomplete - Agent Mention should show bot', status: 'failed', title: 'should show bot', }], }); }); it('should keep a retried test\'s earlier failures for TSIO, and nothing for an untried one', () => { const converted = toTsioDetoxSuite({ name: '/repo/detox/e2e/test/threads/reply_to_thread.e2e.ts', assertionResults: [ {fullName: 'Threads MM-T4809_1', title: 'MM-T4809_1', status: 'passed', failureMessages: [], retryReasons: ['Test Failed: No elements found']}, {fullName: 'Threads MM-T4809_2', title: 'MM-T4809_2', status: 'passed', failureMessages: [], retryReasons: []}, ], }, {repoRoot: '/repo'}); assert.deepEqual(converted.testResults[0].retryReasons, ['Test Failed: No elements found']); assert.equal('retryReasons' in converted.testResults[1], false); }); it('should pass through suites already in TSIO Detox shape', () => { const converted = toTsioDetoxSuite({ testFilePath: 'e2e/detox/test/foo.e2e.ts', testResults: [{ ancestorTitles: [], duration: 1, failureMessages: [], fullName: 'foo', status: 'passed', title: 'foo', }], }); assert.equal(converted.testFilePath, 'e2e/detox/test/foo.e2e.ts'); assert.equal(converted.testResults.length, 1); assert.equal(converted.testResults[0].status, 'passed'); }); it('should merge multiple CLI shard files into one TSIO payload with real test cases', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tsio-merge-')); const a = path.join(dir, 'a.json'); const b = path.join(dir, 'b.json'); fs.writeFileSync(a, JSON.stringify({ numTotalTests: 1, testResults: [{ name: '/x/e2e/detox/test/a.e2e.ts', assertionResults: [{ ancestorTitles: ['A'], duration: 1, failureMessages: [], fullName: 'A pass', status: 'passed', title: 'pass', }], }], })); fs.writeFileSync(b, JSON.stringify({ numTotalTests: 1, testResults: [{ name: '/x/e2e/detox/test/b.e2e.ts', assertionResults: [{ ancestorTitles: ['B'], duration: 2, failureMessages: ['fail'], fullName: 'B fail', status: 'failed', title: 'fail', }], }], })); const opts = {repoRoot: '/x'}; const merged = mergeJestResultsForTsio([a, b], opts); assert.equal(merged.testResults.length, 2); assert.equal(merged.testResults[0].testFilePath, 'e2e/detox/test/a.e2e.ts'); assert.equal(merged.testResults[0].testResults[0].status, 'passed'); assert.equal(merged.testResults[1].testFilePath, 'e2e/detox/test/b.e2e.ts'); assert.equal(merged.testResults[1].testResults[0].status, 'failed'); assert.equal(merged.testResults[0].assertionResults, undefined); assert.equal(merged.testResults[0].name, undefined); const out = path.join(dir, 'merged.json'); const stats = writeMergedJestResultsForTsio([a, b], out, opts); assert.deepEqual(stats, {suites: 2, tests: 2}); assert.equal(JSON.parse(fs.readFileSync(out, 'utf8')).testResults.length, 2); fs.rmSync(dir, {recursive: true, force: true}); }); it('should mark specs that never reported as failed', () => { // main d943628: machine-4's Jest was killed mid-run, jest-stare's partial report // was promoted, and 3 specs vanished from a report that published 100% green. const merged = {testResults: [{testFilePath: 'detox/e2e/test/a.e2e.ts', testResults: [{status: 'passed'}]}]}; const missing = appendUnreportedSpecs(merged, [ 'detox/e2e/test/a.e2e.ts', 'detox/e2e/test/b.e2e.ts', 'detox/e2e/test/c.e2e.ts', ]); assert.deepEqual(missing, ['detox/e2e/test/b.e2e.ts', 'detox/e2e/test/c.e2e.ts']); assert.equal(merged.testResults.length, 3, 'every assigned spec must appear'); assert.equal(merged.testResults[1].testResults[0].status, 'failed'); assert.match(merged.testResults[1].testResults[0].failureMessages[0], /produced no result/); }); it('should say whether the spec\'s shard reported nothing or stopped before reaching it', () => { // Triage re-runs a shard that reported nothing, and judges a spec whose shard stopped before it by its history. const merged = {testResults: [{testFilePath: 'detox/e2e/test/a.e2e.ts', testResults: [{status: 'passed'}]}]}; const shards = [['detox/e2e/test/a.e2e.ts', 'detox/e2e/test/b.e2e.ts'], ['detox/e2e/test/c.e2e.ts', 'detox/e2e/test/d.e2e.ts']]; appendUnreportedSpecs(merged, shards.flat(), shards); const reason = (spec) => merged.testResults.find((s) => s.testFilePath === spec).testResults[0].failureMessages[0]; assert.equal(reason('detox/e2e/test/b.e2e.ts'), 'detox/e2e/test/b.e2e.ts was assigned to a shard but produced no result: other specs of its shard reported, so its Jest process stopped before reaching this spec.'); assert.equal(reason('detox/e2e/test/c.e2e.ts'), 'detox/e2e/test/c.e2e.ts was assigned to a shard but produced no result: no spec of its shard reported, so the shard ran or uploaded nothing.'); assert.match(reason('detox/e2e/test/d.e2e.ts'), /no spec of its shard reported/); }); it('should read each generate-specs row as one shard', () => { assert.deepEqual(parseShards(JSON.stringify({include: [{specs: 'a.e2e.ts b.e2e.ts'}, {specs: 'c.e2e.ts'}, {}]})), [['a.e2e.ts', 'b.e2e.ts'], ['c.e2e.ts']]); assert.deepEqual(parseShards(''), []); }); it('should not touch a complete report', () => { const merged = {testResults: [{testFilePath: 'detox/e2e/test/a.e2e.ts'}]}; assert.deepEqual(appendUnreportedSpecs(merged, ['detox/e2e/test/a.e2e.ts']), []); assert.equal(merged.testResults.length, 1); }); it('should mark every spec failed when no shard reported at all', () => { // If the merge exits before this, TSIO never receives a report and the commit // status stays pending instead of failing. const merged = mergeJestResultsForTsio([]); const missing = appendUnreportedSpecs(merged, ['detox/e2e/test/a.e2e.ts', 'detox/e2e/test/b.e2e.ts']); assert.equal(missing.length, 2); assert.equal(merged.testResults.length, 2, 'a run with no shard reports must still emit a report'); }); it('should flatten a generate-specs matrix and de-duplicate', () => { const specs = parseExpectedSpecs(JSON.stringify({include: [ {runId: 1, specs: 'a.e2e.ts b.e2e.ts'}, {runId: 2, specs: 'b.e2e.ts c.e2e.ts'}, ]})); assert.deepEqual(specs, ['a.e2e.ts', 'b.e2e.ts', 'c.e2e.ts']); assert.deepEqual(parseExpectedSpecs(''), []); assert.deepEqual(parseExpectedSpecs('{}'), []); }); });