|
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'; |
@@ -3536,3 +3536,169 @@ describe('dashboardUrl on run completion', () => { |
3536 | 3536 | ).toBe(true); |
3537 | 3537 | }); |
3538 | 3538 | }); |
| 3539 | + |
| 3540 | +describe('runTestRunAll — JUnit report export', () => { |
| 3541 | + const BATCH_FRESH_RESP: BatchRunFreshResponse = { |
| 3542 | + accepted: [ |
| 3543 | + { testId: 'test_be_01', runId: 'run_fresh_01', enqueuedAt: '2026-06-09T10:00:00.000Z' }, |
| 3544 | + { testId: 'test_be_02', runId: 'run_fresh_02', enqueuedAt: '2026-06-09T10:00:01.000Z' }, |
| 3545 | + ], |
| 3546 | + conflicts: [], |
| 3547 | + deferred: [], |
| 3548 | + skippedFrontend: [], |
| 3549 | + skippedIntegration: [], |
| 3550 | + }; |
| 3551 | + |
| 3552 | + function makeTerminalRun(runId: string, testId: string, status: string): RunResponse { |
| 3553 | + return { |
| 3554 | + runId, |
| 3555 | + testId, |
| 3556 | + projectId: 'project_be', |
| 3557 | + userId: 'user_1', |
| 3558 | + status: status as RunResponse['status'], |
| 3559 | + source: 'cli', |
| 3560 | + createdAt: '2026-06-09T10:00:00.000Z', |
| 3561 | + startedAt: '2026-06-09T10:00:01.000Z', |
| 3562 | + finishedAt: '2026-06-09T10:00:30.000Z', |
| 3563 | + codeVersion: 'v1', |
| 3564 | + targetUrl: 'https://api.example.com', |
| 3565 | + createdFrom: 'cli', |
| 3566 | + failedStepIndex: null, |
| 3567 | + failureKind: null, |
| 3568 | + error: null, |
| 3569 | + videoUrl: null, |
| 3570 | + stepSummary: { total: 3, completed: 3, passedCount: status === 'passed' ? 3 : 0, failedCount: 0 }, |
| 3571 | + }; |
| 3572 | + } |
| 3573 | + |
| 3574 | + it('--wait --report junit writes XML after polling', async () => { |
| 3575 | + const { credentialsPath } = makeCreds(); |
| 3576 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-all-')); |
| 3577 | + const reportPath = join(dir, 'results.xml'); |
| 3578 | + const fetchImpl = makeFetch((url, init) => { |
| 3579 | + if ((init.method ?? 'GET') === 'POST') return { body: BATCH_FRESH_RESP }; |
| 3580 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 3581 | + if (runId === 'run_fresh_01') |
| 3582 | + return { body: makeTerminalRun('run_fresh_01', 'test_be_01', 'passed') }; |
| 3583 | + if (runId === 'run_fresh_02') |
| 3584 | + return { body: makeTerminalRun('run_fresh_02', 'test_be_02', 'passed') }; |
| 3585 | + return errorBody('NOT_FOUND'); |
| 3586 | + }); |
| 3587 | + |
| 3588 | + await runTestRunAll( |
| 3589 | + { |
| 3590 | + profile: 'default', |
| 3591 | + output: 'json', |
| 3592 | + debug: false, |
| 3593 | + projectId: 'project_be', |
| 3594 | + wait: true, |
| 3595 | + timeoutSeconds: 60, |
| 3596 | + maxConcurrency: 5, |
| 3597 | + report: 'junit', |
| 3598 | + reportFile: reportPath, |
| 3599 | + }, |
| 3600 | + { |
| 3601 | + credentialsPath, |
| 3602 | + fetchImpl, |
| 3603 | + stdout: () => undefined, |
| 3604 | + stderr: () => undefined, |
| 3605 | + sleep: instantSleep, |
| 3606 | + }, |
| 3607 | + ); |
| 3608 | + |
| 3609 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3610 | + expect(xml).toContain('<testsuite name="testsprite:project_be"'); |
| 3611 | + expect(xml).toContain('name="test_be_01"'); |
| 3612 | + expect(xml).toContain('name="test_be_02"'); |
| 3613 | + expect(xml).toContain('failures="0"'); |
| 3614 | + }); |
| 3615 | + |
| 3616 | + it('--wait --report junit writes XML even when batch exits 1', async () => { |
| 3617 | + const { credentialsPath } = makeCreds(); |
| 3618 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-fail-')); |
| 3619 | + const reportPath = join(dir, 'results.xml'); |
| 3620 | + const fetchImpl = makeFetch((url, init) => { |
| 3621 | + if ((init.method ?? 'GET') === 'POST') return { body: BATCH_FRESH_RESP }; |
| 3622 | + const runId = url.split('/runs/')[1]?.split('?')[0] ?? ''; |
| 3623 | + if (runId === 'run_fresh_01') |
| 3624 | + return { body: makeTerminalRun('run_fresh_01', 'test_be_01', 'passed') }; |
| 3625 | + if (runId === 'run_fresh_02') |
| 3626 | + return { body: makeTerminalRun('run_fresh_02', 'test_be_02', 'failed') }; |
| 3627 | + return errorBody('NOT_FOUND'); |
| 3628 | + }); |
| 3629 | + |
| 3630 | + await expect( |
| 3631 | + runTestRunAll( |
| 3632 | + { |
| 3633 | + profile: 'default', |
| 3634 | + output: 'json', |
| 3635 | + debug: false, |
| 3636 | + projectId: 'project_be', |
| 3637 | + wait: true, |
| 3638 | + timeoutSeconds: 60, |
| 3639 | + maxConcurrency: 5, |
| 3640 | + report: 'junit', |
| 3641 | + reportFile: reportPath, |
| 3642 | + }, |
| 3643 | + { |
| 3644 | + credentialsPath, |
| 3645 | + fetchImpl, |
| 3646 | + stdout: () => undefined, |
| 3647 | + stderr: () => undefined, |
| 3648 | + sleep: instantSleep, |
| 3649 | + }, |
| 3650 | + ), |
| 3651 | + ).rejects.toMatchObject({ exitCode: 1 }); |
| 3652 | + |
| 3653 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3654 | + expect(xml).toContain('failures="1"'); |
| 3655 | + expect(xml).toContain('name="test_be_02"'); |
| 3656 | + }); |
| 3657 | + |
| 3658 | + it('rejects --report without --wait', async () => { |
| 3659 | + await expect( |
| 3660 | + runTestRunAll( |
| 3661 | + { |
| 3662 | + profile: 'default', |
| 3663 | + output: 'json', |
| 3664 | + debug: false, |
| 3665 | + projectId: 'project_be', |
| 3666 | + wait: false, |
| 3667 | + timeoutSeconds: 60, |
| 3668 | + maxConcurrency: 5, |
| 3669 | + report: 'junit', |
| 3670 | + reportFile: './results.xml', |
| 3671 | + }, |
| 3672 | + {}, |
| 3673 | + ), |
| 3674 | + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); |
| 3675 | + }); |
| 3676 | + |
| 3677 | + it('--dry-run --report junit writes canned sample XML', async () => { |
| 3678 | + const dir = mkdtempSync(join(tmpdir(), 'junit-run-dry-')); |
| 3679 | + const reportPath = join(dir, 'results.xml'); |
| 3680 | + |
| 3681 | + await runTestRunAll( |
| 3682 | + { |
| 3683 | + profile: 'default', |
| 3684 | + output: 'json', |
| 3685 | + debug: false, |
| 3686 | + dryRun: true, |
| 3687 | + projectId: 'project_be', |
| 3688 | + wait: true, |
| 3689 | + timeoutSeconds: 60, |
| 3690 | + maxConcurrency: 5, |
| 3691 | + report: 'junit', |
| 3692 | + reportFile: reportPath, |
| 3693 | + }, |
| 3694 | + { |
| 3695 | + stdout: () => undefined, |
| 3696 | + stderr: () => undefined, |
| 3697 | + }, |
| 3698 | + ); |
| 3699 | + |
| 3700 | + const xml = readFileSync(reportPath, 'utf8'); |
| 3701 | + expect(xml).toContain('name="test_fresh_wave_01"'); |
| 3702 | + expect(xml).toContain('failures="1"'); |
| 3703 | + }); |
| 3704 | +}); |
0 commit comments