playwright-test-explorer 0.0.12 → 0.0.13
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/README.md +17 -1
- package/dist/{api-Bbd6qGsY.mjs → api-CvJUOXye.mjs} +382 -208
- package/dist/cli.mjs +15 -5
- package/dist/index.d.mts +12 -1
- package/dist/index.mjs +2 -2
- package/dist/treeReporter.mjs +54 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,12 +36,15 @@ pw-test-explorer tree
|
|
|
36
36
|
|
|
37
37
|
# 保存结果到文件
|
|
38
38
|
pw-test-explorer tree -o output.json
|
|
39
|
+
|
|
40
|
+
# 根据 Git 变更增量更新已有树(缓存文件及其 state 文件需在构建间保留)
|
|
41
|
+
pw-test-explorer tree --incremental -o output.json
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
### API 使用
|
|
42
45
|
|
|
43
46
|
```typescript
|
|
44
|
-
import { listTestFile, listTestCase, treeStructure } from 'playwright-test-explorer';
|
|
47
|
+
import { listTestFile, listTestCase, treeStructure, updateTreeStructure } from 'playwright-test-explorer';
|
|
45
48
|
|
|
46
49
|
// 列出测试文件
|
|
47
50
|
await listTestFile('/path/to/project', 'playwright.config.ts');
|
|
@@ -51,8 +54,21 @@ await listTestCase('/path/to/project', 'playwright.config.ts', ['tests/example.s
|
|
|
51
54
|
|
|
52
55
|
// 生成测试树
|
|
53
56
|
await treeStructure('/path/to/project', 'playwright.config.ts');
|
|
57
|
+
|
|
58
|
+
// 根据缓存记录的 Git 提交之后的变更更新测试树;状态文件默认为 <output>.state.json。
|
|
59
|
+
await updateTreeStructure('/path/to/project', 'playwright.config.ts', '/path/to/project/test-tree.json');
|
|
54
60
|
```
|
|
55
61
|
|
|
62
|
+
`tree --incremental` 在没有有效缓存时全量构建测试树。后续运行根据缓存记录的 Git 提交之后的变更,重新收集发生变化的测试文件,并移除已删除或重命名文件的旧节点。CI 构建之间需要同时保留测试树输出文件及其配套的 `.state.json` 状态文件。配置或参与缓存校验的环境变量发生变化时,缓存失效并触发全量重建。
|
|
63
|
+
|
|
64
|
+
增量比较只考虑已提交的 Git 变更,假设工作区内容与记录的提交一致。工作区可以是 Git 仓库的子目录。非测试文件发生变化时,会保守地触发全量重建。对同一组输出文件和状态文件的更新应串行执行。
|
|
65
|
+
|
|
66
|
+
缓存默认跟踪 `CI`、`NODE_ENV`、`NODE_OPTIONS`、`TZ`,以及配置文件内容、工作区路径和 Node.js 版本。CI 任务 ID 等临时变量不会使缓存失效。如果配置或测试定义依赖其他环境变量,需要通过 `--cache-env DOC,AI` 声明变量名,或通过 API 的第五个参数传入 `{ environmentKeys: ['DOC', 'AI'] }`。这些项目自定义变量仅在显式声明后参与缓存校验。状态文件只保存计算后的哈希值,不保存环境变量的原始值。升级后,版本 1 的旧缓存会触发一次全量重建。
|
|
67
|
+
|
|
68
|
+
`tree` 和 `list-tests` 输出的报告文件路径均相对于 Playwright 的 `rootDir`,即使各项目配置了不同的 `testDir`,也使用这一基准。
|
|
69
|
+
|
|
70
|
+
全量构建通过 Playwright 的 `test --list` 和本库内置的 reporter 收集测试。测试文件在一个子进程中加载,仅将最终测试树传回主进程,省去 test server 模式下 loader 向 server 传输依赖缓存的开销。收集过程不会执行全局 setup、测试正文或用户配置的 reporter。增量收集继续使用 test server。
|
|
71
|
+
|
|
56
72
|
## 文档
|
|
57
73
|
|
|
58
74
|
- [功能规格说明](./SPEC.md) - 完整的功能需求和 API 文档
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { spawn } from "child_process";
|
|
3
|
+
import { execFile, fork, spawn } from "child_process";
|
|
4
4
|
import WebSocket from "ws";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
5
6
|
import fs from "fs/promises";
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
import { promisify } from "util";
|
|
6
9
|
//#region \0rolldown/runtime.js
|
|
7
10
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
8
11
|
//#endregion
|
|
@@ -645,14 +648,6 @@ function escapeRegex(text) {
|
|
|
645
648
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
646
649
|
}
|
|
647
650
|
/**
|
|
648
|
-
* Path resolution strategy for TeleReporterReceiver
|
|
649
|
-
* For file paths (with extensions), return relative path (Playwright format)
|
|
650
|
-
* For directory paths (testDir, outputDir, etc.), return absolute path
|
|
651
|
-
*/
|
|
652
|
-
function resolvePathForFiles(rootDir, relativePath) {
|
|
653
|
-
return /\.\w+$/.test(relativePath) ? relativePath : path.join(rootDir, relativePath);
|
|
654
|
-
}
|
|
655
|
-
/**
|
|
656
651
|
* Standard path resolution - always returns absolute path
|
|
657
652
|
*/
|
|
658
653
|
function resolveAbsolutePath(rootDir, relativePath) {
|
|
@@ -708,17 +703,6 @@ function createTestServerEnv(baseEnv, customEnv, cwd) {
|
|
|
708
703
|
//#endregion
|
|
709
704
|
//#region src/utils/reporterFactory.ts
|
|
710
705
|
/**
|
|
711
|
-
* Create TeleReporterReceiver for listing files
|
|
712
|
-
* Uses special path resolution for preserving relative paths for files
|
|
713
|
-
*/
|
|
714
|
-
function createFileListReceiver(reporter) {
|
|
715
|
-
return new TeleReporterReceiver(reporter, {
|
|
716
|
-
mergeProjects: true,
|
|
717
|
-
mergeTestCases: true,
|
|
718
|
-
resolvePath: resolvePathForFiles
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
706
|
* Create TeleReporterReceiver for listing tests
|
|
723
707
|
* Uses standard absolute path resolution
|
|
724
708
|
*/
|
|
@@ -773,11 +757,16 @@ var PlaywrightTestServer = class {
|
|
|
773
757
|
}
|
|
774
758
|
async listFiles() {
|
|
775
759
|
const connection = await this._ensureConnection();
|
|
776
|
-
const result = {
|
|
777
|
-
|
|
760
|
+
const result = {
|
|
761
|
+
rootDir: this._config.workspaceFolder,
|
|
762
|
+
projects: []
|
|
763
|
+
};
|
|
764
|
+
const { report, status } = await connection.listFiles({});
|
|
778
765
|
const workspaceFolder = this._config.workspaceFolder;
|
|
779
|
-
processReport(
|
|
780
|
-
onConfigure: (_config) => {
|
|
766
|
+
processReport(createTestListReceiver({
|
|
767
|
+
onConfigure: (_config) => {
|
|
768
|
+
result.rootDir = _config.rootDir;
|
|
769
|
+
},
|
|
781
770
|
onBegin(rootSuite) {
|
|
782
771
|
for (const projectSuite of rootSuite.suites) {
|
|
783
772
|
const project = projectSuite.project();
|
|
@@ -789,24 +778,31 @@ var PlaywrightTestServer = class {
|
|
|
789
778
|
use: project.use || {},
|
|
790
779
|
files
|
|
791
780
|
});
|
|
792
|
-
for (const fileSuite of projectSuite.suites) files.push(fileSuite.location.file);
|
|
781
|
+
for (const fileSuite of projectSuite.suites) files.push(path.relative(result.rootDir, fileSuite.location.file).replace(/\\/g, "/"));
|
|
793
782
|
}
|
|
794
783
|
},
|
|
795
784
|
onError(error) {
|
|
796
785
|
result.error = error;
|
|
797
786
|
}
|
|
798
787
|
}), report);
|
|
788
|
+
if (result.error) throw new Error(result.error.message || "Playwright file collection failed");
|
|
789
|
+
if (status !== "passed") throw new Error(`Playwright file collection ${status}`);
|
|
799
790
|
return result;
|
|
800
791
|
}
|
|
801
792
|
/**
|
|
802
793
|
* Get raw test report without processing through TeleReporterReceiver
|
|
803
794
|
* This preserves the complete test.describe hierarchy
|
|
804
795
|
*/
|
|
805
|
-
async getRawTestReport(locations, token) {
|
|
796
|
+
async getRawTestReport(locations, token, projects) {
|
|
806
797
|
if (token?.isCancellationRequested) return [];
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
798
|
+
const { report, status } = await (await this._ensureConnection()).listTests({
|
|
799
|
+
locations: locations?.map(escapeRegex),
|
|
800
|
+
projects
|
|
801
|
+
});
|
|
802
|
+
if (status !== "passed") {
|
|
803
|
+
const error = report.find((message) => message.method === "onError");
|
|
804
|
+
throw new Error(error?.params.error.message || `Playwright test collection ${status}`);
|
|
805
|
+
}
|
|
810
806
|
return report;
|
|
811
807
|
}
|
|
812
808
|
async listTests(locations, reporter, token) {
|
|
@@ -905,203 +901,381 @@ var PlaywrightTestServer = class {
|
|
|
905
901
|
}
|
|
906
902
|
};
|
|
907
903
|
//#endregion
|
|
904
|
+
//#region src/utils/tree.ts
|
|
905
|
+
/** Consumes the report, reusing its suite/entry arrays instead of retaining two trees. */
|
|
906
|
+
function consumeTestReport(report) {
|
|
907
|
+
const root = {
|
|
908
|
+
type: "suite",
|
|
909
|
+
title: "Root",
|
|
910
|
+
children: []
|
|
911
|
+
};
|
|
912
|
+
const filePath = (file) => file.replace(/\\/g, "/");
|
|
913
|
+
function convertEntries(entries) {
|
|
914
|
+
for (let i = 0; i < entries.length; i++) {
|
|
915
|
+
const entry = entries[i];
|
|
916
|
+
entries[i] = entry.testId ? {
|
|
917
|
+
type: "test",
|
|
918
|
+
title: entry.title,
|
|
919
|
+
testId: entry.testId,
|
|
920
|
+
location: {
|
|
921
|
+
...entry.location,
|
|
922
|
+
file: filePath(entry.location.file)
|
|
923
|
+
},
|
|
924
|
+
tags: entry.tags || [],
|
|
925
|
+
annotations: entry.annotations || [],
|
|
926
|
+
expectedStatus: "passed",
|
|
927
|
+
timeout: 0
|
|
928
|
+
} : {
|
|
929
|
+
type: "suite",
|
|
930
|
+
title: entry.title,
|
|
931
|
+
file: entry.location ? filePath(entry.location.file) : void 0,
|
|
932
|
+
children: convertEntries(entry.entries || [])
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
return entries;
|
|
936
|
+
}
|
|
937
|
+
const error = report.find((message) => message.method === "onError");
|
|
938
|
+
if (error) throw new Error(error.params.error.message || "Playwright test collection failed");
|
|
939
|
+
for (let i = 0; i < report.length; i++) {
|
|
940
|
+
const message = report[i];
|
|
941
|
+
report[i] = void 0;
|
|
942
|
+
if (message.method !== "onProject") continue;
|
|
943
|
+
const project = message.params.project;
|
|
944
|
+
root.children.push({
|
|
945
|
+
type: "suite",
|
|
946
|
+
title: project.name,
|
|
947
|
+
children: convertEntries(project.suites || [])
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
report.length = 0;
|
|
951
|
+
return root;
|
|
952
|
+
}
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region src/collectTree.ts
|
|
955
|
+
/** CLI list mode loads tests in one process, without transferring the loader's dependency graph. */
|
|
956
|
+
function collectFullTree(workspaceFolder, configFile, token) {
|
|
957
|
+
if (token?.isCancellationRequested) return Promise.reject(/* @__PURE__ */ new Error("Test collection cancelled"));
|
|
958
|
+
workspaceFolder = path.resolve(workspaceFolder);
|
|
959
|
+
const cli = findPlaywrightCli(workspaceFolder);
|
|
960
|
+
const reporter = fileURLToPath(new URL("./treeReporter.mjs", import.meta.url));
|
|
961
|
+
return new Promise((resolve, reject) => {
|
|
962
|
+
const child = fork(cli, [
|
|
963
|
+
"test",
|
|
964
|
+
"--list",
|
|
965
|
+
"--reporter",
|
|
966
|
+
reporter,
|
|
967
|
+
"--project",
|
|
968
|
+
"*",
|
|
969
|
+
"--repeat-each",
|
|
970
|
+
"1",
|
|
971
|
+
"--retries",
|
|
972
|
+
"0",
|
|
973
|
+
"--global-timeout",
|
|
974
|
+
"0",
|
|
975
|
+
"--pass-with-no-tests",
|
|
976
|
+
"-c",
|
|
977
|
+
path.resolve(workspaceFolder, configFile)
|
|
978
|
+
], {
|
|
979
|
+
cwd: workspaceFolder,
|
|
980
|
+
env: createTestServerEnv(process.env, {}, workspaceFolder),
|
|
981
|
+
execArgv: [],
|
|
982
|
+
serialization: "advanced",
|
|
983
|
+
stdio: [
|
|
984
|
+
"ignore",
|
|
985
|
+
"ignore",
|
|
986
|
+
"pipe",
|
|
987
|
+
"ipc"
|
|
988
|
+
]
|
|
989
|
+
});
|
|
990
|
+
let result;
|
|
991
|
+
let stderr = "";
|
|
992
|
+
child.stderr.on("data", (chunk) => {
|
|
993
|
+
stderr = (stderr + chunk).slice(-64 * 1024);
|
|
994
|
+
});
|
|
995
|
+
child.on("message", (message) => {
|
|
996
|
+
if (message && typeof message === "object" && "type" in message && message.type === "test-explorer:tree") result = message;
|
|
997
|
+
});
|
|
998
|
+
const cancellation = token?.onCancellationRequested(() => child.kill());
|
|
999
|
+
child.once("error", reject);
|
|
1000
|
+
child.once("close", (code, signal) => {
|
|
1001
|
+
cancellation?.dispose();
|
|
1002
|
+
if (token?.isCancellationRequested) reject(/* @__PURE__ */ new Error("Test collection cancelled"));
|
|
1003
|
+
else if (code !== 0 || !result?.tree || result.error) reject(new Error(result?.error || stderr.trim() || `Playwright test collection failed (${signal || code})`));
|
|
1004
|
+
else resolve(result.tree);
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
908
1009
|
//#region src/api.ts
|
|
909
|
-
|
|
910
|
-
|
|
1010
|
+
const execFileAsync = promisify(execFile);
|
|
1011
|
+
const GIT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
1012
|
+
const COLLECTION_ENV_KEYS = [
|
|
1013
|
+
"CI",
|
|
1014
|
+
"NODE_ENV",
|
|
1015
|
+
"NODE_OPTIONS",
|
|
1016
|
+
"TZ"
|
|
1017
|
+
];
|
|
1018
|
+
async function runGit(workspaceFolder, args) {
|
|
1019
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
1020
|
+
cwd: workspaceFolder,
|
|
1021
|
+
encoding: "buffer",
|
|
1022
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
1023
|
+
});
|
|
1024
|
+
return stdout;
|
|
1025
|
+
}
|
|
1026
|
+
function sha256(value) {
|
|
1027
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1028
|
+
}
|
|
1029
|
+
async function incrementalInputHash(workspaceFolder, configFile, options) {
|
|
1030
|
+
const configPath = path.resolve(workspaceFolder, configFile);
|
|
1031
|
+
const environment = [...new Set([...COLLECTION_ENV_KEYS, ...options.environmentKeys ?? []])].sort().map((key) => [key, process.env[key]]);
|
|
1032
|
+
return createHash("sha256").update(JSON.stringify([
|
|
911
1033
|
workspaceFolder,
|
|
912
|
-
|
|
1034
|
+
configPath,
|
|
1035
|
+
process.version,
|
|
1036
|
+
environment
|
|
1037
|
+
])).update("\0").update(await fs.readFile(configPath)).digest("hex");
|
|
1038
|
+
}
|
|
1039
|
+
function parseGitNameStatus(buffer) {
|
|
1040
|
+
const fields = buffer.toString("utf8").split("\0");
|
|
1041
|
+
const changes = [];
|
|
1042
|
+
for (let i = 0; i < fields.length;) {
|
|
1043
|
+
const status = fields[i++];
|
|
1044
|
+
if (!status) continue;
|
|
1045
|
+
if (status.startsWith("R") || status.startsWith("C")) changes.push({
|
|
1046
|
+
status: status[0],
|
|
1047
|
+
from: fields[i++],
|
|
1048
|
+
to: fields[i++]
|
|
1049
|
+
});
|
|
1050
|
+
else changes.push({
|
|
1051
|
+
status: status[0],
|
|
1052
|
+
path: fields[i++]
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return changes;
|
|
1056
|
+
}
|
|
1057
|
+
function normalizePath(file) {
|
|
1058
|
+
return file.replace(/\\/g, "/");
|
|
1059
|
+
}
|
|
1060
|
+
function isWithin(directory, file) {
|
|
1061
|
+
const relative = path.relative(directory, file);
|
|
1062
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
1063
|
+
}
|
|
1064
|
+
/** Share untouched branches; only replace affected project/file arrays. */
|
|
1065
|
+
function mergeIncrementalTree(cachedTree, changedTree, updatedFilesByProject) {
|
|
1066
|
+
const changedProjects = new Map((changedTree.children ?? []).map((project) => [project.title, project]));
|
|
1067
|
+
const projects = [...cachedTree.children ?? []];
|
|
1068
|
+
const knownProjects = new Set(projects.map((project) => project.title));
|
|
1069
|
+
for (const project of changedTree.children ?? []) if (!knownProjects.has(project.title)) projects.push({
|
|
1070
|
+
...project,
|
|
1071
|
+
children: []
|
|
913
1072
|
});
|
|
1073
|
+
return {
|
|
1074
|
+
...cachedTree,
|
|
1075
|
+
children: projects.map((project) => {
|
|
1076
|
+
const files = updatedFilesByProject.get(project.title);
|
|
1077
|
+
if (!files?.size) return project;
|
|
1078
|
+
const children = (project.children ?? []).filter((node) => !files.has(normalizePath(node.file || "")));
|
|
1079
|
+
for (const node of changedProjects.get(project.title)?.children ?? []) if (files.has(normalizePath(node.file || ""))) children.push(node);
|
|
1080
|
+
children.sort((a, b) => (a.file || "") < (b.file || "") ? -1 : (a.file || "") > (b.file || "") ? 1 : 0);
|
|
1081
|
+
return {
|
|
1082
|
+
...project,
|
|
1083
|
+
children
|
|
1084
|
+
};
|
|
1085
|
+
}).filter((project) => project.children?.length)
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
async function writeFileAtomic(file, value) {
|
|
1089
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
914
1090
|
try {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const jsonString = JSON.stringify(output, null, 2);
|
|
920
|
-
if (outputFile) {
|
|
921
|
-
await fs.writeFile(outputFile, jsonString, "utf-8");
|
|
922
|
-
console.log(`Results saved to: ${outputFile}`);
|
|
923
|
-
} else console.log(jsonString);
|
|
924
|
-
testServer.close();
|
|
925
|
-
} catch (error) {
|
|
926
|
-
console.error("Error:", error);
|
|
927
|
-
testServer.close();
|
|
1091
|
+
await fs.writeFile(temporary, value);
|
|
1092
|
+
await fs.rename(temporary, file);
|
|
1093
|
+
} finally {
|
|
1094
|
+
await fs.unlink(temporary).catch(() => {});
|
|
928
1095
|
}
|
|
929
1096
|
}
|
|
930
|
-
async function
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1097
|
+
async function readIncrementalState(stateFile) {
|
|
1098
|
+
try {
|
|
1099
|
+
return JSON.parse(await fs.readFile(stateFile, "utf8"));
|
|
1100
|
+
} catch {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
async function isGitAncestor(workspaceFolder, base, head) {
|
|
1105
|
+
if (base === head) return true;
|
|
1106
|
+
try {
|
|
1107
|
+
await runGit(workspaceFolder, [
|
|
1108
|
+
"merge-base",
|
|
1109
|
+
"--is-ancestor",
|
|
1110
|
+
base,
|
|
1111
|
+
head
|
|
1112
|
+
]);
|
|
1113
|
+
return true;
|
|
1114
|
+
} catch {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function isTestFile(file) {
|
|
1119
|
+
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(file);
|
|
1120
|
+
}
|
|
1121
|
+
function createServer(workspaceFolder, configFile) {
|
|
1122
|
+
return new PlaywrightTestServer({
|
|
1123
|
+
workspaceFolder: path.resolve(workspaceFolder),
|
|
1124
|
+
configFile: path.resolve(workspaceFolder, configFile)
|
|
934
1125
|
});
|
|
1126
|
+
}
|
|
1127
|
+
/** Update committed test changes; non-test changes conservatively rebuild the tree. */
|
|
1128
|
+
async function updateTreeStructure(workspaceFolder, configFile, outputFile, stateFile = `${outputFile}.state.json`, options = {}) {
|
|
1129
|
+
workspaceFolder = path.resolve(workspaceFolder);
|
|
1130
|
+
const head = (await runGit(workspaceFolder, ["rev-parse", "HEAD"])).toString("utf8").trim();
|
|
1131
|
+
const inputHash = await incrementalInputHash(workspaceFolder, configFile, options);
|
|
1132
|
+
const state = await readIncrementalState(stateFile);
|
|
1133
|
+
let treeContent = await fs.readFile(outputFile).catch(() => void 0);
|
|
1134
|
+
const validCache = state?.version === 2 && state.inputHash === inputHash && treeContent !== void 0 && state.treeHash === sha256(treeContent) && await isGitAncestor(workspaceFolder, state.commit, head);
|
|
1135
|
+
const save = async (tree) => {
|
|
1136
|
+
const content = `${JSON.stringify(tree, null, 2)}\n`;
|
|
1137
|
+
const nextState = {
|
|
1138
|
+
version: 2,
|
|
1139
|
+
commit: head,
|
|
1140
|
+
inputHash,
|
|
1141
|
+
treeHash: sha256(content)
|
|
1142
|
+
};
|
|
1143
|
+
await writeFileAtomic(outputFile, content);
|
|
1144
|
+
await writeFileAtomic(stateFile, `${JSON.stringify(nextState, null, 2)}\n`);
|
|
1145
|
+
};
|
|
1146
|
+
if (validCache && state.commit === head) return {
|
|
1147
|
+
mode: "unchanged",
|
|
1148
|
+
updatedFiles: 0,
|
|
1149
|
+
deletedFiles: 0
|
|
1150
|
+
};
|
|
1151
|
+
const gitRoot = (await runGit(workspaceFolder, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
|
|
1152
|
+
const changes = validCache ? parseGitNameStatus(await runGit(gitRoot, [
|
|
1153
|
+
"diff",
|
|
1154
|
+
"--no-relative",
|
|
1155
|
+
"--name-status",
|
|
1156
|
+
"-z",
|
|
1157
|
+
"--find-renames",
|
|
1158
|
+
`${state.commit}...${head}`
|
|
1159
|
+
])) : [];
|
|
1160
|
+
if (validCache && !changes.length) {
|
|
1161
|
+
await writeFileAtomic(stateFile, `${JSON.stringify({
|
|
1162
|
+
...state,
|
|
1163
|
+
commit: head
|
|
1164
|
+
}, null, 2)}\n`);
|
|
1165
|
+
return {
|
|
1166
|
+
mode: "unchanged",
|
|
1167
|
+
updatedFiles: 0,
|
|
1168
|
+
deletedFiles: 0
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const needsFullBuild = !validCache || changes.some((change) => ("from" in change ? [change.from, change.to] : [change.path]).some((file) => !isTestFile(file)));
|
|
1172
|
+
let cachedTree;
|
|
1173
|
+
if (!needsFullBuild) try {
|
|
1174
|
+
cachedTree = JSON.parse(treeContent.toString("utf8"));
|
|
1175
|
+
if (cachedTree?.type !== "suite" || !Array.isArray(cachedTree.children)) cachedTree = void 0;
|
|
1176
|
+
} catch {}
|
|
1177
|
+
treeContent = void 0;
|
|
1178
|
+
if (!cachedTree) {
|
|
1179
|
+
await save(await collectFullTree(workspaceFolder, configFile));
|
|
1180
|
+
return {
|
|
1181
|
+
mode: "full",
|
|
1182
|
+
updatedFiles: 0,
|
|
1183
|
+
deletedFiles: 0
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const server = createServer(workspaceFolder, configFile);
|
|
935
1187
|
try {
|
|
936
|
-
const
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const testDirRelative = project.testDir || "";
|
|
946
|
-
for (const file of project.files) {
|
|
947
|
-
const fullPath = testDirRelative ? path.join(testDirRelative, file).replace(/\\/g, "/") : file.replace(/\\/g, "/");
|
|
948
|
-
if (fullPath.startsWith(normalizedInputPath + "/") || fullPath === normalizedInputPath) expandedPaths.push(fullPath);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
} else expandedPaths.push(normalizedInputPath);
|
|
952
|
-
} catch {
|
|
953
|
-
expandedPaths.push(normalizedInputPath);
|
|
1188
|
+
const listed = await server.listFiles();
|
|
1189
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1190
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
1191
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1192
|
+
for (const change of changes) {
|
|
1193
|
+
if ("from" in change && change.status === "R") {
|
|
1194
|
+
const oldFile = normalizePath(path.relative(listed.rootDir, path.resolve(gitRoot, change.from)));
|
|
1195
|
+
affected.add(oldFile);
|
|
1196
|
+
deleted.add(oldFile);
|
|
954
1197
|
}
|
|
1198
|
+
const file = normalizePath(path.relative(listed.rootDir, path.resolve(gitRoot, "to" in change ? change.to : change.path)));
|
|
1199
|
+
affected.add(file);
|
|
1200
|
+
if (change.status === "D") deleted.add(file);
|
|
1201
|
+
else candidates.add(file);
|
|
955
1202
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
type: "test",
|
|
966
|
-
title: entry.title,
|
|
967
|
-
testId: entry.testId,
|
|
968
|
-
location: {
|
|
969
|
-
file: filePath,
|
|
970
|
-
line: entry.location.line,
|
|
971
|
-
column: entry.location.column
|
|
972
|
-
},
|
|
973
|
-
tags: entry.tags || [],
|
|
974
|
-
annotations: entry.annotations || [],
|
|
975
|
-
expectedStatus: "passed",
|
|
976
|
-
timeout: 0
|
|
977
|
-
};
|
|
978
|
-
} else {
|
|
979
|
-
const filePath = entry.location && testDir ? path.join(testDir, entry.location.file).replace(/\\/g, "/") : entry.location?.file.replace(/\\/g, "/");
|
|
980
|
-
const node = {
|
|
981
|
-
type: "suite",
|
|
982
|
-
title: entry.title,
|
|
983
|
-
file: filePath,
|
|
984
|
-
children: []
|
|
985
|
-
};
|
|
986
|
-
if (entry.entries && Array.isArray(entry.entries)) for (const childEntry of entry.entries) node.children.push(parseEntry(childEntry, testDir));
|
|
987
|
-
return node;
|
|
1203
|
+
const pathsToCollect = /* @__PURE__ */ new Set();
|
|
1204
|
+
const updatedByProject = /* @__PURE__ */ new Map();
|
|
1205
|
+
const deletedFiles = /* @__PURE__ */ new Set();
|
|
1206
|
+
for (const project of cachedTree.children ?? []) {
|
|
1207
|
+
const files = /* @__PURE__ */ new Set();
|
|
1208
|
+
for (const node of project.children ?? []) {
|
|
1209
|
+
const file = normalizePath(node.file || "");
|
|
1210
|
+
if (affected.has(file)) files.add(file);
|
|
1211
|
+
if (deleted.has(file)) deletedFiles.add(file);
|
|
988
1212
|
}
|
|
1213
|
+
updatedByProject.set(project.title, files);
|
|
989
1214
|
}
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const project = message.params.project;
|
|
997
|
-
const projectNode = {
|
|
998
|
-
type: "suite",
|
|
999
|
-
title: project.name,
|
|
1000
|
-
children: []
|
|
1001
|
-
};
|
|
1002
|
-
let testDir = "";
|
|
1003
|
-
if (project.testDir) testDir = path.isAbsolute(project.testDir) ? path.relative(workspaceFolder, project.testDir).replace(/\\/g, "/") : project.testDir.replace(/\\/g, "/");
|
|
1004
|
-
if (project.suites && Array.isArray(project.suites)) for (const suite of project.suites) {
|
|
1005
|
-
const suiteNode = parseEntry(suite, testDir);
|
|
1006
|
-
projectNode.children.push(suiteNode);
|
|
1215
|
+
for (const project of listed.projects) {
|
|
1216
|
+
const files = updatedByProject.get(project.name) ?? /* @__PURE__ */ new Set();
|
|
1217
|
+
for (const file of project.files) {
|
|
1218
|
+
if (!candidates.has(file)) continue;
|
|
1219
|
+
files.add(file);
|
|
1220
|
+
pathsToCollect.add(path.resolve(listed.rootDir, file));
|
|
1007
1221
|
}
|
|
1008
|
-
|
|
1222
|
+
updatedByProject.set(project.name, files);
|
|
1009
1223
|
}
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1224
|
+
const partialTree = pathsToCollect.size ? consumeTestReport(await server.getRawTestReport([...pathsToCollect])) : consumeTestReport([]);
|
|
1225
|
+
const merged = mergeIncrementalTree(cachedTree, partialTree, updatedByProject);
|
|
1226
|
+
const projectOrder = new Map(listed.projects.map((project, index) => [project.name, index]));
|
|
1227
|
+
merged.children.sort((a, b) => projectOrder.get(a.title) - projectOrder.get(b.title));
|
|
1228
|
+
await save(merged);
|
|
1229
|
+
const updatedFiles = pathsToCollect.size;
|
|
1230
|
+
return {
|
|
1231
|
+
mode: updatedFiles || deletedFiles.size ? "incremental" : "unchanged",
|
|
1232
|
+
updatedFiles,
|
|
1233
|
+
deletedFiles: deletedFiles.size
|
|
1234
|
+
};
|
|
1235
|
+
} finally {
|
|
1236
|
+
server.close();
|
|
1019
1237
|
}
|
|
1020
1238
|
}
|
|
1021
|
-
async function
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1239
|
+
async function outputJson(value, outputFile) {
|
|
1240
|
+
const content = JSON.stringify(value, null, 2);
|
|
1241
|
+
if (outputFile) {
|
|
1242
|
+
await fs.writeFile(outputFile, content, "utf8");
|
|
1243
|
+
console.log(`Results saved to: ${outputFile}`);
|
|
1244
|
+
} else console.log(content);
|
|
1245
|
+
}
|
|
1246
|
+
async function listTestFile(workspaceFolder = process.cwd(), configFile = "playwright.config.ts", outputFile) {
|
|
1247
|
+
const server = createServer(workspaceFolder, configFile);
|
|
1026
1248
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const filePath = entry.location.file.replace(/\\/g, "/");
|
|
1049
|
-
return {
|
|
1050
|
-
type: "test",
|
|
1051
|
-
title: entry.title,
|
|
1052
|
-
testId: entry.testId,
|
|
1053
|
-
location: {
|
|
1054
|
-
file: filePath,
|
|
1055
|
-
line: entry.location.line,
|
|
1056
|
-
column: entry.location.column
|
|
1057
|
-
},
|
|
1058
|
-
tags: entry.tags || [],
|
|
1059
|
-
annotations: entry.annotations || [],
|
|
1060
|
-
expectedStatus: "passed",
|
|
1061
|
-
timeout: 0
|
|
1062
|
-
};
|
|
1063
|
-
} else {
|
|
1064
|
-
const filePath = entry.location?.file.replace(/\\/g, "/");
|
|
1065
|
-
const node = {
|
|
1066
|
-
type: "suite",
|
|
1067
|
-
title: entry.title,
|
|
1068
|
-
file: filePath,
|
|
1069
|
-
children: []
|
|
1070
|
-
};
|
|
1071
|
-
if (entry.entries && Array.isArray(entry.entries)) for (const childEntry of entry.entries) node.children.push(parseEntry(childEntry, testDir));
|
|
1072
|
-
return node;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
const rootNode = {
|
|
1076
|
-
type: "suite",
|
|
1077
|
-
title: "Root",
|
|
1078
|
-
children: []
|
|
1079
|
-
};
|
|
1080
|
-
for (const message of report) if (message.method === "onProject") {
|
|
1081
|
-
const project = message.params.project;
|
|
1082
|
-
const projectNode = {
|
|
1083
|
-
type: "suite",
|
|
1084
|
-
title: project.name,
|
|
1085
|
-
children: []
|
|
1086
|
-
};
|
|
1087
|
-
let testDir = "";
|
|
1088
|
-
if (project.testDir) testDir = path.isAbsolute(project.testDir) ? path.relative(workspaceFolder, project.testDir).replace(/\\/g, "/") : project.testDir.replace(/\\/g, "/");
|
|
1089
|
-
if (project.suites && Array.isArray(project.suites)) for (const suite of project.suites) {
|
|
1090
|
-
const suiteNode = parseEntry(suite, testDir);
|
|
1091
|
-
projectNode.children.push(suiteNode);
|
|
1092
|
-
}
|
|
1093
|
-
rootNode.children.push(projectNode);
|
|
1249
|
+
const { projects } = await server.listFiles();
|
|
1250
|
+
await outputJson({ projects }, outputFile);
|
|
1251
|
+
} finally {
|
|
1252
|
+
server.close();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async function listTestCase(workspaceFolder = process.cwd(), configFile = "playwright.config.ts", relativePaths, outputFile) {
|
|
1256
|
+
workspaceFolder = path.resolve(workspaceFolder);
|
|
1257
|
+
const server = createServer(workspaceFolder, configFile);
|
|
1258
|
+
try {
|
|
1259
|
+
const expandedPaths = /* @__PURE__ */ new Set();
|
|
1260
|
+
let listed;
|
|
1261
|
+
for (const input of relativePaths) {
|
|
1262
|
+
const absolutePath = path.resolve(workspaceFolder, normalizePath(input));
|
|
1263
|
+
if ((await fs.stat(absolutePath).catch(() => void 0))?.isDirectory()) {
|
|
1264
|
+
listed ??= await server.listFiles();
|
|
1265
|
+
for (const project of listed.projects) for (const file of project.files) {
|
|
1266
|
+
const absoluteFile = path.resolve(listed.rootDir, file);
|
|
1267
|
+
if (isWithin(absolutePath, absoluteFile)) expandedPaths.add(absoluteFile);
|
|
1268
|
+
}
|
|
1269
|
+
} else expandedPaths.add(absolutePath);
|
|
1094
1270
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
} else console.log(jsonString);
|
|
1100
|
-
testServer.close();
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
console.error("Error:", error);
|
|
1103
|
-
testServer.close();
|
|
1271
|
+
if (!expandedPaths.size) return;
|
|
1272
|
+
await outputJson(consumeTestReport(await server.getRawTestReport([...expandedPaths])), outputFile);
|
|
1273
|
+
} finally {
|
|
1274
|
+
server.close();
|
|
1104
1275
|
}
|
|
1105
1276
|
}
|
|
1277
|
+
async function treeStructure(workspaceFolder = process.cwd(), configFile = "playwright.config.ts", cancellationToken, outputFile) {
|
|
1278
|
+
await outputJson(await collectFullTree(workspaceFolder, configFile, cancellationToken), outputFile);
|
|
1279
|
+
}
|
|
1106
1280
|
//#endregion
|
|
1107
|
-
export { CancellationToken as i, listTestFile as n, treeStructure as r, listTestCase as t };
|
|
1281
|
+
export { CancellationToken as a, updateTreeStructure as i, listTestFile as n, treeStructure as r, listTestCase as t };
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as CancellationToken, i as updateTreeStructure, n as listTestFile, r as treeStructure, t as listTestCase } from "./api-CvJUOXye.mjs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { program } from "commander";
|
|
5
5
|
import chalk from "chalk";
|
|
@@ -40,8 +40,8 @@ program.command("list-tests").description("List all test cases in specified file
|
|
|
40
40
|
process.exit(1);
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
-
program.command("tree").description("Display test structure as a tree").option("-w, --workspace <path>", "workspace folder path", process.cwd()).option("-c, --config <file>", "config file path (relative to workspace)", "playwright.config.ts").option("-o, --output <file>", "output file path to save JSON results").action(async (options) => {
|
|
44
|
-
const spinner = ora("Building test tree...").start();
|
|
43
|
+
program.command("tree").description("Display test structure as a tree").option("-w, --workspace <path>", "workspace folder path", process.cwd()).option("-c, --config <file>", "config file path (relative to workspace)", "playwright.config.ts").option("-o, --output <file>", "output file path to save JSON results").option("--incremental", "update a saved tree from Git changes").option("--state <file>", "incremental state file path (defaults to <output>.state.json)").option("--cache-env <names>", "additional collection environment keys, comma-separated").action(async (options) => {
|
|
44
|
+
const spinner = ora(options.incremental ? "Updating test tree..." : "Building test tree...").start();
|
|
45
45
|
try {
|
|
46
46
|
const workspaceFolder = path.resolve(options.workspace);
|
|
47
47
|
const token = new CancellationToken();
|
|
@@ -51,8 +51,15 @@ program.command("tree").description("Display test structure as a tree").option("
|
|
|
51
51
|
process.exit(0);
|
|
52
52
|
});
|
|
53
53
|
const outputFile = options.output ? path.resolve(options.output) : void 0;
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
if (options.incremental) {
|
|
55
|
+
const incrementalOutput = outputFile || path.join(workspaceFolder, "test-case.json");
|
|
56
|
+
const stateFile = options.state ? path.resolve(options.state) : `${incrementalOutput}.state.json`;
|
|
57
|
+
const result = await updateTreeStructure(workspaceFolder, options.config, incrementalOutput, stateFile, { environmentKeys: options.cacheEnv?.split(",").map((key) => key.trim()).filter(Boolean) });
|
|
58
|
+
spinner.succeed(chalk.green(`Test tree ${result.mode}: ${result.updatedFiles} updated, ${result.deletedFiles} deleted`));
|
|
59
|
+
} else {
|
|
60
|
+
await treeStructure(workspaceFolder, options.config, token, outputFile);
|
|
61
|
+
spinner.succeed(chalk.green("Test tree built successfully"));
|
|
62
|
+
}
|
|
56
63
|
} catch (error) {
|
|
57
64
|
spinner.fail(chalk.red("Failed to build test tree"));
|
|
58
65
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
@@ -89,6 +96,9 @@ ${chalk.bold("Examples:")}
|
|
|
89
96
|
${chalk.cyan("# Save tree structure to JSON file")}
|
|
90
97
|
$ pw-test-explorer tree -o tree.json
|
|
91
98
|
|
|
99
|
+
${chalk.cyan("# Incrementally update a saved tree from Git changes")}
|
|
100
|
+
$ pw-test-explorer tree --incremental -o tree.json
|
|
101
|
+
|
|
92
102
|
${chalk.cyan("# Display tree for specific workspace")}
|
|
93
103
|
$ pw-test-explorer tree -w /path/to/project
|
|
94
104
|
`);
|
package/dist/index.d.mts
CHANGED
|
@@ -34,8 +34,19 @@ declare class CancellationToken {
|
|
|
34
34
|
}
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/api.d.ts
|
|
37
|
+
type IncrementalTreeOptions = {
|
|
38
|
+
/** Additional environment variables that affect configuration or test collection. */environmentKeys?: string[];
|
|
39
|
+
};
|
|
40
|
+
type IncrementalTreeResult = {
|
|
41
|
+
mode: 'full' | 'incremental' | 'unchanged';
|
|
42
|
+
updatedFiles: number;
|
|
43
|
+
deletedFiles: number;
|
|
44
|
+
};
|
|
45
|
+
/** Share untouched branches; only replace affected project/file arrays. */
|
|
46
|
+
/** Update committed test changes; non-test changes conservatively rebuild the tree. */
|
|
47
|
+
declare function updateTreeStructure(workspaceFolder: string, configFile: string, outputFile: string, stateFile?: string, options?: IncrementalTreeOptions): Promise<IncrementalTreeResult>;
|
|
37
48
|
declare function listTestFile(workspaceFolder?: string, configFile?: string, outputFile?: string): Promise<void>;
|
|
38
49
|
declare function listTestCase(workspaceFolder: string | undefined, configFile: string | undefined, relativePaths: string[], outputFile?: string): Promise<void>;
|
|
39
50
|
declare function treeStructure(workspaceFolder?: string, configFile?: string, cancellationToken?: CancellationToken, outputFile?: string): Promise<void>;
|
|
40
51
|
//#endregion
|
|
41
|
-
export { listTestCase, listTestFile, treeStructure };
|
|
52
|
+
export { type IncrementalTreeOptions, listTestCase, listTestFile, treeStructure, updateTreeStructure };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as listTestFile, r as treeStructure, t as listTestCase } from "./api-
|
|
2
|
-
export { listTestCase, listTestFile, treeStructure };
|
|
1
|
+
import { i as updateTreeStructure, n as listTestFile, r as treeStructure, t as listTestCase } from "./api-CvJUOXye.mjs";
|
|
2
|
+
export { listTestCase, listTestFile, treeStructure, updateTreeStructure };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/treeReporter.mjs
|
|
3
|
+
var TreeReporter = class {
|
|
4
|
+
tree;
|
|
5
|
+
errors = [];
|
|
6
|
+
printsToStdio() {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
onBegin(config, rootSuite) {
|
|
10
|
+
const location = (value) => value && {
|
|
11
|
+
...value,
|
|
12
|
+
file: path.relative(config.rootDir, value.file).replace(/\\/g, "/")
|
|
13
|
+
};
|
|
14
|
+
const convert = (entry) => entry.type === "test" ? {
|
|
15
|
+
type: "test",
|
|
16
|
+
title: entry.title,
|
|
17
|
+
testId: entry.id,
|
|
18
|
+
location: location(entry.location),
|
|
19
|
+
tags: entry.tags || [],
|
|
20
|
+
annotations: entry.annotations.map((annotation) => ({
|
|
21
|
+
...annotation,
|
|
22
|
+
location: location(annotation.location)
|
|
23
|
+
})),
|
|
24
|
+
expectedStatus: "passed",
|
|
25
|
+
timeout: 0
|
|
26
|
+
} : {
|
|
27
|
+
type: "suite",
|
|
28
|
+
title: entry.title,
|
|
29
|
+
file: location(entry.location)?.file,
|
|
30
|
+
children: entry.entries().map(convert)
|
|
31
|
+
};
|
|
32
|
+
this.tree = {
|
|
33
|
+
type: "suite",
|
|
34
|
+
title: "Root",
|
|
35
|
+
children: rootSuite.suites.map((project) => ({
|
|
36
|
+
type: "suite",
|
|
37
|
+
title: project.title,
|
|
38
|
+
children: project.entries().map(convert)
|
|
39
|
+
}))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
onError(error) {
|
|
43
|
+
this.errors.push(error.message || error.value || "Playwright test collection failed");
|
|
44
|
+
}
|
|
45
|
+
async onEnd(result) {
|
|
46
|
+
await new Promise((resolve, reject) => process.send({
|
|
47
|
+
type: "test-explorer:tree",
|
|
48
|
+
tree: result.status === "passed" ? this.tree : void 0,
|
|
49
|
+
error: this.errors.join("\n")
|
|
50
|
+
}, (error) => error ? reject(error) : resolve()));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { TreeReporter as default };
|