@midscene/test 1.17.6 → 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,58 @@ 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
- };
1139
1168
  }
1140
1169
  function renderTestBadge(summary) {
1141
1170
  return `![Midscene Test: ${summary.passed} passed, ${summary.failed} failed](https://badge.midscenejs.com/e2e?pass=${summary.passed}&fail=${summary.failed}&style=large)`;
@@ -1886,7 +1915,7 @@ const latestById = (items, id)=>[
1886
1915
  const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
1887
1916
  async function runPreparedTestProject(plan) {
1888
1917
  const reports = new ReportSession(join(plan.runDir, 'records'));
1889
- return reports.run(()=>runWithReportSession(plan, reports));
1918
+ return runWithMidsceneRunDir(plan.artifactDir, ()=>reports.run(()=>runWithReportSession(plan, reports)));
1890
1919
  }
1891
1920
  async function runWithReportSession(plan, reports) {
1892
1921
  const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;