@midscene/test 1.15.2 → 1.15.3-beta-20261012131523.0

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.
@@ -634,6 +634,9 @@ const writeTestProjectRunResult = (options)=>{
634
634
  ...result.reportPath ? {
635
635
  report: relativeToSummary(result.summaryPath, result.reportPath)
636
636
  } : {},
637
+ ...result.markdownPath ? {
638
+ markdown: relativeToSummary(result.summaryPath, result.markdownPath)
639
+ } : {},
637
640
  summary: result.summary,
638
641
  projects: result.projects.map((project)=>({
639
642
  projectId: project.projectId,
@@ -1022,6 +1025,103 @@ async function prepareTestRun(options) {
1022
1025
  onProgress: options.onProgress ?? (()=>{})
1023
1026
  };
1024
1027
  }
1028
+ const result_markdown_text = (value)=>value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll('|', '&#124;').replaceAll('[', '&#91;').replaceAll(']', '&#93;').replaceAll('*', '&#42;').replaceAll('_', '&#95;').replaceAll('`', '&#96;').replaceAll('\\', '&#92;').replace(/[\r\n]+/g, ' ');
1029
+ const duration = (ms)=>{
1030
+ const seconds = Math.round(ms / 1000);
1031
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
1032
+ };
1033
+ const href = (from, target)=>relative(dirname(from), target).split(sep).map(encodeURIComponent).join('/');
1034
+ function renderTestResultMarkdown(dump, options = {}) {
1035
+ const { summary } = dump;
1036
+ const outcome = 'success' === dump.status ? `✅ **${summary.passed}/${summary.total} passed**` : `❌ **${summary.failed} failed** · ${summary.passed} passed · ${summary.notRun} not run`;
1037
+ const lines = [
1038
+ '## Midscene Test',
1039
+ '',
1040
+ `${outcome} · ${duration(dump.durationMs)}`,
1041
+ ''
1042
+ ];
1043
+ if (summary.filtered) lines.push(`**Filtered:** ${summary.filtered}`, '');
1044
+ const infrastructure = [
1045
+ summary.collectionErrors ? `${summary.collectionErrors} collection errors` : '',
1046
+ summary.documentFailures ? `${summary.documentFailures} document failures` : '',
1047
+ summary.projectFailures ? `${summary.projectFailures} project failures` : ''
1048
+ ].filter(Boolean);
1049
+ if (infrastructure.length) lines.push(`**Infrastructure:** ${infrastructure.join(' · ')}`, '');
1050
+ const groups = [];
1051
+ const errors = [];
1052
+ const recordSteps = (label, steps)=>{
1053
+ for (const step of steps)if ('failed' === step.status) errors.push(`${label} / ${step.node}: ${step.error?.message ?? 'Step failed'}`);
1054
+ };
1055
+ for (const project of dump.projects){
1056
+ if (project.lifecycle?.setupError) errors.push(`${project.name}: ${project.lifecycle.setupError.message}`);
1057
+ for (const error of project.lifecycle?.teardownErrors ?? [])errors.push(`${project.name}: ${error.message}`);
1058
+ for (const error of project.collectionErrors)errors.push(`${project.name} / ${error.sourcePath}: ${error.error.message}`);
1059
+ for (const document of project.documents){
1060
+ const name = document.sourcePath.replace(/\\/g, '/').replace(/\.(?:yaml|yml)$/, '');
1061
+ const group = {
1062
+ name: dump.projects.length > 1 ? `${project.name} / ${name}` : name,
1063
+ rows: []
1064
+ };
1065
+ groups.push(group);
1066
+ recordSteps(`${project.name} / ${document.sourcePath}`, [
1067
+ ...document.beforeAll,
1068
+ ...document.afterAll
1069
+ ]);
1070
+ for (const error of document.teardownErrors ?? [])errors.push(`${document.sourcePath}: ${error.message}`);
1071
+ for (const failure of document.hostErrors ?? [])errors.push(`${document.sourcePath} / ${failure.phase}: ${failure.error.message}`);
1072
+ for (const testCase of document.cases){
1073
+ const attempt = testCase.attempts.at(-1);
1074
+ const steps = attempt ? [
1075
+ ...attempt.beforeEach,
1076
+ ...attempt.steps,
1077
+ ...attempt.afterEach
1078
+ ] : [];
1079
+ const failedSteps = steps.filter((step)=>'failed' === step.status);
1080
+ const step = failedSteps.at(-1) ?? [
1081
+ ...steps
1082
+ ].reverse().find((item)=>item.agentDetails?.length) ?? steps.at(-1);
1083
+ const route = new URLSearchParams({
1084
+ 'runner-page': 'case',
1085
+ 'runner-project': project.projectId,
1086
+ 'runner-case': testCase.caseId,
1087
+ ...step ? {
1088
+ 'runner-step': step.id
1089
+ } : {}
1090
+ });
1091
+ const target = options.reportHref ? `${options.reportHref}#${route}` : void 0;
1092
+ const name = target ? `[${result_markdown_text(testCase.name)}](<${target}>)` : result_markdown_text(testCase.name);
1093
+ group.rows.push(` - ${'success' === testCase.status ? '✅' : 'not-run' === testCase.status ? '⏭️' : '❌'} ${name}`);
1094
+ }
1095
+ }
1096
+ }
1097
+ for (const diagnostic of dump.diagnostics ?? [])if ('error' === diagnostic.level) errors.push(diagnostic.message);
1098
+ if (errors.length) lines.push('### Execution errors', '', ...errors.map((error)=>`- ${result_markdown_text(error)}`), '');
1099
+ for (const group of groups)if (group.rows.length) lines.push(`- ${result_markdown_text(group.name)}`, ...group.rows);
1100
+ if (!groups.some((group)=>group.rows.length)) lines.push('No cases were reported.');
1101
+ if (options.reportHref) lines.push('', `[Open the Midscene Test report ↗](<${options.reportHref}>)`);
1102
+ return `${lines.join('\n')}\n`;
1103
+ }
1104
+ const emptyIndex = ()=>({
1105
+ sources: [],
1106
+ metrics: {
1107
+ modelCallCount: 0,
1108
+ modelTimeMs: 0,
1109
+ promptTokens: 0,
1110
+ cachedInputTokens: 0,
1111
+ completionTokens: 0,
1112
+ totalTokens: 0
1113
+ }
1114
+ });
1115
+ async function writeTestResultMarkdown(result, index = emptyIndex()) {
1116
+ const output = join(dirname(result.summaryPath), 'result.md');
1117
+ const dump = buildTestRunReportDump(result, index);
1118
+ await writeFile(output, renderTestResultMarkdown(dump, {
1119
+ ...result.reportPath ? {
1120
+ reportHref: href(output, result.reportPath)
1121
+ } : {}
1122
+ }));
1123
+ return output;
1124
+ }
1025
1125
  const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
1026
1126
  const formatStep = (info)=>{
1027
1127
  const position = stepPosition(info);
@@ -1480,6 +1580,7 @@ async function runPreparedTestProject(plan) {
1480
1580
  } : {}
1481
1581
  };
1482
1582
  let completedResult = result;
1583
+ let reportIndex;
1483
1584
  const publish = async (operation, path, callback)=>{
1484
1585
  try {
1485
1586
  await callback();
@@ -1510,6 +1611,7 @@ async function runPreparedTestProject(plan) {
1510
1611
  overwrite: false,
1511
1612
  sources: collectTestRunReportSources(completedResult),
1512
1613
  buildRunnerDump: (index)=>{
1614
+ reportIndex = index;
1513
1615
  const dump = buildTestRunReportDump(completedResult, index);
1514
1616
  const warn = getDebug('test-runner:report-assembler', {
1515
1617
  console: true
@@ -1527,6 +1629,13 @@ async function runPreparedTestProject(plan) {
1527
1629
  for (const publication of plan.publications)await publish(publication.operation, publication.path, async ()=>{
1528
1630
  await publication.publish(completedResult);
1529
1631
  });
1632
+ await publish('write-result', join(dirname(summaryPath), 'result.md'), async ()=>{
1633
+ const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
1634
+ completedResult = {
1635
+ ...completedResult,
1636
+ markdownPath
1637
+ };
1638
+ });
1530
1639
  await writeSummary();
1531
1640
  if (hasInfrastructureError) throw new WorkflowExecutionFailure(completedResult, infrastructureErrors);
1532
1641
  return completedResult;