|
5 | 5 | * sleep injection is wired through `TestDeps.sleep` to avoid real delays. |
6 | 6 | */ |
7 | 7 |
|
8 | | -import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; |
| 8 | +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; |
9 | 9 | import { tmpdir } from 'node:os'; |
10 | 10 | import { join } from 'node:path'; |
11 | 11 | import type { Command } from 'commander'; |
@@ -3624,3 +3624,169 @@ describe('dashboardUrl on run completion', () => { |
3624 | 3624 | ).toBe(true); |
3625 | 3625 | }); |
3626 | 3626 | }); |
| 3627 | + |
| 3628 | +describe('runTestRunAll — JUnit report export', () => { |
| 3629 | + const BATCH_FRESH_RESP: BatchRunFreshResponse = { |
| 3630 | + accepted: [ |
| 3631 | + { testId: 'test_be_01', runId: 'run_fresh_01', enqueuedAt: '2026-06-09T10:00:00.000Z' }, |
| 3632 | + { testId: 'test_be_02', runId: 'run_fresh_02', enqueuedAt: '2026-06-09T10:00:01.000Z' }, |
| 3633 | + ], |
| 3634 | + conflicts: [], |
| 3635 | + deferred: [], |
| 3636 | + skippedFrontend: [], |
| 3637 | + skippedIntegration: [], |
| 3638 | + }; |
| 3639 | + |
| 3640 | + function makeTerminalRun(runId: string, testId: string, status: string): RunResponse { |
| 3641 | + return { |
| 3642 | + runId, |
| 3643 | + testId, |
| 3644 | + projectId: 'project_be', |
| 3645 | + userId: 'user_1', |
| 3646 | + status: status as RunResponse['status'], |
| 3647 | + source: 'cli', |
| 3648 | + createdAt: '2026-06-09T10:00:00.000Z', |
| 3649 | + startedAt: '2026-06-09T10:00:01.000Z', |
| 3650 | + finishedAt: '2026-06-09T10:00:30.000Z', |
| 3651 | + codeVersion: 'v1', |
| 3652 | + targetUrl: 'https://api.example.com', |
| 3653 | + createdFrom: 'cli', |
| 3654 | + failedStepIndex: null, |
| 3655 | + failureKind: null, |
| 3656 | + error: null, |
| 3657 | + videoUrl: null, |
| 3658 | + stepSummary: { total: 3, completed: 3, passedCount: status === 'passed' ? 3 : 0, failedCount: 0 }, |
| 3659 | + }; |
| 3660 | + } |
| 3661 | + |
| 3662 | + it('--wait --report junit writes XML after polling', async () => { |
| 3663 | + const { credentialsPath } = makeCreds(); |
| 3664 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-all-')); |
| 3665 | + const reportPath = join(dir, 'results.xml'); |
| 3666 | + const fetchImpl = makeFetch((url, init) => { |
| 3667 | + if ((init.method ?? 'GET') === 'POST') return { body: BATCH_FRESH_RESP }; |
| 3668 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 3669 | + if (runId === 'run_fresh_01') |
| 3670 | + return { body: makeTerminalRun('run_fresh_01', 'test_be_01', 'passed') }; |
| 3671 | + if (runId === 'run_fresh_02') |
| 3672 | + return { body: makeTerminalRun('run_fresh_02', 'test_be_02', 'passed') }; |
| 3673 | + return errorBody('NOT_FOUND'); |
| 3674 | + }); |
| 3675 | + |
| 3676 | + await runTestRunAll( |
| 3677 | + { |
| 3678 | + profile: 'default', |
| 3679 | + output: 'json', |
| 3680 | + debug: false, |
| 3681 | + projectId: 'project_be', |
| 3682 | + wait: true, |
| 3683 | + timeoutSeconds: 60, |
| 3684 | + maxConcurrency: 5, |
| 3685 | + report: 'junit', |
| 3686 | + reportFile: reportPath, |
| 3687 | + }, |
| 3688 | + { |
| 3689 | + credentialsPath, |
| 3690 | + fetchImpl, |
| 3691 | + stdout: () => undefined, |
| 3692 | + stderr: () => undefined, |
| 3693 | + sleep: instantSleep, |
| 3694 | + }, |
| 3695 | + ); |
| 3696 | + |
| 3697 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3698 | + expect(xml).toContain('<testsuite name="testsprite:project_be"'); |
| 3699 | + expect(xml).toContain('name="test_be_01"'); |
| 3700 | + expect(xml).toContain('name="test_be_02"'); |
| 3701 | + expect(xml).toContain('failures="0"'); |
| 3702 | + }); |
| 3703 | + |
| 3704 | + it('--wait --report junit writes XML even when batch exits 1', async () => { |
| 3705 | + const { credentialsPath } = makeCreds(); |
| 3706 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-fail-')); |
| 3707 | + const reportPath = join(dir, 'results.xml'); |
| 3708 | + const fetchImpl = makeFetch((url, init) => { |
| 3709 | + if ((init.method ?? 'GET') === 'POST') return { body: BATCH_FRESH_RESP }; |
| 3710 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 3711 | + if (runId === 'run_fresh_01') |
| 3712 | + return { body: makeTerminalRun('run_fresh_01', 'test_be_01', 'passed') }; |
| 3713 | + if (runId === 'run_fresh_02') |
| 3714 | + return { body: makeTerminalRun('run_fresh_02', 'test_be_02', 'failed') }; |
| 3715 | + return errorBody('NOT_FOUND'); |
| 3716 | + }); |
| 3717 | + |
| 3718 | + await expect( |
| 3719 | + runTestRunAll( |
| 3720 | + { |
| 3721 | + profile: 'default', |
| 3722 | + output: 'json', |
| 3723 | + debug: false, |
| 3724 | + projectId: 'project_be', |
| 3725 | + wait: true, |
| 3726 | + timeoutSeconds: 60, |
| 3727 | + maxConcurrency: 5, |
| 3728 | + report: 'junit', |
| 3729 | + reportFile: reportPath, |
| 3730 | + }, |
| 3731 | + { |
| 3732 | + credentialsPath, |
| 3733 | + fetchImpl, |
| 3734 | + stdout: () => undefined, |
| 3735 | + stderr: () => undefined, |
| 3736 | + sleep: instantSleep, |
| 3737 | + }, |
| 3738 | + ), |
| 3739 | + ).rejects.toMatchObject({ exitCode: 1 }); |
| 3740 | + |
| 3741 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3742 | + expect(xml).toContain('failures="1"'); |
| 3743 | + expect(xml).toContain('name="test_be_02"'); |
| 3744 | + }); |
| 3745 | + |
| 3746 | + it('rejects --report without --wait', async () => { |
| 3747 | + await expect( |
| 3748 | + runTestRunAll( |
| 3749 | + { |
| 3750 | + profile: 'default', |
| 3751 | + output: 'json', |
| 3752 | + debug: false, |
| 3753 | + projectId: 'project_be', |
| 3754 | + wait: false, |
| 3755 | + timeoutSeconds: 60, |
| 3756 | + maxConcurrency: 5, |
| 3757 | + report: 'junit', |
| 3758 | + reportFile: './results.xml', |
| 3759 | + }, |
| 3760 | + {}, |
| 3761 | + ), |
| 3762 | + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); |
| 3763 | + }); |
| 3764 | + |
| 3765 | + it('--dry-run --report junit writes canned sample XML', async () => { |
| 3766 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-dry-')); |
| 3767 | + const reportPath = join(dir, 'results.xml'); |
| 3768 | + |
| 3769 | + await runTestRunAll( |
| 3770 | + { |
| 3771 | + profile: 'default', |
| 3772 | + output: 'json', |
| 3773 | + debug: false, |
| 3774 | + dryRun: true, |
| 3775 | + projectId: 'project_be', |
| 3776 | + wait: true, |
| 3777 | + timeoutSeconds: 60, |
| 3778 | + maxConcurrency: 5, |
| 3779 | + report: 'junit', |
| 3780 | + reportFile: reportPath, |
| 3781 | + }, |
| 3782 | + { |
| 3783 | + stdout: () => undefined, |
| 3784 | + stderr: () => undefined, |
| 3785 | + }, |
| 3786 | + ); |
| 3787 | + |
| 3788 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3789 | + expect(xml).toContain('name="test_fresh_wave_01"'); |
| 3790 | + expect(xml).toContain('failures="1"'); |
| 3791 | + }); |
| 3792 | +}); |
0 commit comments