@midscene/testing-framework 0.0.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/es/builtin-steps.mjs +30 -0
  4. package/dist/es/builtin-steps.mjs.map +1 -0
  5. package/dist/es/config.mjs +83 -0
  6. package/dist/es/config.mjs.map +1 -0
  7. package/dist/es/dotenv.mjs +50 -0
  8. package/dist/es/dotenv.mjs.map +1 -0
  9. package/dist/es/index.mjs +5 -0
  10. package/dist/es/runner-worker.mjs +86 -0
  11. package/dist/es/runner-worker.mjs.map +1 -0
  12. package/dist/es/runner.mjs +165 -0
  13. package/dist/es/runner.mjs.map +1 -0
  14. package/dist/es/runtime/index.mjs +86 -0
  15. package/dist/es/runtime/index.mjs.map +1 -0
  16. package/dist/es/runtime/setup.mjs +59 -0
  17. package/dist/es/runtime/setup.mjs.map +1 -0
  18. package/dist/es/runtime/source.mjs +27 -0
  19. package/dist/es/runtime/source.mjs.map +1 -0
  20. package/dist/es/runtime/yaml.mjs +88 -0
  21. package/dist/es/runtime/yaml.mjs.map +1 -0
  22. package/dist/es/types.mjs +0 -0
  23. package/dist/lib/builtin-steps.js +67 -0
  24. package/dist/lib/builtin-steps.js.map +1 -0
  25. package/dist/lib/config.js +126 -0
  26. package/dist/lib/config.js.map +1 -0
  27. package/dist/lib/dotenv.js +97 -0
  28. package/dist/lib/dotenv.js.map +1 -0
  29. package/dist/lib/index.js +65 -0
  30. package/dist/lib/index.js.map +1 -0
  31. package/dist/lib/runner-worker.js +92 -0
  32. package/dist/lib/runner-worker.js.map +1 -0
  33. package/dist/lib/runner.js +199 -0
  34. package/dist/lib/runner.js.map +1 -0
  35. package/dist/lib/runtime/index.js +141 -0
  36. package/dist/lib/runtime/index.js.map +1 -0
  37. package/dist/lib/runtime/setup.js +96 -0
  38. package/dist/lib/runtime/setup.js.map +1 -0
  39. package/dist/lib/runtime/source.js +61 -0
  40. package/dist/lib/runtime/source.js.map +1 -0
  41. package/dist/lib/runtime/yaml.js +138 -0
  42. package/dist/lib/runtime/yaml.js.map +1 -0
  43. package/dist/lib/types.js +20 -0
  44. package/dist/lib/types.js.map +1 -0
  45. package/dist/types/builtin-steps.d.ts +11 -0
  46. package/dist/types/config.d.ts +18 -0
  47. package/dist/types/dotenv.d.ts +28 -0
  48. package/dist/types/index.d.ts +5 -0
  49. package/dist/types/runner-worker.d.ts +1 -0
  50. package/dist/types/runner.d.ts +33 -0
  51. package/dist/types/runtime/index.d.ts +24 -0
  52. package/dist/types/runtime/setup.d.ts +14 -0
  53. package/dist/types/runtime/source.d.ts +21 -0
  54. package/dist/types/runtime/yaml.d.ts +30 -0
  55. package/dist/types/types.d.ts +105 -0
  56. package/package.json +76 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Bytedance, Inc. and its affiliates.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @midscene/testing-framework
2
+
3
+ Runtime for Midscene's AI-native UI Testing Framework. It turns a
4
+ `midscene.config.ts` plus natural-language YAML cases into an Rstest run.
5
+
6
+ See the [UI Testing Framework](https://midscenejs.com/ui-testing-framework)
7
+ guide for the full topic.
8
+
9
+ ## Usage
10
+
11
+ Define a project config:
12
+
13
+ ```ts
14
+ // midscene.config.ts
15
+ import { defineMidsceneConfig } from '@midscene/testing-framework';
16
+
17
+ export default defineMidsceneConfig({
18
+ target: { type: 'web', options: { url: 'http://127.0.0.1:3000' } },
19
+ testDir: './e2e',
20
+ include: ['**/*.yaml'],
21
+ testRunner: { maxConcurrency: 1, bail: 0, testTimeout: 120_000 },
22
+ output: { summary: './midscene_run/output/summary.json' },
23
+ agentOptions: { cache: true },
24
+ });
25
+ ```
26
+
27
+ Run the suite from a thin entry:
28
+
29
+ ```ts
30
+ // run-suite.ts
31
+ import { runMidsceneSuite } from '@midscene/testing-framework';
32
+
33
+ await runMidsceneSuite();
34
+ ```
35
+
36
+ ## What it provides
37
+
38
+ - `defineMidsceneConfig` — type-only config helper that returns the config
39
+ unchanged.
40
+ - `runMidsceneSuite` — loads `midscene.config.ts`, discovers cases, and runs
41
+ them through Rstest.
42
+ - Default `web` (PlaywrightAgent) and `android` (`agentFromAdbDevice`) targets,
43
+ or a fully custom `setup`.
44
+ - Custom `yamlSteps` that interleave with built-in Midscene YAML steps.
45
+
46
+ `@rstest/core` is a peer dependency; install it in your test project (or in the
47
+ project emitted by `midscene emit`).
@@ -0,0 +1,30 @@
1
+ const BUILTIN_YAML_STEP_NAMES = new Set([
2
+ 'ai',
3
+ 'aiAct',
4
+ 'aiAction',
5
+ 'aiAssert',
6
+ 'aiQuery',
7
+ 'aiBoolean',
8
+ 'aiNumber',
9
+ 'aiString',
10
+ 'aiLocate',
11
+ 'aiInput',
12
+ 'aiTap',
13
+ 'aiRightClick',
14
+ 'aiDoubleClick',
15
+ 'aiHover',
16
+ 'aiScroll',
17
+ 'aiKeyboardPress',
18
+ 'aiWaitFor',
19
+ 'sleep',
20
+ "javascript",
21
+ 'logScreenshot',
22
+ 'launch',
23
+ 'terminate',
24
+ 'runAdbShell',
25
+ 'runWdaRequest'
26
+ ]);
27
+ const isBuiltinYamlStep = (stepName)=>BUILTIN_YAML_STEP_NAMES.has(stepName);
28
+ export { BUILTIN_YAML_STEP_NAMES, isBuiltinYamlStep };
29
+
30
+ //# sourceMappingURL=builtin-steps.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin-steps.mjs","sources":["../../src/builtin-steps.ts"],"sourcesContent":["/**\n * Built-in Midscene YAML step keys. When a `flow` step's action key matches one\n * of these, the framework hands the step to the existing `agent.runYaml`\n * runner. Custom `yamlSteps` are not allowed to reuse these names.\n *\n * The set covers the keys documented for #2509 plus the action keys that the\n * example cases actually use (e.g. `aiWaitFor`, `value`-bearing steps). Extend\n * it together with a test when a missing built-in key surfaces.\n */\nexport const BUILTIN_YAML_STEP_NAMES: ReadonlySet<string> = new Set([\n 'ai',\n 'aiAct',\n 'aiAction',\n 'aiAssert',\n 'aiQuery',\n 'aiBoolean',\n 'aiNumber',\n 'aiString',\n 'aiLocate',\n 'aiInput',\n 'aiTap',\n 'aiRightClick',\n 'aiDoubleClick',\n 'aiHover',\n 'aiScroll',\n 'aiKeyboardPress',\n 'aiWaitFor',\n 'sleep',\n 'javascript',\n 'logScreenshot',\n 'launch',\n 'terminate',\n 'runAdbShell',\n 'runWdaRequest',\n]);\n\nexport const isBuiltinYamlStep = (stepName: string): boolean =>\n BUILTIN_YAML_STEP_NAMES.has(stepName);\n"],"names":["BUILTIN_YAML_STEP_NAMES","Set","isBuiltinYamlStep","stepName"],"mappings":"AASO,MAAMA,0BAA+C,IAAIC,IAAI;IAClE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAEM,MAAMC,oBAAoB,CAACC,WAChCH,wBAAwB,GAAG,CAACG"}
@@ -0,0 +1,83 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { glob } from "glob";
4
+ import { createJiti } from "jiti";
5
+ import { BUILTIN_YAML_STEP_NAMES } from "./builtin-steps.mjs";
6
+ function defineMidsceneConfig(config) {
7
+ return config;
8
+ }
9
+ const DEFAULT_CONFIG_BASENAMES = [
10
+ 'midscene.config.ts',
11
+ 'midscene.config.mts',
12
+ 'midscene.config.cts',
13
+ 'midscene.config.js',
14
+ 'midscene.config.mjs',
15
+ 'midscene.config.cjs'
16
+ ];
17
+ const resolveConfigPath = (configPath)=>{
18
+ if (configPath) return resolve(configPath);
19
+ const cwd = process.cwd();
20
+ const matched = DEFAULT_CONFIG_BASENAMES.map((name)=>resolve(cwd, name)).find((candidate)=>existsSync(candidate));
21
+ return matched || resolve(cwd, 'midscene.config.ts');
22
+ };
23
+ async function loadMidsceneConfig(configPath) {
24
+ const resolvedPath = resolveConfigPath(configPath);
25
+ if (!existsSync(resolvedPath)) throw new Error(`midscene config not found: ${resolvedPath}`);
26
+ const jiti = createJiti(resolvedPath, {
27
+ moduleCache: false
28
+ });
29
+ const config = await jiti.import(resolvedPath, {
30
+ default: true
31
+ });
32
+ if (!config || 'object' != typeof config) throw new Error(`midscene config must export a default object: ${resolvedPath}`);
33
+ validateMidsceneConfig(config, resolvedPath);
34
+ return {
35
+ path: resolvedPath,
36
+ root: dirname(resolvedPath),
37
+ config
38
+ };
39
+ }
40
+ function validateMidsceneConfig(config, source = 'midscene.config.ts') {
41
+ if (!config.testDir) throw new Error(`${source} must define "testDir"`);
42
+ if (!Array.isArray(config.include) || 0 === config.include.length) throw new Error(`${source} must define a non-empty "include" array`);
43
+ if (config.target && config.setup) throw new Error(`${source} cannot define both "target" and "setup"; pick one runtime target definition`);
44
+ if (config.yamlSteps) {
45
+ const overridden = Object.keys(config.yamlSteps).filter((name)=>BUILTIN_YAML_STEP_NAMES.has(name));
46
+ if (overridden.length > 0) throw new Error(`${source} custom yamlSteps cannot override built-in steps: ${overridden.join(', ')}`);
47
+ }
48
+ }
49
+ const toPosixPath = (value)=>value.split('\\').join('/');
50
+ const uniqueSorted = (files)=>Array.from(new Set(files)).sort((a, b)=>a.localeCompare(b));
51
+ const inferFileType = (filePath)=>filePath.endsWith('.yaml') || filePath.endsWith('.yml') ? 'yaml' : 'test';
52
+ async function collectFrameworkTestFiles(input) {
53
+ const testDir = input.config.testDir || './e2e';
54
+ const include = input.config.include && input.config.include.length > 0 ? input.config.include : [
55
+ '**/*.yaml',
56
+ '**/*.yml',
57
+ '**/*.test.ts'
58
+ ];
59
+ const exclude = input.config.exclude || [];
60
+ const cwd = resolve(input.root, testDir);
61
+ const files = [];
62
+ for (const pattern of include){
63
+ const matched = await glob(pattern, {
64
+ cwd,
65
+ absolute: true,
66
+ ignore: exclude,
67
+ nodir: true,
68
+ dot: true
69
+ });
70
+ files.push(...matched);
71
+ }
72
+ return uniqueSorted(files).map((filePath)=>{
73
+ const relativeToRoot = toPosixPath(filePath.startsWith(input.root) ? filePath.slice(input.root.length + 1) : filePath);
74
+ return {
75
+ filePath,
76
+ relativePath: relativeToRoot,
77
+ type: inferFileType(filePath)
78
+ };
79
+ });
80
+ }
81
+ export { collectFrameworkTestFiles, defineMidsceneConfig, loadMidsceneConfig, validateMidsceneConfig };
82
+
83
+ //# sourceMappingURL=config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.mjs","sources":["../../src/config.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { glob } from 'glob';\nimport { createJiti } from 'jiti';\nimport { BUILTIN_YAML_STEP_NAMES } from './builtin-steps';\nimport type {\n FrameworkTestFile,\n LoadedMidsceneConfig,\n MidsceneFrameworkConfig,\n} from './types';\n\n/**\n * Type-only helper. It returns the config unchanged so the runtime stays\n * explicit and the project keeps full control over its own configuration.\n */\nexport function defineMidsceneConfig<T extends MidsceneFrameworkConfig>(\n config: T,\n): T {\n return config;\n}\n\nconst DEFAULT_CONFIG_BASENAMES = [\n 'midscene.config.ts',\n 'midscene.config.mts',\n 'midscene.config.cts',\n 'midscene.config.js',\n 'midscene.config.mjs',\n 'midscene.config.cjs',\n];\n\nconst resolveConfigPath = (configPath?: string): string => {\n if (configPath) {\n return resolve(configPath);\n }\n\n const cwd = process.cwd();\n const matched = DEFAULT_CONFIG_BASENAMES.map((name) =>\n resolve(cwd, name),\n ).find((candidate) => existsSync(candidate));\n\n return matched || resolve(cwd, 'midscene.config.ts');\n};\n\nexport async function loadMidsceneConfig(\n configPath?: string,\n): Promise<LoadedMidsceneConfig> {\n const resolvedPath = resolveConfigPath(configPath);\n if (!existsSync(resolvedPath)) {\n throw new Error(`midscene config not found: ${resolvedPath}`);\n }\n\n const jiti = createJiti(resolvedPath, {\n moduleCache: false,\n });\n const config = await jiti.import<MidsceneFrameworkConfig | undefined>(\n resolvedPath,\n {\n default: true,\n },\n );\n if (!config || typeof config !== 'object') {\n throw new Error(\n `midscene config must export a default object: ${resolvedPath}`,\n );\n }\n\n validateMidsceneConfig(config, resolvedPath);\n\n return {\n path: resolvedPath,\n root: dirname(resolvedPath),\n config,\n };\n}\n\n/**\n * Validate the documented invariants of a `midscene.config.ts`:\n * - `testDir` and `include` are required.\n * - `target` and `setup` cannot be defined together (two runtime targets).\n * - custom `yamlSteps` must not override built-in step names.\n */\nexport function validateMidsceneConfig(\n config: MidsceneFrameworkConfig,\n source = 'midscene.config.ts',\n): void {\n if (!config.testDir) {\n throw new Error(`${source} must define \"testDir\"`);\n }\n\n if (!Array.isArray(config.include) || config.include.length === 0) {\n throw new Error(`${source} must define a non-empty \"include\" array`);\n }\n\n if (config.target && config.setup) {\n throw new Error(\n `${source} cannot define both \"target\" and \"setup\"; pick one runtime target definition`,\n );\n }\n\n if (config.yamlSteps) {\n const overridden = Object.keys(config.yamlSteps).filter((name) =>\n BUILTIN_YAML_STEP_NAMES.has(name),\n );\n if (overridden.length > 0) {\n throw new Error(\n `${source} custom yamlSteps cannot override built-in steps: ${overridden.join(', ')}`,\n );\n }\n }\n}\n\nconst toPosixPath = (value: string): string => value.split('\\\\').join('/');\n\nconst uniqueSorted = (files: string[]): string[] =>\n Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));\n\nconst inferFileType = (filePath: string): FrameworkTestFile['type'] =>\n filePath.endsWith('.yaml') || filePath.endsWith('.yml') ? 'yaml' : 'test';\n\nexport async function collectFrameworkTestFiles(input: {\n root: string;\n config: MidsceneFrameworkConfig;\n}): Promise<FrameworkTestFile[]> {\n const testDir = input.config.testDir || './e2e';\n const include =\n input.config.include && input.config.include.length > 0\n ? input.config.include\n : ['**/*.yaml', '**/*.yml', '**/*.test.ts'];\n const exclude = input.config.exclude || [];\n const cwd = resolve(input.root, testDir);\n\n const files: string[] = [];\n for (const pattern of include) {\n const matched = await glob(pattern, {\n cwd,\n absolute: true,\n ignore: exclude,\n nodir: true,\n dot: true,\n });\n files.push(...matched);\n }\n\n return uniqueSorted(files).map((filePath) => {\n const relativeToRoot = toPosixPath(\n filePath.startsWith(input.root)\n ? filePath.slice(input.root.length + 1)\n : filePath,\n );\n return {\n filePath,\n relativePath: relativeToRoot,\n type: inferFileType(filePath),\n };\n });\n}\n"],"names":["defineMidsceneConfig","config","DEFAULT_CONFIG_BASENAMES","resolveConfigPath","configPath","resolve","cwd","process","matched","name","candidate","existsSync","loadMidsceneConfig","resolvedPath","Error","jiti","createJiti","validateMidsceneConfig","dirname","source","Array","overridden","Object","BUILTIN_YAML_STEP_NAMES","toPosixPath","value","uniqueSorted","files","Set","a","b","inferFileType","filePath","collectFrameworkTestFiles","input","testDir","include","exclude","pattern","glob","relativeToRoot"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import dotenv from "dotenv";
4
+ const toAbsolutePath = (cwd, candidate)=>resolve(cwd, candidate);
5
+ const dedupePaths = (paths)=>{
6
+ const seen = new Set();
7
+ const result = [];
8
+ for (const path of paths)if (!seen.has(path)) {
9
+ seen.add(path);
10
+ result.push(path);
11
+ }
12
+ return result;
13
+ };
14
+ function resolveDotenvCandidates(input) {
15
+ const { cwd, configDir, envConfig } = input;
16
+ if (envConfig?.path) {
17
+ const list = Array.isArray(envConfig.path) ? envConfig.path : [
18
+ envConfig.path
19
+ ];
20
+ return dedupePaths(list.map((entry)=>toAbsolutePath(cwd, entry)));
21
+ }
22
+ return dedupePaths([
23
+ toAbsolutePath(cwd, '.env'),
24
+ toAbsolutePath(configDir, '.env')
25
+ ]);
26
+ }
27
+ function loadFrameworkDotenv(input) {
28
+ if (input.envConfig?.enabled === false) return [];
29
+ const candidates = resolveDotenvCandidates(input);
30
+ const override = input.envConfig?.override === true;
31
+ const debug = input.envConfig?.debug === true;
32
+ return candidates.map((path)=>{
33
+ if (!existsSync(path)) return {
34
+ path,
35
+ loaded: false
36
+ };
37
+ dotenv.config({
38
+ path,
39
+ override,
40
+ debug
41
+ });
42
+ return {
43
+ path,
44
+ loaded: true
45
+ };
46
+ });
47
+ }
48
+ export { loadFrameworkDotenv, resolveDotenvCandidates };
49
+
50
+ //# sourceMappingURL=dotenv.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dotenv.mjs","sources":["../../src/dotenv.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport dotenv from 'dotenv';\nimport type { MidsceneFrameworkConfig } from './types';\n\nexport interface LoadedDotenvFile {\n path: string;\n loaded: boolean;\n}\n\nconst toAbsolutePath = (cwd: string, candidate: string): string =>\n resolve(cwd, candidate);\n\nconst dedupePaths = (paths: string[]): string[] => {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const path of paths) {\n if (!seen.has(path)) {\n seen.add(path);\n result.push(path);\n }\n }\n return result;\n};\n\n/**\n * Resolve the ordered list of `.env` files to consider for a suite run.\n * Defaults to `[<cwd>/.env, <configDir>/.env]` so a project can keep its env\n * either next to the config or at the working directory it was invoked from.\n * Explicit `env.path` overrides the default lookup entirely.\n */\nexport function resolveDotenvCandidates(input: {\n cwd: string;\n configDir: string;\n envConfig?: MidsceneFrameworkConfig['env'];\n}): string[] {\n const { cwd, configDir, envConfig } = input;\n\n if (envConfig?.path) {\n const list = Array.isArray(envConfig.path)\n ? envConfig.path\n : [envConfig.path];\n return dedupePaths(list.map((entry) => toAbsolutePath(cwd, entry)));\n }\n\n return dedupePaths([\n toAbsolutePath(cwd, '.env'),\n toAbsolutePath(configDir, '.env'),\n ]);\n}\n\n/**\n * Load `.env` files for a suite run. Mirrors `@midscene/cli` semantics:\n * existing `process.env` values are preserved unless `override` is set, and\n * missing files are skipped silently so a project without a `.env` keeps\n * working. Returns the considered files so the caller can log what was\n * actually applied.\n */\nexport function loadFrameworkDotenv(input: {\n cwd: string;\n configDir: string;\n envConfig?: MidsceneFrameworkConfig['env'];\n}): LoadedDotenvFile[] {\n if (input.envConfig?.enabled === false) {\n return [];\n }\n\n const candidates = resolveDotenvCandidates(input);\n const override = input.envConfig?.override === true;\n const debug = input.envConfig?.debug === true;\n\n return candidates.map((path) => {\n if (!existsSync(path)) {\n return { path, loaded: false };\n }\n dotenv.config({ path, override, debug });\n return { path, loaded: true };\n });\n}\n"],"names":["toAbsolutePath","cwd","candidate","resolve","dedupePaths","paths","seen","Set","result","path","resolveDotenvCandidates","input","configDir","envConfig","list","Array","entry","loadFrameworkDotenv","candidates","override","debug","existsSync","dotenv"],"mappings":";;;AAUA,MAAMA,iBAAiB,CAACC,KAAaC,YACnCC,QAAQF,KAAKC;AAEf,MAAME,cAAc,CAACC;IACnB,MAAMC,OAAO,IAAIC;IACjB,MAAMC,SAAmB,EAAE;IAC3B,KAAK,MAAMC,QAAQJ,MACjB,IAAI,CAACC,KAAK,GAAG,CAACG,OAAO;QACnBH,KAAK,GAAG,CAACG;QACTD,OAAO,IAAI,CAACC;IACd;IAEF,OAAOD;AACT;AAQO,SAASE,wBAAwBC,KAIvC;IACC,MAAM,EAAEV,GAAG,EAAEW,SAAS,EAAEC,SAAS,EAAE,GAAGF;IAEtC,IAAIE,WAAW,MAAM;QACnB,MAAMC,OAAOC,MAAM,OAAO,CAACF,UAAU,IAAI,IACrCA,UAAU,IAAI,GACd;YAACA,UAAU,IAAI;SAAC;QACpB,OAAOT,YAAYU,KAAK,GAAG,CAAC,CAACE,QAAUhB,eAAeC,KAAKe;IAC7D;IAEA,OAAOZ,YAAY;QACjBJ,eAAeC,KAAK;QACpBD,eAAeY,WAAW;KAC3B;AACH;AASO,SAASK,oBAAoBN,KAInC;IACC,IAAIA,MAAM,SAAS,EAAE,YAAY,OAC/B,OAAO,EAAE;IAGX,MAAMO,aAAaR,wBAAwBC;IAC3C,MAAMQ,WAAWR,MAAM,SAAS,EAAE,aAAa;IAC/C,MAAMS,QAAQT,MAAM,SAAS,EAAE,UAAU;IAEzC,OAAOO,WAAW,GAAG,CAAC,CAACT;QACrB,IAAI,CAACY,WAAWZ,OACd,OAAO;YAAEA;YAAM,QAAQ;QAAM;QAE/Ba,OAAO,MAAM,CAAC;YAAEb;YAAMU;YAAUC;QAAM;QACtC,OAAO;YAAEX;YAAM,QAAQ;QAAK;IAC9B;AACF"}
@@ -0,0 +1,5 @@
1
+ import { collectFrameworkTestFiles, defineMidsceneConfig, loadMidsceneConfig, validateMidsceneConfig } from "./config.mjs";
2
+ import { BUILTIN_YAML_STEP_NAMES, isBuiltinYamlStep } from "./builtin-steps.mjs";
3
+ import { loadFrameworkDotenv, resolveDotenvCandidates } from "./dotenv.mjs";
4
+ import { runMidsceneSuite } from "./runner.mjs";
5
+ export { BUILTIN_YAML_STEP_NAMES, collectFrameworkTestFiles, defineMidsceneConfig, isBuiltinYamlStep, loadFrameworkDotenv, loadMidsceneConfig, resolveDotenvCandidates, runMidsceneSuite, validateMidsceneConfig };
@@ -0,0 +1,86 @@
1
+ import { createRequire } from "node:module";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ const readStdin = ()=>new Promise((resolveStdin, reject)=>{
5
+ let data = '';
6
+ process.stdin.setEncoding('utf8');
7
+ process.stdin.on('data', (chunk)=>{
8
+ data += chunk;
9
+ });
10
+ process.stdin.on('end', ()=>resolveStdin(data));
11
+ process.stdin.on('error', reject);
12
+ });
13
+ const writeOutput = (output)=>{
14
+ process.stdout.write(`__MIDSCENE_RUNNER_WORKER_RESULT__${JSON.stringify(output)}\n`);
15
+ };
16
+ const main = async ()=>{
17
+ const raw = await readStdin();
18
+ const input = JSON.parse(raw);
19
+ const projectRequire = createRequire(resolve(input.root, 'package.json'));
20
+ const rstestPkgJson = projectRequire.resolve('@rstest/core/package.json');
21
+ const rsbuildEntry = createRequire(rstestPkgJson).resolve('@rsbuild/core');
22
+ const [{ runRstest }, { rspack }] = await Promise.all([
23
+ import("@rstest/core/api"),
24
+ import(pathToFileURL(rsbuildEntry).href)
25
+ ]);
26
+ const maxConcurrency = void 0 !== input.maxConcurrency ? Math.max(1, input.maxConcurrency) : void 0;
27
+ const inlineConfig = {
28
+ root: input.root,
29
+ include: input.include,
30
+ testEnvironment: 'node',
31
+ reporters: [],
32
+ ...void 0 !== input.testTimeout ? {
33
+ testTimeout: input.testTimeout
34
+ } : {},
35
+ ...void 0 !== maxConcurrency ? {
36
+ maxConcurrency
37
+ } : {},
38
+ ...void 0 !== maxConcurrency ? {
39
+ pool: {
40
+ maxWorkers: maxConcurrency,
41
+ minWorkers: maxConcurrency
42
+ }
43
+ } : {},
44
+ ...void 0 !== input.bail ? {
45
+ bail: input.bail
46
+ } : {},
47
+ ...void 0 !== input.retry ? {
48
+ retry: input.retry
49
+ } : {},
50
+ tools: {
51
+ rspack: (_config, { appendPlugins })=>{
52
+ appendPlugins(new rspack.experiments.VirtualModulesPlugin(input.virtualModules));
53
+ }
54
+ }
55
+ };
56
+ const result = await runRstest({
57
+ cwd: input.cwd,
58
+ inlineConfig
59
+ });
60
+ const unhandled = result?.unhandledErrors ?? [];
61
+ writeOutput({
62
+ ok: Boolean(result?.ok),
63
+ unhandledErrors: unhandled.map((error)=>({
64
+ name: error.name,
65
+ message: error.message,
66
+ stack: error.stack
67
+ }))
68
+ });
69
+ };
70
+ main().catch((error)=>{
71
+ const message = error instanceof Error ? error.stack || error.message : String(error);
72
+ process.stderr.write(`runner-worker fatal: ${message}\n`);
73
+ writeOutput({
74
+ ok: false,
75
+ unhandledErrors: [
76
+ {
77
+ name: error instanceof Error ? error.name : 'WorkerError',
78
+ message: error instanceof Error ? error.message : String(error),
79
+ stack: error instanceof Error ? error.stack : void 0
80
+ }
81
+ ]
82
+ });
83
+ process.exit(1);
84
+ });
85
+
86
+ //# sourceMappingURL=runner-worker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner-worker.mjs","sources":["../../src/runner-worker.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\ninterface WorkerInput {\n cwd: string;\n root: string;\n include: string[];\n virtualModules: Record<string, string>;\n maxConcurrency?: number;\n testTimeout?: number;\n bail?: number;\n retry?: number;\n}\n\ninterface WorkerOutput {\n ok: boolean;\n unhandledErrors: Array<{\n name?: string;\n message?: string;\n stack?: string;\n }>;\n}\n\nconst readStdin = (): Promise<string> =>\n new Promise((resolveStdin, reject) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk) => {\n data += chunk;\n });\n process.stdin.on('end', () => resolveStdin(data));\n process.stdin.on('error', reject);\n });\n\nconst writeOutput = (output: WorkerOutput): void => {\n process.stdout.write(\n `__MIDSCENE_RUNNER_WORKER_RESULT__${JSON.stringify(output)}\\n`,\n );\n};\n\nconst main = async (): Promise<void> => {\n const raw = await readStdin();\n const input = JSON.parse(raw) as WorkerInput;\n\n const projectRequire = createRequire(resolve(input.root, 'package.json'));\n const rstestPkgJson = projectRequire.resolve('@rstest/core/package.json');\n const rsbuildEntry = createRequire(rstestPkgJson).resolve('@rsbuild/core');\n\n const [{ runRstest }, { rspack }] = await Promise.all([\n import('@rstest/core/api'),\n import(pathToFileURL(rsbuildEntry).href),\n ]);\n\n const maxConcurrency =\n input.maxConcurrency !== undefined\n ? Math.max(1, input.maxConcurrency)\n : undefined;\n\n const inlineConfig: Record<string, unknown> = {\n root: input.root,\n include: input.include,\n testEnvironment: 'node',\n reporters: [],\n ...(input.testTimeout !== undefined\n ? { testTimeout: input.testTimeout }\n : {}),\n ...(maxConcurrency !== undefined ? { maxConcurrency } : {}),\n ...(maxConcurrency !== undefined\n ? { pool: { maxWorkers: maxConcurrency, minWorkers: maxConcurrency } }\n : {}),\n ...(input.bail !== undefined ? { bail: input.bail } : {}),\n ...(input.retry !== undefined ? { retry: input.retry } : {}),\n tools: {\n rspack: (\n _config: unknown,\n { appendPlugins }: { appendPlugins: (plugin: unknown) => void },\n ) => {\n appendPlugins(\n new rspack.experiments.VirtualModulesPlugin(input.virtualModules),\n );\n },\n },\n };\n\n const result = await runRstest({ cwd: input.cwd, inlineConfig });\n\n type UnhandledError = { name?: string; message?: string; stack?: string };\n const unhandled =\n (result as { unhandledErrors?: UnhandledError[] } | undefined)\n ?.unhandledErrors ?? [];\n\n writeOutput({\n ok: Boolean(result?.ok),\n unhandledErrors: unhandled.map((error) => ({\n name: error.name,\n message: error.message,\n stack: error.stack,\n })),\n });\n};\n\nmain().catch((error) => {\n const message =\n error instanceof Error ? error.stack || error.message : String(error);\n process.stderr.write(`runner-worker fatal: ${message}\\n`);\n writeOutput({\n ok: false,\n unhandledErrors: [\n {\n name: error instanceof Error ? error.name : 'WorkerError',\n message: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n },\n ],\n });\n process.exit(1);\n});\n"],"names":["readStdin","Promise","resolveStdin","reject","data","process","chunk","writeOutput","output","JSON","main","raw","input","projectRequire","createRequire","resolve","rstestPkgJson","rsbuildEntry","runRstest","rspack","pathToFileURL","maxConcurrency","undefined","Math","inlineConfig","_config","appendPlugins","result","unhandled","Boolean","error","message","Error","String"],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ import { fork } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { collectFrameworkTestFiles, loadMidsceneConfig } from "./config.mjs";
6
+ import { loadFrameworkDotenv } from "./dotenv.mjs";
7
+ import { createYamlFrameworkSuiteSource } from "./runtime/source.mjs";
8
+ const RUNNER_WORKER_RESULT_PREFIX = '__MIDSCENE_RUNNER_WORKER_RESULT__';
9
+ const SUITE_MODULE_ID = 'virtual:midscene-framework/suite.test.ts';
10
+ const safeStem = (relativePath, index)=>{
11
+ const base = relativePath.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
12
+ return `${String(index + 1).padStart(3, '0')}-${base || 'case'}`;
13
+ };
14
+ const resolveWorkerEntry = ()=>{
15
+ const moduleUrl = 'string' == typeof __filename ? null : import.meta?.url;
16
+ const here = moduleUrl ? fileURLToPath(moduleUrl) : __filename;
17
+ const ext = here.endsWith('.mjs') ? '.mjs' : '.js';
18
+ return resolve(dirname(here), `runner-worker${ext}`);
19
+ };
20
+ const defaultRstestRunner = async (project)=>{
21
+ const workerEntry = resolveWorkerEntry();
22
+ const input = {
23
+ cwd: project.root,
24
+ root: project.root,
25
+ include: project.include,
26
+ virtualModules: project.virtualModules,
27
+ maxConcurrency: project.maxConcurrency,
28
+ testTimeout: project.testTimeout,
29
+ bail: project.bail,
30
+ retry: project.retry
31
+ };
32
+ return await new Promise((resolveRunner, rejectRunner)=>{
33
+ const child = fork(workerEntry, [], {
34
+ cwd: project.root,
35
+ env: process.env,
36
+ stdio: [
37
+ 'pipe',
38
+ 'pipe',
39
+ 'inherit',
40
+ 'ipc'
41
+ ]
42
+ });
43
+ let stdoutBuffer = '';
44
+ let parsedResult;
45
+ child.stdout?.setEncoding('utf8');
46
+ child.stdout?.on('data', (chunk)=>{
47
+ stdoutBuffer += chunk;
48
+ let newlineIndex = stdoutBuffer.indexOf('\n');
49
+ while(-1 !== newlineIndex){
50
+ const line = stdoutBuffer.slice(0, newlineIndex);
51
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
52
+ if (line.startsWith(RUNNER_WORKER_RESULT_PREFIX)) try {
53
+ parsedResult = JSON.parse(line.slice(RUNNER_WORKER_RESULT_PREFIX.length));
54
+ } catch (error) {
55
+ rejectRunner(new Error(`Failed to parse runner-worker result: ${error instanceof Error ? error.message : String(error)}`));
56
+ return;
57
+ }
58
+ else if (line.length > 0) process.stdout.write(`${line}\n`);
59
+ newlineIndex = stdoutBuffer.indexOf('\n');
60
+ }
61
+ });
62
+ child.on('error', (error)=>{
63
+ rejectRunner(error);
64
+ });
65
+ child.on('exit', (code, signal)=>{
66
+ if (stdoutBuffer.length > 0 && !stdoutBuffer.startsWith(RUNNER_WORKER_RESULT_PREFIX)) process.stdout.write(stdoutBuffer);
67
+ if (!parsedResult) return void rejectRunner(new Error(`runner-worker exited (code=${code ?? 'null'}, signal=${signal ?? 'null'}) without producing a result`));
68
+ if (parsedResult.unhandledErrors.length > 0) for (const error of parsedResult.unhandledErrors)console.error(error.stack || `${error.name}: ${error.message}`);
69
+ resolveRunner({
70
+ ok: parsedResult.ok
71
+ });
72
+ });
73
+ child.stdin?.write(JSON.stringify(input));
74
+ child.stdin?.end();
75
+ });
76
+ };
77
+ const readCaseResult = (item)=>{
78
+ if (existsSync(item.resultFile)) return JSON.parse(readFileSync(item.resultFile, 'utf8'));
79
+ return {
80
+ file: item.filePath,
81
+ testName: item.testName,
82
+ success: false,
83
+ duration: 0,
84
+ error: 'Not executed'
85
+ };
86
+ };
87
+ const writeSummaryFile = (loaded, summary)=>{
88
+ const target = loaded.config.output?.summary;
89
+ if (!target) return;
90
+ const summaryPath = resolve(loaded.root, target);
91
+ mkdirSync(dirname(summaryPath), {
92
+ recursive: true
93
+ });
94
+ writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
95
+ return summaryPath;
96
+ };
97
+ const printSummary = (summary, summaryPath)=>{
98
+ console.log('\nšŸ“Š Midscene suite summary');
99
+ console.log(` Total: ${summary.total}`);
100
+ console.log(` Passed: ${summary.passed}`);
101
+ console.log(` Failed: ${summary.failed}`);
102
+ console.log(` Duration: ${(summary.durationMs / 1000).toFixed(2)}s`);
103
+ if (summaryPath) console.log(` Summary: ${summaryPath}`);
104
+ for (const result of summary.results)if (!result.success) console.log(` āŒ ${result.testName}: ${result.error ?? 'failed'}`);
105
+ };
106
+ async function runMidsceneSuite(options = {}) {
107
+ const loaded = await loadMidsceneConfig(options.configPath);
108
+ const dotenvFiles = loadFrameworkDotenv({
109
+ cwd: process.cwd(),
110
+ configDir: loaded.root,
111
+ envConfig: loaded.config.env
112
+ });
113
+ for (const entry of dotenvFiles)if (entry.loaded) console.log(` Env file: ${entry.path}`);
114
+ const files = await collectFrameworkTestFiles({
115
+ root: loaded.root,
116
+ config: loaded.config
117
+ });
118
+ if (0 === files.length) throw new Error(`No test files found in ${resolve(loaded.root, loaded.config.testDir || './e2e')}`);
119
+ const outputDir = options.outputDir || join(loaded.root, 'midscene_run', 'tmp', `framework-${Date.now()}`);
120
+ const resultDir = join(outputDir, 'results');
121
+ mkdirSync(resultDir, {
122
+ recursive: true
123
+ });
124
+ const yamlCases = files.filter((file)=>'yaml' === file.type).map((file, index)=>({
125
+ filePath: file.filePath,
126
+ testName: file.relativePath,
127
+ resultFile: join(resultDir, `${safeStem(file.relativePath, index)}.json`)
128
+ }));
129
+ const virtualModules = {};
130
+ const include = [];
131
+ if (yamlCases.length > 0) {
132
+ virtualModules[SUITE_MODULE_ID] = createYamlFrameworkSuiteSource({
133
+ configPath: loaded.path,
134
+ projectDir: loaded.root,
135
+ cases: yamlCases
136
+ });
137
+ include.push(SUITE_MODULE_ID);
138
+ }
139
+ for (const file of files)if ('test' === file.type) include.push(file.filePath);
140
+ const runner = options.rstestRunner || defaultRstestRunner;
141
+ const runResult = await runner({
142
+ root: loaded.root,
143
+ include,
144
+ virtualModules,
145
+ maxConcurrency: loaded.config.testRunner?.maxConcurrency,
146
+ testTimeout: loaded.config.testRunner?.testTimeout,
147
+ bail: loaded.config.testRunner?.bail,
148
+ retry: loaded.config.testRunner?.retry
149
+ });
150
+ const results = yamlCases.map(readCaseResult);
151
+ const summary = {
152
+ total: results.length,
153
+ passed: results.filter((result)=>result.success).length,
154
+ failed: results.filter((result)=>!result.success).length,
155
+ durationMs: results.reduce((sum, result)=>sum + (result.duration || 0), 0),
156
+ results
157
+ };
158
+ const summaryPath = writeSummaryFile(loaded, summary);
159
+ printSummary(summary, summaryPath);
160
+ if (!runResult.ok || summary.failed > 0) process.exitCode = 1;
161
+ return summary;
162
+ }
163
+ export { runMidsceneSuite };
164
+
165
+ //# sourceMappingURL=runner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.mjs","sources":["../../src/runner.ts"],"sourcesContent":["import { fork } from 'node:child_process';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { collectFrameworkTestFiles, loadMidsceneConfig } from './config';\nimport { loadFrameworkDotenv } from './dotenv';\nimport { createYamlFrameworkSuiteSource } from './runtime/source';\nimport type {\n FrameworkCaseResult,\n FrameworkSuiteSummary,\n LoadedMidsceneConfig,\n} from './types';\n\nconst RUNNER_WORKER_RESULT_PREFIX = '__MIDSCENE_RUNNER_WORKER_RESULT__';\n\nconst SUITE_MODULE_ID = 'virtual:midscene-framework/suite.test.ts';\n\nexport interface FrameworkRstestProject {\n root: string;\n include: string[];\n virtualModules: Record<string, string>;\n maxConcurrency?: number;\n testTimeout?: number;\n bail?: number;\n retry?: number;\n}\n\nexport type FrameworkRstestRunner = (\n project: FrameworkRstestProject,\n) => Promise<{ ok: boolean }>;\n\nexport interface RunMidsceneSuiteOptions {\n /** Path to `midscene.config.ts`. Defaults to the one in `cwd`. */\n configPath?: string;\n /** Directory for generated wiring and per-case result files. */\n outputDir?: string;\n /** Override the Rstest runner. Mainly for tests. */\n rstestRunner?: FrameworkRstestRunner;\n stdio?: 'inherit' | 'pipe';\n}\n\ninterface SuiteCase {\n filePath: string;\n testName: string;\n resultFile: string;\n}\n\nconst safeStem = (relativePath: string, index: number): string => {\n const base = relativePath\n .replace(/\\.[^.]+$/, '')\n .replace(/[^a-zA-Z0-9._-]+/g, '-')\n .replace(/^-+|-+$/g, '');\n return `${String(index + 1).padStart(3, '0')}-${base || 'case'}`;\n};\n\ninterface WorkerInput {\n cwd: string;\n root: string;\n include: string[];\n virtualModules: Record<string, string>;\n maxConcurrency?: number;\n testTimeout?: number;\n bail?: number;\n retry?: number;\n}\n\ninterface WorkerOutput {\n ok: boolean;\n unhandledErrors: Array<{\n name?: string;\n message?: string;\n stack?: string;\n }>;\n}\n\nconst resolveWorkerEntry = (): string => {\n // `runner.js` and `runner-worker.js` ship side-by-side in both the CJS and\n // ESM bundles, so resolving relative to the current module works in both\n // formats. Use `__filename` for CJS and `import.meta.url` for ESM. The ESM\n // build emits `.mjs` files; pick the matching extension.\n const moduleUrl =\n typeof __filename === 'string'\n ? null\n : (import.meta as { url?: string } | undefined)?.url;\n const here = moduleUrl ? fileURLToPath(moduleUrl) : __filename;\n const ext = here.endsWith('.mjs') ? '.mjs' : '.js';\n return resolve(dirname(here), `runner-worker${ext}`);\n};\n\n/**\n * Default Rstest driver. We deliberately spawn `runRstest` in a child process:\n * the user's `midscene.config.ts` typically imports playwright (or\n * `@midscene/web/playwright`), which transitively initializes a bundled\n * `@vitest/expect` copy and defines `Symbol.for('$$jest-matchers-object')` on\n * `globalThis` non-configurably. Calling `runRstest` in the same process then\n * fails with `TypeError: Cannot redefine property: Symbol($$jest-matchers-object)`\n * because Rstest's own `@vitest/expect` copy tries to redefine the same global.\n * Forking sidesteps the collision: the child has never imported playwright.\n */\nconst defaultRstestRunner: FrameworkRstestRunner = async (project) => {\n const workerEntry = resolveWorkerEntry();\n\n const input: WorkerInput = {\n cwd: project.root,\n root: project.root,\n include: project.include,\n virtualModules: project.virtualModules,\n maxConcurrency: project.maxConcurrency,\n testTimeout: project.testTimeout,\n bail: project.bail,\n retry: project.retry,\n };\n\n return await new Promise<{ ok: boolean }>((resolveRunner, rejectRunner) => {\n const child = fork(workerEntry, [], {\n cwd: project.root,\n env: process.env,\n stdio: ['pipe', 'pipe', 'inherit', 'ipc'],\n });\n\n let stdoutBuffer = '';\n let parsedResult: WorkerOutput | undefined;\n\n child.stdout?.setEncoding('utf8');\n child.stdout?.on('data', (chunk: string) => {\n stdoutBuffer += chunk;\n let newlineIndex = stdoutBuffer.indexOf('\\n');\n while (newlineIndex !== -1) {\n const line = stdoutBuffer.slice(0, newlineIndex);\n stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);\n if (line.startsWith(RUNNER_WORKER_RESULT_PREFIX)) {\n try {\n parsedResult = JSON.parse(\n line.slice(RUNNER_WORKER_RESULT_PREFIX.length),\n ) as WorkerOutput;\n } catch (error) {\n rejectRunner(\n new Error(\n `Failed to parse runner-worker result: ${\n error instanceof Error ? error.message : String(error)\n }`,\n ),\n );\n return;\n }\n } else if (line.length > 0) {\n process.stdout.write(`${line}\\n`);\n }\n newlineIndex = stdoutBuffer.indexOf('\\n');\n }\n });\n\n child.on('error', (error) => {\n rejectRunner(error);\n });\n\n child.on('exit', (code, signal) => {\n if (\n stdoutBuffer.length > 0 &&\n !stdoutBuffer.startsWith(RUNNER_WORKER_RESULT_PREFIX)\n ) {\n process.stdout.write(stdoutBuffer);\n }\n\n if (!parsedResult) {\n rejectRunner(\n new Error(\n `runner-worker exited (code=${code ?? 'null'}, signal=${\n signal ?? 'null'\n }) without producing a result`,\n ),\n );\n return;\n }\n\n if (parsedResult.unhandledErrors.length > 0) {\n for (const error of parsedResult.unhandledErrors) {\n console.error(error.stack || `${error.name}: ${error.message}`);\n }\n }\n\n resolveRunner({ ok: parsedResult.ok });\n });\n\n child.stdin?.write(JSON.stringify(input));\n child.stdin?.end();\n });\n};\n\nconst readCaseResult = (item: SuiteCase): FrameworkCaseResult => {\n if (existsSync(item.resultFile)) {\n return JSON.parse(\n readFileSync(item.resultFile, 'utf8'),\n ) as FrameworkCaseResult;\n }\n\n return {\n file: item.filePath,\n testName: item.testName,\n success: false,\n duration: 0,\n error: 'Not executed',\n };\n};\n\nconst writeSummaryFile = (\n loaded: LoadedMidsceneConfig,\n summary: FrameworkSuiteSummary,\n): string | undefined => {\n const target = loaded.config.output?.summary;\n if (!target) {\n return undefined;\n }\n const summaryPath = resolve(loaded.root, target);\n mkdirSync(dirname(summaryPath), { recursive: true });\n writeFileSync(summaryPath, JSON.stringify(summary, null, 2));\n return summaryPath;\n};\n\nconst printSummary = (summary: FrameworkSuiteSummary, summaryPath?: string) => {\n console.log('\\nšŸ“Š Midscene suite summary');\n console.log(` Total: ${summary.total}`);\n console.log(` Passed: ${summary.passed}`);\n console.log(` Failed: ${summary.failed}`);\n console.log(` Duration: ${(summary.durationMs / 1000).toFixed(2)}s`);\n if (summaryPath) {\n console.log(` Summary: ${summaryPath}`);\n }\n for (const result of summary.results) {\n if (!result.success) {\n console.log(` āŒ ${result.testName}: ${result.error ?? 'failed'}`);\n }\n }\n};\n\n/**\n * Load `midscene.config.ts`, discover cases, run them through Rstest, and write\n * the suite summary. Intended to be the entire body of a project's\n * `run-suite.ts`:\n *\n * ```ts\n * import { runMidsceneSuite } from '@midscene/testing-framework';\n * await runMidsceneSuite();\n * ```\n */\nexport async function runMidsceneSuite(\n options: RunMidsceneSuiteOptions = {},\n): Promise<FrameworkSuiteSummary> {\n const loaded = await loadMidsceneConfig(options.configPath);\n\n const dotenvFiles = loadFrameworkDotenv({\n cwd: process.cwd(),\n configDir: loaded.root,\n envConfig: loaded.config.env,\n });\n for (const entry of dotenvFiles) {\n if (entry.loaded) {\n console.log(` Env file: ${entry.path}`);\n }\n }\n\n const files = await collectFrameworkTestFiles({\n root: loaded.root,\n config: loaded.config,\n });\n\n if (files.length === 0) {\n throw new Error(\n `No test files found in ${resolve(loaded.root, loaded.config.testDir || './e2e')}`,\n );\n }\n\n const outputDir =\n options.outputDir ||\n join(loaded.root, 'midscene_run', 'tmp', `framework-${Date.now()}`);\n const resultDir = join(outputDir, 'results');\n mkdirSync(resultDir, { recursive: true });\n\n const yamlCases: SuiteCase[] = files\n .filter((file) => file.type === 'yaml')\n .map((file, index) => ({\n filePath: file.filePath,\n testName: file.relativePath,\n resultFile: join(resultDir, `${safeStem(file.relativePath, index)}.json`),\n }));\n\n const virtualModules: Record<string, string> = {};\n const include: string[] = [];\n\n if (yamlCases.length > 0) {\n virtualModules[SUITE_MODULE_ID] = createYamlFrameworkSuiteSource({\n configPath: loaded.path,\n projectDir: loaded.root,\n cases: yamlCases,\n });\n include.push(SUITE_MODULE_ID);\n }\n\n for (const file of files) {\n if (file.type === 'test') {\n include.push(file.filePath);\n }\n }\n\n const runner = options.rstestRunner || defaultRstestRunner;\n const runResult = await runner({\n root: loaded.root,\n include,\n virtualModules,\n maxConcurrency: loaded.config.testRunner?.maxConcurrency,\n testTimeout: loaded.config.testRunner?.testTimeout,\n bail: loaded.config.testRunner?.bail,\n retry: loaded.config.testRunner?.retry,\n });\n\n const results = yamlCases.map(readCaseResult);\n const summary: FrameworkSuiteSummary = {\n total: results.length,\n passed: results.filter((result) => result.success).length,\n failed: results.filter((result) => !result.success).length,\n durationMs: results.reduce(\n (sum, result) => sum + (result.duration || 0),\n 0,\n ),\n results,\n };\n\n const summaryPath = writeSummaryFile(loaded, summary);\n printSummary(summary, summaryPath);\n\n if (!runResult.ok || summary.failed > 0) {\n process.exitCode = 1;\n }\n\n return summary;\n}\n"],"names":["RUNNER_WORKER_RESULT_PREFIX","SUITE_MODULE_ID","safeStem","relativePath","index","base","String","resolveWorkerEntry","moduleUrl","__filename","here","fileURLToPath","ext","resolve","dirname","defaultRstestRunner","project","workerEntry","input","Promise","resolveRunner","rejectRunner","child","fork","process","stdoutBuffer","parsedResult","chunk","newlineIndex","line","JSON","error","Error","code","signal","console","readCaseResult","item","existsSync","readFileSync","writeSummaryFile","loaded","summary","target","summaryPath","mkdirSync","writeFileSync","printSummary","result","runMidsceneSuite","options","loadMidsceneConfig","dotenvFiles","loadFrameworkDotenv","entry","files","collectFrameworkTestFiles","outputDir","join","Date","resultDir","yamlCases","file","virtualModules","include","createYamlFrameworkSuiteSource","runner","runResult","results","sum"],"mappings":"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"}