@midscene/test 1.17.5 → 1.17.7

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.
@@ -7,6 +7,7 @@ import { globSync } from "tinyglobby";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { JSON_SCHEMA, load } from "js-yaml";
9
9
  import { mkdir, writeFile } from "node:fs/promises";
10
+ import { resolveMidsceneRunDir, runWithMidsceneRunDir } from "@midscene/shared/common";
10
11
  import { setMaxListeners } from "node:events";
11
12
  import { ReportSession, TestRunReportAssembler, calculateTestRunHealth } from "@midscene/core/report";
12
13
  import { getDebug, runWithConsoleLogScope } from "@midscene/shared/logger";
@@ -249,8 +250,7 @@ const validateTestOptions = (value, hasExplicitProjects)=>{
249
250
  testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
250
251
  });
251
252
  };
252
- const validateOutputPath = (value, fallback, label)=>{
253
- if (void 0 === value) return fallback;
253
+ const validateOutputPath = (value, label)=>{
254
254
  if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
255
255
  return value;
256
256
  };
@@ -264,7 +264,9 @@ const validateOutput = (value)=>{
264
264
  if (void 0 !== candidate.reportFormat && 'single-html' !== candidate.reportFormat && 'html-and-external-assets' !== candidate.reportFormat) throw new TypeError('Midscene config output.reportFormat must be single-html or html-and-external-assets.');
265
265
  return Object.freeze({
266
266
  reportFormat: candidate.reportFormat ?? 'single-html',
267
- reportDir: validateOutputPath(candidate.reportDir, './midscene_run/report', 'output.reportDir')
267
+ ...void 0 === candidate.reportDir ? {} : {
268
+ reportDir: validateOutputPath(candidate.reportDir, 'output.reportDir')
269
+ }
268
270
  });
269
271
  };
270
272
  const validateTestProjectDefinition = (definition)=>{
@@ -852,8 +854,16 @@ const DEFAULT_TEST_FILE_SELECTION = {
852
854
  ]
853
855
  };
854
856
  const project_preparation_toPosix = (value)=>value.split(sep).join('/');
855
- const discoverResolvedTestFiles = (projectRoot, selection)=>{
857
+ const discoverResolvedTestFiles = (projectRoot, selection, excludedDirectories = [])=>{
856
858
  const root = external_node_path_resolve(projectRoot);
859
+ const outputPatterns = excludedDirectories.flatMap((directory)=>{
860
+ const withinRoot = relative(root, directory);
861
+ if (!withinRoot || isAbsolute(withinRoot) || '..' === withinRoot || withinRoot.startsWith(`..${sep}`)) return [];
862
+ const literal = project_preparation_toPosix(withinRoot).replace(/([*?{}[\]()!])/g, '\\$1');
863
+ return [
864
+ `${literal}/**`
865
+ ];
866
+ });
857
867
  const match = (patterns)=>globSync(patterns, {
858
868
  absolute: true,
859
869
  caseSensitiveMatch: false,
@@ -863,6 +873,7 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
863
873
  followSymbolicLinks: false,
864
874
  ignore: [
865
875
  ...ALWAYS_IGNORED_PATTERNS,
876
+ ...outputPatterns,
866
877
  ...selection.exclude ?? []
867
878
  ],
868
879
  onlyFiles: true
@@ -937,7 +948,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
937
948
  const platform = options.platform?.trim();
938
949
  const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
939
950
  const prerequisiteFile = options.prerequisiteFile ? external_node_path_resolve(options.prerequisiteFile) : void 0;
940
- const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
951
+ const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection, options.excludedDirectories);
941
952
  const files = [
942
953
  ...prerequisiteFile ? [
943
954
  prerequisiteFile
@@ -1102,40 +1113,61 @@ async function prepareTestRun(options) {
1102
1113
  if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test --config only accepts a native TypeScript or JavaScript project config: ${configPath}. Run legacy batch YAML with \`midscene --config ${options.configPath}\`.`);
1103
1114
  if (options.configPath && !existsSync(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
1104
1115
  const definition = await loadTestProject(configPath);
1105
- const resultDir = options.resultDir ? external_node_path_resolve(input.cwd, options.resultDir) : join(input.projectRoot, '.midscene', 'test-results');
1106
- const runDir = join(resultDir, runId);
1107
- const summaryPath = join(runDir, 'summary.json');
1108
- const reportDir = external_node_path_resolve(input.projectRoot, definition.output.reportDir);
1109
- mkdirSync(resultDir, {
1110
- recursive: true
1116
+ const artifactDir = resolveMidsceneRunDir(input.projectRoot);
1117
+ return runWithMidsceneRunDir(artifactDir, async ()=>{
1118
+ const resultDir = options.resultDir ? external_node_path_resolve(input.cwd, options.resultDir) : join(artifactDir, 'test-results');
1119
+ const runDir = join(resultDir, runId);
1120
+ const summaryPath = join(runDir, 'summary.json');
1121
+ const reportDir = definition.output.reportDir ? external_node_path_resolve(input.projectRoot, definition.output.reportDir) : join(artifactDir, 'report');
1122
+ mkdirSync(resultDir, {
1123
+ recursive: true
1124
+ });
1125
+ mkdirSync(runDir);
1126
+ const projects = [];
1127
+ for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
1128
+ ...input.singleFile ? {
1129
+ files: [
1130
+ input.singleFile
1131
+ ]
1132
+ } : {},
1133
+ excludedDirectories: [
1134
+ artifactDir,
1135
+ resultDir,
1136
+ reportDir,
1137
+ ...[
1138
+ 'cache',
1139
+ 'log',
1140
+ 'output',
1141
+ 'tmp',
1142
+ 'dump'
1143
+ ].map((name)=>join(artifactDir, name))
1144
+ ]
1145
+ }));
1146
+ const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
1147
+ return {
1148
+ startedAt,
1149
+ runId,
1150
+ projectRoot: input.projectRoot,
1151
+ artifactDir,
1152
+ ...configPath ? {
1153
+ configPath
1154
+ } : {},
1155
+ resultDir,
1156
+ runDir,
1157
+ summaryPath,
1158
+ reportDir,
1159
+ definition,
1160
+ projects,
1161
+ preflightScope: hasFormatMismatch ? 'run' : 'project',
1162
+ reportEnabled: true,
1163
+ publications: [],
1164
+ onProgress: options.onProgress ?? (()=>{}),
1165
+ onEvent: options.onEvent
1166
+ };
1111
1167
  });
1112
- mkdirSync(runDir);
1113
- const projects = [];
1114
- for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
1115
- files: [
1116
- input.singleFile
1117
- ]
1118
- } : void 0));
1119
- const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
1120
- return {
1121
- startedAt,
1122
- runId,
1123
- projectRoot: input.projectRoot,
1124
- ...configPath ? {
1125
- configPath
1126
- } : {},
1127
- resultDir,
1128
- runDir,
1129
- summaryPath,
1130
- reportDir,
1131
- definition,
1132
- projects,
1133
- preflightScope: hasFormatMismatch ? 'run' : 'project',
1134
- reportEnabled: true,
1135
- publications: [],
1136
- onProgress: options.onProgress ?? (()=>{}),
1137
- onEvent: options.onEvent
1138
- };
1168
+ }
1169
+ function renderTestBadge(summary) {
1170
+ return `![Midscene Test: ${summary.passed} passed, ${summary.failed} failed](https://badge.midscenejs.com/e2e?pass=${summary.passed}&fail=${summary.failed}&style=large)`;
1139
1171
  }
1140
1172
  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, ' ');
1141
1173
  const duration = (ms)=>{
@@ -1149,6 +1181,8 @@ function renderTestResultMarkdown(dump, options = {}) {
1149
1181
  const lines = [
1150
1182
  '## Midscene Test',
1151
1183
  '',
1184
+ renderTestBadge(summary),
1185
+ '',
1152
1186
  `${outcome} · ${duration(dump.durationMs)}`,
1153
1187
  ''
1154
1188
  ];
@@ -1881,7 +1915,7 @@ const latestById = (items, id)=>[
1881
1915
  const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
1882
1916
  async function runPreparedTestProject(plan) {
1883
1917
  const reports = new ReportSession(join(plan.runDir, 'records'));
1884
- return reports.run(()=>runWithReportSession(plan, reports));
1918
+ return runWithMidsceneRunDir(plan.artifactDir, ()=>reports.run(()=>runWithReportSession(plan, reports)));
1885
1919
  }
1886
1920
  async function runWithReportSession(plan, reports) {
1887
1921
  const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;