|
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,221 @@ 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: { |
| 3659 | + total: 3, |
| 3660 | + completed: 3, |
| 3661 | + passedCount: status === 'passed' ? 3 : 0, |
| 3662 | + failedCount: 0, |
| 3663 | + }, |
| 3664 | + }; |
| 3665 | + } |
| 3666 | + |
| 3667 | + it('--wait --report junit writes XML after polling', async () => { |
| 3668 | + const { credentialsPath } = makeCreds(); |
| 3669 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-all-')); |
| 3670 | + const reportPath = join(dir, 'results.xml'); |
| 3671 | + const fetchImpl = makeFetch((url, init) => { |
| 3672 | + if ((init.method ?? 'GET') === 'POST') return { body: BATCH_FRESH_RESP }; |
| 3673 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 3674 | + if (runId === 'run_fresh_01') |
| 3675 | + return { body: makeTerminalRun('run_fresh_01', 'test_be_01', 'passed') }; |
| 3676 | + if (runId === 'run_fresh_02') |
| 3677 | + return { body: makeTerminalRun('run_fresh_02', 'test_be_02', 'passed') }; |
| 3678 | + return errorBody('NOT_FOUND'); |
| 3679 | + }); |
| 3680 | + |
| 3681 | + await runTestRunAll( |
| 3682 | + { |
| 3683 | + profile: 'default', |
| 3684 | + output: 'json', |
| 3685 | + debug: false, |
| 3686 | + projectId: 'project_be', |
| 3687 | + wait: true, |
| 3688 | + timeoutSeconds: 60, |
| 3689 | + maxConcurrency: 5, |
| 3690 | + report: 'junit', |
| 3691 | + reportFile: reportPath, |
| 3692 | + }, |
| 3693 | + { |
| 3694 | + credentialsPath, |
| 3695 | + fetchImpl, |
| 3696 | + stdout: () => undefined, |
| 3697 | + stderr: () => undefined, |
| 3698 | + sleep: instantSleep, |
| 3699 | + }, |
| 3700 | + ); |
| 3701 | + |
| 3702 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3703 | + expect(xml).toContain('<testsuite name="testsprite:project_be"'); |
| 3704 | + expect(xml).toContain('name="test_be_01"'); |
| 3705 | + expect(xml).toContain('name="test_be_02"'); |
| 3706 | + expect(xml).toContain('failures="0"'); |
| 3707 | + }); |
| 3708 | + |
| 3709 | + it('--wait --report junit writes XML even when batch exits 1', async () => { |
| 3710 | + const { credentialsPath } = makeCreds(); |
| 3711 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-fail-')); |
| 3712 | + const reportPath = join(dir, 'results.xml'); |
| 3713 | + const fetchImpl = makeFetch((url, init) => { |
| 3714 | + if ((init.method ?? 'GET') === 'POST') return { body: BATCH_FRESH_RESP }; |
| 3715 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 3716 | + if (runId === 'run_fresh_01') |
| 3717 | + return { body: makeTerminalRun('run_fresh_01', 'test_be_01', 'passed') }; |
| 3718 | + if (runId === 'run_fresh_02') |
| 3719 | + return { body: makeTerminalRun('run_fresh_02', 'test_be_02', 'failed') }; |
| 3720 | + return errorBody('NOT_FOUND'); |
| 3721 | + }); |
| 3722 | + |
| 3723 | + await expect( |
| 3724 | + runTestRunAll( |
| 3725 | + { |
| 3726 | + profile: 'default', |
| 3727 | + output: 'json', |
| 3728 | + debug: false, |
| 3729 | + projectId: 'project_be', |
| 3730 | + wait: true, |
| 3731 | + timeoutSeconds: 60, |
| 3732 | + maxConcurrency: 5, |
| 3733 | + report: 'junit', |
| 3734 | + reportFile: reportPath, |
| 3735 | + }, |
| 3736 | + { |
| 3737 | + credentialsPath, |
| 3738 | + fetchImpl, |
| 3739 | + stdout: () => undefined, |
| 3740 | + stderr: () => undefined, |
| 3741 | + sleep: instantSleep, |
| 3742 | + }, |
| 3743 | + ), |
| 3744 | + ).rejects.toMatchObject({ exitCode: 1 }); |
| 3745 | + |
| 3746 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3747 | + expect(xml).toContain('failures="1"'); |
| 3748 | + expect(xml).toContain('name="test_be_02"'); |
| 3749 | + }); |
| 3750 | + |
| 3751 | + it('rejects --report without --wait', async () => { |
| 3752 | + await expect( |
| 3753 | + runTestRunAll( |
| 3754 | + { |
| 3755 | + profile: 'default', |
| 3756 | + output: 'json', |
| 3757 | + debug: false, |
| 3758 | + projectId: 'project_be', |
| 3759 | + wait: false, |
| 3760 | + timeoutSeconds: 60, |
| 3761 | + maxConcurrency: 5, |
| 3762 | + report: 'junit', |
| 3763 | + reportFile: './results.xml', |
| 3764 | + }, |
| 3765 | + {}, |
| 3766 | + ), |
| 3767 | + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); |
| 3768 | + }); |
| 3769 | + |
| 3770 | + it('rejects --report-suite-name without --report', async () => { |
| 3771 | + await expect( |
| 3772 | + runTestRunAll( |
| 3773 | + { |
| 3774 | + profile: 'default', |
| 3775 | + output: 'json', |
| 3776 | + debug: false, |
| 3777 | + projectId: 'project_be', |
| 3778 | + wait: true, |
| 3779 | + timeoutSeconds: 60, |
| 3780 | + maxConcurrency: 5, |
| 3781 | + reportSuiteName: 'orphan-suite', |
| 3782 | + }, |
| 3783 | + {}, |
| 3784 | + ), |
| 3785 | + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); |
| 3786 | + }); |
| 3787 | + |
| 3788 | + it('--dry-run --report junit writes canned sample XML', async () => { |
| 3789 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-dry-')); |
| 3790 | + const reportPath = join(dir, 'results.xml'); |
| 3791 | + |
| 3792 | + await runTestRunAll( |
| 3793 | + { |
| 3794 | + profile: 'default', |
| 3795 | + output: 'json', |
| 3796 | + debug: false, |
| 3797 | + dryRun: true, |
| 3798 | + projectId: 'project_be', |
| 3799 | + wait: true, |
| 3800 | + timeoutSeconds: 60, |
| 3801 | + maxConcurrency: 5, |
| 3802 | + report: 'junit', |
| 3803 | + reportFile: reportPath, |
| 3804 | + }, |
| 3805 | + { |
| 3806 | + stdout: () => undefined, |
| 3807 | + stderr: () => undefined, |
| 3808 | + }, |
| 3809 | + ); |
| 3810 | + |
| 3811 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3812 | + expect(xml).toContain('name="test_fresh_wave_01"'); |
| 3813 | + expect(xml).toContain('failures="1"'); |
| 3814 | + }); |
| 3815 | + |
| 3816 | + it('--dry-run --report junit --report-suite-name overrides canned suite name', async () => { |
| 3817 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-dry-suite-')); |
| 3818 | + const reportPath = join(dir, 'results.xml'); |
| 3819 | + |
| 3820 | + await runTestRunAll( |
| 3821 | + { |
| 3822 | + profile: 'default', |
| 3823 | + output: 'json', |
| 3824 | + debug: false, |
| 3825 | + dryRun: true, |
| 3826 | + projectId: 'project_be', |
| 3827 | + wait: true, |
| 3828 | + timeoutSeconds: 60, |
| 3829 | + maxConcurrency: 5, |
| 3830 | + report: 'junit', |
| 3831 | + reportFile: reportPath, |
| 3832 | + reportSuiteName: 'ci-checkout-suite', |
| 3833 | + }, |
| 3834 | + { |
| 3835 | + stdout: () => undefined, |
| 3836 | + stderr: () => undefined, |
| 3837 | + }, |
| 3838 | + ); |
| 3839 | + |
| 3840 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3841 | + expect(xml).toContain('<testsuite name="ci-checkout-suite"'); |
| 3842 | + expect(xml).not.toContain('testsprite:project_be'); |
| 3843 | + }); |
| 3844 | +}); |
0 commit comments