donobu 5.41.0 → 5.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/donobu-cli.js +142 -163
- package/dist/esm/cli/donobu-cli.js +142 -163
- package/dist/esm/main.d.ts +0 -1
- package/dist/esm/main.js +1 -3
- package/dist/main.d.ts +0 -1
- package/dist/main.js +1 -3
- package/package.json +2 -8
- package/dist/cli/generate-site-tests.d.ts +0 -2
- package/dist/cli/generate-site-tests.js +0 -43
- package/dist/codegen/runGenerateSiteTests.d.ts +0 -69
- package/dist/codegen/runGenerateSiteTests.js +0 -937
- package/dist/esm/cli/generate-site-tests.d.ts +0 -2
- package/dist/esm/cli/generate-site-tests.js +0 -43
- package/dist/esm/codegen/runGenerateSiteTests.d.ts +0 -69
- package/dist/esm/codegen/runGenerateSiteTests.js +0 -937
package/dist/cli/donobu-cli.js
CHANGED
|
@@ -309,14 +309,23 @@ const noopAsync = async () => { };
|
|
|
309
309
|
* wrapper config when necessary so we can append the JSON reporter alongside
|
|
310
310
|
* user-specified reporters.
|
|
311
311
|
*/
|
|
312
|
-
async function ensureJsonReporter(originalArgs, options
|
|
312
|
+
async function ensureJsonReporter(originalArgs, options) {
|
|
313
313
|
const args = [...originalArgs];
|
|
314
|
+
// For branches that don't use the config wrapper, the JSON reporter has no
|
|
315
|
+
// explicit `outputFile` and Playwright falls back to env vars — which we set
|
|
316
|
+
// in applyJsonReportEnv to (playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME).
|
|
317
|
+
// Either honour an explicit override or reconstruct that path here.
|
|
318
|
+
const envDerivedJsonOutputFile = options.jsonOutputFile ??
|
|
319
|
+
path.join(options.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
|
|
314
320
|
const argInjection = injectJsonReporterIntoArgs(args);
|
|
315
321
|
if (argInjection.reporterFlagFound) {
|
|
316
322
|
return {
|
|
317
323
|
args,
|
|
318
324
|
cleanup: noopAsync,
|
|
319
|
-
|
|
325
|
+
resolveJsonReporterInfo: async () => ({
|
|
326
|
+
userHadJson: argInjection.userHadJson,
|
|
327
|
+
jsonOutputFile: envDerivedJsonOutputFile,
|
|
328
|
+
}),
|
|
320
329
|
};
|
|
321
330
|
}
|
|
322
331
|
const configPath = await resolvePlaywrightConfigPath(args);
|
|
@@ -332,17 +341,22 @@ async function ensureJsonReporter(originalArgs, options = {}) {
|
|
|
332
341
|
return {
|
|
333
342
|
args,
|
|
334
343
|
cleanup: noopAsync,
|
|
335
|
-
|
|
344
|
+
resolveJsonReporterInfo: async () => ({
|
|
345
|
+
userHadJson: false,
|
|
346
|
+
jsonOutputFile: envDerivedJsonOutputFile,
|
|
347
|
+
}),
|
|
336
348
|
};
|
|
337
349
|
}
|
|
338
|
-
const wrapper = await createConfigWrapperWithJsonReporter(configPath,
|
|
350
|
+
const wrapper = await createConfigWrapperWithJsonReporter(configPath, {
|
|
351
|
+
jsonOutputFile: options.jsonOutputFile,
|
|
352
|
+
});
|
|
339
353
|
Logger_1.appLogger.debug(`Augmenting Playwright config at ${configPath} with temporary wrapper ${wrapper.configPath} to ensure JSON reporter.`);
|
|
340
354
|
const strippedArgs = stripConfigArgs(args);
|
|
341
355
|
const finalArgs = insertConfigArg(strippedArgs, wrapper.configPath);
|
|
342
356
|
return {
|
|
343
357
|
args: finalArgs,
|
|
344
358
|
cleanup: wrapper.cleanup,
|
|
345
|
-
|
|
359
|
+
resolveJsonReporterInfo: wrapper.resolveJsonReporterInfo,
|
|
346
360
|
};
|
|
347
361
|
}
|
|
348
362
|
function hasReporterArg(args) {
|
|
@@ -499,18 +513,26 @@ async function createConfigWrapperWithJsonReporter(originalConfigPath, options =
|
|
|
499
513
|
// The wrapper writes the sentinel inside its top-level config-load code, so
|
|
500
514
|
// it exists by the time Playwright has loaded the config. Read it after the
|
|
501
515
|
// wrapped run finishes and before `cleanup` tears the staging dir down.
|
|
502
|
-
const
|
|
516
|
+
const resolveJsonReporterInfo = async () => {
|
|
503
517
|
try {
|
|
504
518
|
const raw = await fs_1.promises.readFile(sentinelPath, 'utf8');
|
|
505
|
-
|
|
519
|
+
const parsed = JSON.parse(raw);
|
|
520
|
+
return {
|
|
521
|
+
userHadJson: parsed?.userHadJson === true,
|
|
522
|
+
jsonOutputFile: typeof parsed?.jsonOutputFile === 'string' &&
|
|
523
|
+
parsed.jsonOutputFile.length > 0
|
|
524
|
+
? parsed.jsonOutputFile
|
|
525
|
+
: null,
|
|
526
|
+
};
|
|
506
527
|
}
|
|
507
528
|
catch {
|
|
508
|
-
// Sentinel missing (wrapper never loaded, or already
|
|
509
|
-
// the side of "no user JSON" so we don't leave a
|
|
510
|
-
|
|
529
|
+
// Sentinel missing or malformed (wrapper never loaded, or already
|
|
530
|
+
// cleaned up). Err on the side of "no user JSON" so we don't leave a
|
|
531
|
+
// giant artifact behind.
|
|
532
|
+
return { userHadJson: false, jsonOutputFile: null };
|
|
511
533
|
}
|
|
512
534
|
};
|
|
513
|
-
return { configPath: wrapperPath, cleanup,
|
|
535
|
+
return { configPath: wrapperPath, cleanup, resolveJsonReporterInfo };
|
|
514
536
|
}
|
|
515
537
|
function buildConfigWrapperContent(originalConfigPath, userHadJsonSentinelPath, jsonOutputFileOverride) {
|
|
516
538
|
const sanitisedPath = originalConfigPath.replace(/\\/g, '\\\\');
|
|
@@ -622,6 +644,17 @@ const reporters = Array.isArray(normalizedConfig.reporter)
|
|
|
622
644
|
? [normalizedConfig.reporter]
|
|
623
645
|
: [];
|
|
624
646
|
|
|
647
|
+
function computeEnvDerivedJsonOutputFile() {
|
|
648
|
+
const outputDir = process.env.PLAYWRIGHT_JSON_OUTPUT_DIR
|
|
649
|
+
? path.resolve(process.cwd(), process.env.PLAYWRIGHT_JSON_OUTPUT_DIR)
|
|
650
|
+
: path.resolve(configDir, 'test-results');
|
|
651
|
+
const outputName =
|
|
652
|
+
process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || '${defaultJsonName}';
|
|
653
|
+
return path.isAbsolute(outputName)
|
|
654
|
+
? outputName
|
|
655
|
+
: path.join(outputDir, outputName);
|
|
656
|
+
}
|
|
657
|
+
|
|
625
658
|
const hasJsonReporter = reporters.some((entry) => {
|
|
626
659
|
if (!entry) {
|
|
627
660
|
return false;
|
|
@@ -640,30 +673,11 @@ const hasJsonReporter = reporters.some((entry) => {
|
|
|
640
673
|
return false;
|
|
641
674
|
});
|
|
642
675
|
|
|
643
|
-
// Tell the parent orchestrator whether the user's own config defined a JSON
|
|
644
|
-
// reporter (vs us about to force-inject one). The orchestrator reads this to
|
|
645
|
-
// decide whether to persist the merged auto-heal report — we don't want to
|
|
646
|
-
// leave a multi-megabyte JSON artifact behind for users who never asked for one.
|
|
647
|
-
try {
|
|
648
|
-
require('fs').writeFileSync(userHadJsonSentinelPath, hasJsonReporter ? '1' : '0', 'utf8');
|
|
649
|
-
} catch (_) {
|
|
650
|
-
// Non-fatal — the orchestrator falls back to "no user JSON" if missing.
|
|
651
|
-
}
|
|
652
|
-
|
|
653
676
|
if (!hasJsonReporter) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
(
|
|
657
|
-
|
|
658
|
-
? path.resolve(process.cwd(), process.env.PLAYWRIGHT_JSON_OUTPUT_DIR)
|
|
659
|
-
: path.resolve(configDir, 'test-results');
|
|
660
|
-
const outputName =
|
|
661
|
-
process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || '${defaultJsonName}';
|
|
662
|
-
return path.isAbsolute(outputName)
|
|
663
|
-
? outputName
|
|
664
|
-
: path.join(outputDir, outputName);
|
|
665
|
-
})();
|
|
666
|
-
reporters.push(['json', { outputFile }]);
|
|
677
|
+
reporters.push([
|
|
678
|
+
'json',
|
|
679
|
+
{ outputFile: forcedJsonOutputFile || computeEnvDerivedJsonOutputFile() },
|
|
680
|
+
]);
|
|
667
681
|
}
|
|
668
682
|
|
|
669
683
|
// Resolve a reporter name to an absolute path so that Playwright can find it
|
|
@@ -729,6 +743,57 @@ const normalisedReporters = reporters.map((entry) => {
|
|
|
729
743
|
return entry;
|
|
730
744
|
});
|
|
731
745
|
|
|
746
|
+
// Walk the final reporter list to surface the JSON output path the orchestrator
|
|
747
|
+
// should treat as the merge target — Playwright's JSON reporter accepts the
|
|
748
|
+
// path via either reporter options or env var, so checking both keeps us in
|
|
749
|
+
// sync with whatever Playwright will actually do.
|
|
750
|
+
function extractJsonOutputFile(rep) {
|
|
751
|
+
for (const entry of rep) {
|
|
752
|
+
if (Array.isArray(entry) && entry[0] === 'json') {
|
|
753
|
+
if (
|
|
754
|
+
entry[1] &&
|
|
755
|
+
typeof entry[1] === 'object' &&
|
|
756
|
+
typeof entry[1].outputFile === 'string'
|
|
757
|
+
) {
|
|
758
|
+
return entry[1].outputFile;
|
|
759
|
+
}
|
|
760
|
+
return computeEnvDerivedJsonOutputFile();
|
|
761
|
+
}
|
|
762
|
+
if (typeof entry === 'string') {
|
|
763
|
+
const segments = entry
|
|
764
|
+
.split(',')
|
|
765
|
+
.map((s) => s.trim())
|
|
766
|
+
.filter((s) => s.length > 0);
|
|
767
|
+
for (const segment of segments) {
|
|
768
|
+
const eqIndex = segment.indexOf('=');
|
|
769
|
+
const name = eqIndex === -1 ? segment : segment.slice(0, eqIndex);
|
|
770
|
+
const value = eqIndex === -1 ? '' : segment.slice(eqIndex + 1);
|
|
771
|
+
if (name === 'json') {
|
|
772
|
+
return value ? absolutify(value) : computeEnvDerivedJsonOutputFile();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Tell the parent orchestrator (a) whether the user's own config defined a
|
|
781
|
+
// JSON reporter and (b) the exact path that JSON will land at. The path lets
|
|
782
|
+
// the orchestrator merge auto-heal results onto the user's file directly,
|
|
783
|
+
// without filesystem-scanning heuristics that can pick the wrong file.
|
|
784
|
+
try {
|
|
785
|
+
require('fs').writeFileSync(
|
|
786
|
+
userHadJsonSentinelPath,
|
|
787
|
+
JSON.stringify({
|
|
788
|
+
userHadJson: hasJsonReporter,
|
|
789
|
+
jsonOutputFile: extractJsonOutputFile(normalisedReporters),
|
|
790
|
+
}),
|
|
791
|
+
'utf8',
|
|
792
|
+
);
|
|
793
|
+
} catch (_) {
|
|
794
|
+
// Non-fatal — the orchestrator falls back to "no user JSON" if missing.
|
|
795
|
+
}
|
|
796
|
+
|
|
732
797
|
module.exports = {
|
|
733
798
|
...normalizedConfig,
|
|
734
799
|
reporter: normalisedReporters,
|
|
@@ -767,65 +832,6 @@ async function copyJsonReport(outputDir, destinationPath, options = {}) {
|
|
|
767
832
|
}
|
|
768
833
|
return null;
|
|
769
834
|
}
|
|
770
|
-
/**
|
|
771
|
-
* Locate the canonical Playwright JSON report and read it into memory.
|
|
772
|
-
* Uses the same candidate-resolution logic as {@link copyJsonReport} but
|
|
773
|
-
* avoids writing a duplicate file to disk.
|
|
774
|
-
*/
|
|
775
|
-
async function loadJsonReport(outputDir, options = {}) {
|
|
776
|
-
const candidatePaths = new Set();
|
|
777
|
-
const envDefinedPath = resolveEnvJsonReportPath(options.envOverrides);
|
|
778
|
-
if (envDefinedPath) {
|
|
779
|
-
candidatePaths.add(envDefinedPath);
|
|
780
|
-
}
|
|
781
|
-
candidatePaths.add(path.join(outputDir, PLAYWRIGHT_JSON_REPORT_FILENAME));
|
|
782
|
-
(options.additionalCandidates ?? []).forEach((candidate) => {
|
|
783
|
-
if (candidate) {
|
|
784
|
-
candidatePaths.add(candidate);
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
for (const sourcePath of candidatePaths) {
|
|
788
|
-
const data = await readJsonIfExists(sourcePath);
|
|
789
|
-
if (data) {
|
|
790
|
-
return { sourcePath, data };
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const fallbackSource = await findJsonReportInDir(outputDir);
|
|
794
|
-
if (fallbackSource) {
|
|
795
|
-
const data = await readJsonIfExists(fallbackSource);
|
|
796
|
-
if (data) {
|
|
797
|
-
return { sourcePath: fallbackSource, data };
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Resolve the canonical Playwright JSON report path without reading or
|
|
804
|
-
* copying. Uses the same candidate-resolution order as {@link loadJsonReport}.
|
|
805
|
-
*/
|
|
806
|
-
async function findJsonReportPath(outputDir, options = {}) {
|
|
807
|
-
const candidatePaths = new Set();
|
|
808
|
-
const envDefinedPath = resolveEnvJsonReportPath(options.envOverrides);
|
|
809
|
-
if (envDefinedPath) {
|
|
810
|
-
candidatePaths.add(envDefinedPath);
|
|
811
|
-
}
|
|
812
|
-
candidatePaths.add(path.join(outputDir, PLAYWRIGHT_JSON_REPORT_FILENAME));
|
|
813
|
-
(options.additionalCandidates ?? []).forEach((candidate) => {
|
|
814
|
-
if (candidate) {
|
|
815
|
-
candidatePaths.add(candidate);
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
for (const candidate of candidatePaths) {
|
|
819
|
-
try {
|
|
820
|
-
await fs_1.promises.access(candidate, fs_1.constants.F_OK);
|
|
821
|
-
return candidate;
|
|
822
|
-
}
|
|
823
|
-
catch {
|
|
824
|
-
// not found, try next
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return findJsonReportInDir(outputDir);
|
|
828
|
-
}
|
|
829
835
|
function resolveEnvJsonReportPath(envOverrides) {
|
|
830
836
|
if (!envOverrides) {
|
|
831
837
|
return null;
|
|
@@ -859,6 +865,16 @@ async function tryCopyReport(sourcePath, destinationPath) {
|
|
|
859
865
|
return false;
|
|
860
866
|
}
|
|
861
867
|
}
|
|
868
|
+
/** Files we write into the Playwright output dir ourselves. They happen to
|
|
869
|
+
* share `suites`-shaped JSON with Playwright's own report, so the scan
|
|
870
|
+
* fallback must exclude them or it can pick our own state file (or a
|
|
871
|
+
* previous heal-run copy) as the "user's JSON report". */
|
|
872
|
+
function isDonobuInternalJsonFile(fileName) {
|
|
873
|
+
return (fileName === model_1.DONOBU_REPORT_STATE_FILENAME ||
|
|
874
|
+
fileName.startsWith('donobu-auto-heal-report-') ||
|
|
875
|
+
fileName.startsWith('donobu-heal-report-') ||
|
|
876
|
+
fileName === 'donobu-heal-merged-report.json');
|
|
877
|
+
}
|
|
862
878
|
async function findJsonReportInDir(outputDir) {
|
|
863
879
|
let entries;
|
|
864
880
|
try {
|
|
@@ -869,6 +885,7 @@ async function findJsonReportInDir(outputDir) {
|
|
|
869
885
|
}
|
|
870
886
|
const candidates = entries
|
|
871
887
|
.filter((entry) => entry.endsWith('.json'))
|
|
888
|
+
.filter((entry) => !isDonobuInternalJsonFile(entry))
|
|
872
889
|
.sort((a, b) => {
|
|
873
890
|
const aScore = a.includes('report') ? 0 : 1;
|
|
874
891
|
const bScore = b.includes('report') ? 0 : 1;
|
|
@@ -895,18 +912,6 @@ async function isLikelyPlaywrightReport(filePath) {
|
|
|
895
912
|
return false;
|
|
896
913
|
}
|
|
897
914
|
}
|
|
898
|
-
async function overwriteReportTargets(sourcePath, targets) {
|
|
899
|
-
const uniqueTargets = Array.from(new Set(targets)).filter((target) => target && target !== sourcePath);
|
|
900
|
-
await Promise.all(uniqueTargets.map(async (targetPath) => {
|
|
901
|
-
try {
|
|
902
|
-
await ensureDirectory(path.dirname(targetPath));
|
|
903
|
-
await fs_1.promises.copyFile(sourcePath, targetPath);
|
|
904
|
-
}
|
|
905
|
-
catch (error) {
|
|
906
|
-
Logger_1.appLogger.warn(`Failed to copy merged Playwright report to ${targetPath}.`, error);
|
|
907
|
-
}
|
|
908
|
-
}));
|
|
909
|
-
}
|
|
910
915
|
/**
|
|
911
916
|
* Remove the JSON artifacts Donobu wrote purely for its own use:
|
|
912
917
|
*
|
|
@@ -924,8 +929,8 @@ async function cleanupForceInjectedJsonArtifacts(params) {
|
|
|
924
929
|
const targets = [
|
|
925
930
|
path.join(params.playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME),
|
|
926
931
|
];
|
|
927
|
-
if (!params.
|
|
928
|
-
targets.push(params.
|
|
932
|
+
if (!params.userHadJson && params.jsonOutputFile) {
|
|
933
|
+
targets.push(params.jsonOutputFile);
|
|
929
934
|
}
|
|
930
935
|
await Promise.all(targets.map(async (target) => {
|
|
931
936
|
try {
|
|
@@ -1331,6 +1336,7 @@ async function attemptAutoHealRun(params) {
|
|
|
1331
1336
|
const healJsonReportPath = path.join(staging.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
|
|
1332
1337
|
const reporterSetup = await ensureJsonReporter(healArgsForRun, {
|
|
1333
1338
|
jsonOutputFile: healJsonReportPath,
|
|
1339
|
+
playwrightOutputDir: staging.playwrightOutputDir,
|
|
1334
1340
|
});
|
|
1335
1341
|
try {
|
|
1336
1342
|
healExitCode = await runPlaywright(reporterSetup.args, envOverrides);
|
|
@@ -1343,7 +1349,7 @@ async function attemptAutoHealRun(params) {
|
|
|
1343
1349
|
// Donobu's by-product. The merge step below reads the report from the
|
|
1344
1350
|
// staging dir's state file, which is still alive at this point, so we
|
|
1345
1351
|
// don't need a persisted copy for our own use.
|
|
1346
|
-
const healReportCopy = params.
|
|
1352
|
+
const healReportCopy = params.userJsonOutputFile
|
|
1347
1353
|
? await copyJsonReport(staging.playwrightOutputDir, path.join(params.playwrightOutputDir, `donobu-auto-heal-report-${Date.now()}.json`), {
|
|
1348
1354
|
envOverrides,
|
|
1349
1355
|
additionalCandidates: [healJsonReportPath],
|
|
@@ -1377,19 +1383,18 @@ async function attemptAutoHealRun(params) {
|
|
|
1377
1383
|
// reporter output for configs that don't wire up the Donobu reporter.
|
|
1378
1384
|
const healReport = await loadDonobuReportForMerge(staging.playwrightOutputDir, healReportCopy?.destinationPath ?? undefined);
|
|
1379
1385
|
if (params.initialReport || healReport) {
|
|
1380
|
-
// Write the merged report directly to the user's
|
|
1386
|
+
// Write the merged report directly to the user's JSON reporter target
|
|
1381
1387
|
// when one exists. When the user did not configure a JSON reporter,
|
|
1382
|
-
// `
|
|
1383
|
-
// leaving a multi-megabyte fallback artifact behind. The HTML /
|
|
1384
|
-
// / Slack regeneration below uses the in-memory merged object,
|
|
1385
|
-
// don't depend on a JSON file landing on disk.
|
|
1386
|
-
const primaryTarget = params.reportTargets[0];
|
|
1388
|
+
// `userJsonOutputFile` is null — skip the disk write entirely rather
|
|
1389
|
+
// than leaving a multi-megabyte fallback artifact behind. The HTML /
|
|
1390
|
+
// Markdown / Slack regeneration below uses the in-memory merged object,
|
|
1391
|
+
// so they don't depend on a JSON file landing on disk.
|
|
1387
1392
|
const mergedReport = await mergePlaywrightJsonReports({
|
|
1388
1393
|
initialReport: params.initialReport ?? null,
|
|
1389
1394
|
initialReportSourcePath: params.initialReportSourcePath,
|
|
1390
1395
|
healReport,
|
|
1391
1396
|
healReportSourcePath: healReportCopy?.destinationPath ?? undefined,
|
|
1392
|
-
mergedReportPath:
|
|
1397
|
+
mergedReportPath: params.userJsonOutputFile ?? undefined,
|
|
1393
1398
|
healedTests: evaluation.eligiblePlans.map((record) => ({
|
|
1394
1399
|
plan: record.plan,
|
|
1395
1400
|
testCase: record.evidence.failureContext.testCase,
|
|
@@ -1398,11 +1403,6 @@ async function attemptAutoHealRun(params) {
|
|
|
1398
1403
|
outputDir: params.playwrightOutputDir,
|
|
1399
1404
|
triageRunDir: params.triageRunDir,
|
|
1400
1405
|
});
|
|
1401
|
-
// Copy to any remaining report targets (skip the primary — already
|
|
1402
|
-
// written there).
|
|
1403
|
-
if (mergedReport && primaryTarget && params.reportTargets.length > 1) {
|
|
1404
|
-
await overwriteReportTargets(primaryTarget, params.reportTargets.slice(1));
|
|
1405
|
-
}
|
|
1406
1406
|
if (mergedReport) {
|
|
1407
1407
|
await regenerateDonobuReports(mergedReport);
|
|
1408
1408
|
await writeAutoHealPullRequestBody(mergedReport, params.playwrightOutputDir);
|
|
@@ -1772,38 +1772,26 @@ async function runTestCommand(cliArgs) {
|
|
|
1772
1772
|
if (effectiveOptions.autoHeal) {
|
|
1773
1773
|
envOverrides.DONOBU_AUTO_HEAL_ORCHESTRATED = '1';
|
|
1774
1774
|
}
|
|
1775
|
-
const reporterSetup = await ensureJsonReporter(playwrightArgs
|
|
1775
|
+
const reporterSetup = await ensureJsonReporter(playwrightArgs, {
|
|
1776
|
+
playwrightOutputDir,
|
|
1777
|
+
});
|
|
1776
1778
|
const runArgs = reporterSetup.args;
|
|
1777
1779
|
Logger_1.appLogger.debug(`Initial Playwright args: ${JSON.stringify(runArgs)} with env overrides ${JSON.stringify(envOverrides)}`);
|
|
1778
1780
|
let exitCode;
|
|
1779
|
-
let
|
|
1781
|
+
let jsonReporterInfo = {
|
|
1782
|
+
userHadJson: false,
|
|
1783
|
+
jsonOutputFile: null,
|
|
1784
|
+
};
|
|
1780
1785
|
try {
|
|
1781
1786
|
exitCode = await runPlaywright(runArgs, envOverrides);
|
|
1782
1787
|
// Read before cleanup, since the wrapper-sentinel lives in the staging dir.
|
|
1783
|
-
|
|
1784
|
-
await reporterSetup.resolveUserConfiguredJson();
|
|
1788
|
+
jsonReporterInfo = await reporterSetup.resolveJsonReporterInfo();
|
|
1785
1789
|
}
|
|
1786
1790
|
finally {
|
|
1787
1791
|
await reporterSetup.cleanup();
|
|
1788
1792
|
}
|
|
1793
|
+
const { userHadJson, jsonOutputFile } = jsonReporterInfo;
|
|
1789
1794
|
let generatedPlans = [];
|
|
1790
|
-
const reportTargets = new Set();
|
|
1791
|
-
// Discover the canonical report path eagerly (cheap — no file read) so we
|
|
1792
|
-
// can populate reportTargets regardless of whether auto-heal is needed.
|
|
1793
|
-
// Uses the same candidate-resolution order as loadJsonReport to avoid
|
|
1794
|
-
// accidentally picking up leftover donobu-*-report-*.json files.
|
|
1795
|
-
// Only treat the discovered report as a real "target to overwrite" when the
|
|
1796
|
-
// user actually configured a JSON reporter — when Donobu force-injected one
|
|
1797
|
-
// for its own use, leaving a multi-megabyte JSON behind is just noise.
|
|
1798
|
-
const discoveredReport = await findJsonReportPath(playwrightOutputDir, {
|
|
1799
|
-
envOverrides,
|
|
1800
|
-
additionalCandidates: [
|
|
1801
|
-
path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
|
|
1802
|
-
],
|
|
1803
|
-
});
|
|
1804
|
-
if (discoveredReport && userConfiguredJsonReporter) {
|
|
1805
|
-
reportTargets.add(discoveredReport);
|
|
1806
|
-
}
|
|
1807
1795
|
try {
|
|
1808
1796
|
if (exitCode === 0 || !effectiveOptions.autoHeal) {
|
|
1809
1797
|
// Auto-heal wasn't attempted (either tests passed or the user disabled
|
|
@@ -1815,29 +1803,20 @@ async function runTestCommand(cliArgs) {
|
|
|
1815
1803
|
return exitCode;
|
|
1816
1804
|
}
|
|
1817
1805
|
// Tests failed and auto-heal is enabled — load the initial report into
|
|
1818
|
-
// memory so we can merge it with the heal-run report later
|
|
1819
|
-
//
|
|
1820
|
-
|
|
1806
|
+
// memory so we can merge it with the heal-run report later. Prefer the
|
|
1807
|
+
// Donobu reporter's state file (carries Donobu-specific metadata like the
|
|
1808
|
+
// HTML output path); fall back to reading the user's JSON file directly.
|
|
1821
1809
|
let initialDonobuReport = null;
|
|
1822
1810
|
if (triageEnabled) {
|
|
1823
|
-
loadedPlaywrightJson = await loadJsonReport(playwrightOutputDir, {
|
|
1824
|
-
envOverrides,
|
|
1825
|
-
additionalCandidates: [
|
|
1826
|
-
path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
|
|
1827
|
-
],
|
|
1828
|
-
});
|
|
1829
|
-
if (loadedPlaywrightJson && userConfiguredJsonReporter) {
|
|
1830
|
-
reportTargets.add(loadedPlaywrightJson.sourcePath);
|
|
1831
|
-
}
|
|
1832
|
-
// Prefer the Donobu reporter's state file (carries the HTML output path
|
|
1833
|
-
// and any Donobu-specific metadata); fall back to the Playwright JSON for
|
|
1834
|
-
// configs that don't wire up the Donobu reporter.
|
|
1835
1811
|
const stateReport = await readJsonIfExists(path.join(playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME));
|
|
1836
1812
|
initialDonobuReport =
|
|
1837
|
-
stateReport ??
|
|
1813
|
+
stateReport ??
|
|
1814
|
+
(jsonOutputFile
|
|
1815
|
+
? await readJsonIfExists(jsonOutputFile)
|
|
1816
|
+
: null);
|
|
1838
1817
|
}
|
|
1839
1818
|
if (triageEnabled && triageContext) {
|
|
1840
|
-
generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs,
|
|
1819
|
+
generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs, jsonOutputFile ?? undefined);
|
|
1841
1820
|
}
|
|
1842
1821
|
const autoHealOutcome = await attemptAutoHealRun({
|
|
1843
1822
|
options: effectiveOptions,
|
|
@@ -1846,10 +1825,9 @@ async function runTestCommand(cliArgs) {
|
|
|
1846
1825
|
generatedPlans,
|
|
1847
1826
|
currentExitCode: exitCode,
|
|
1848
1827
|
initialReport: initialDonobuReport,
|
|
1849
|
-
initialReportSourcePath:
|
|
1828
|
+
initialReportSourcePath: jsonOutputFile ?? undefined,
|
|
1850
1829
|
triageRunDir: triageContext?.runDir,
|
|
1851
|
-
|
|
1852
|
-
userConfiguredJsonReporter,
|
|
1830
|
+
userJsonOutputFile: userHadJson ? jsonOutputFile : null,
|
|
1853
1831
|
});
|
|
1854
1832
|
// When auto-heal was eligible-checked but didn't actually run a rerun (no
|
|
1855
1833
|
// actionable directives), nothing downstream re-renders the Slack payload —
|
|
@@ -1868,8 +1846,8 @@ async function runTestCommand(cliArgs) {
|
|
|
1868
1846
|
// (their own JSON reporter target, HTML, Markdown, Slack) is preserved.
|
|
1869
1847
|
await cleanupForceInjectedJsonArtifacts({
|
|
1870
1848
|
playwrightOutputDir,
|
|
1871
|
-
|
|
1872
|
-
|
|
1849
|
+
userHadJson,
|
|
1850
|
+
jsonOutputFile,
|
|
1873
1851
|
});
|
|
1874
1852
|
}
|
|
1875
1853
|
}
|
|
@@ -1971,6 +1949,7 @@ async function runHealCommand(cliArgs) {
|
|
|
1971
1949
|
const healJsonReportPath = path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
|
|
1972
1950
|
const reporterSetup = await ensureJsonReporter(healArgsWithDirectives, {
|
|
1973
1951
|
jsonOutputFile: healJsonReportPath,
|
|
1952
|
+
playwrightOutputDir,
|
|
1974
1953
|
});
|
|
1975
1954
|
Logger_1.appLogger.debug(`Heal command Playwright args: ${JSON.stringify(reporterSetup.args)} with env overrides ${JSON.stringify(envOverrides)}`);
|
|
1976
1955
|
let exitCode;
|