playwright-test-explorer 0.0.10 → 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-Bc2ZCCQ0.mjs → api-CvJUOXye.mjs} +385 -221
- package/dist/cli.mjs +16 -8
- package/dist/index.d.mts +12 -2
- package/dist/index.mjs +2 -3
- package/dist/treeReporter.mjs +54 -0
- package/package.json +14 -14
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,12 +1,13 @@
|
|
|
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";
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
//#region \0rolldown/runtime.js
|
|
8
10
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
9
|
-
|
|
10
11
|
//#endregion
|
|
11
12
|
//#region src/upstream/teleReceiver.ts
|
|
12
13
|
var TeleReporterReceiver = class {
|
|
@@ -406,7 +407,6 @@ function parseRegexPatterns(patterns) {
|
|
|
406
407
|
return new RegExp(p.r.source, p.r.flags);
|
|
407
408
|
});
|
|
408
409
|
}
|
|
409
|
-
|
|
410
410
|
//#endregion
|
|
411
411
|
//#region src/upstream/events.ts
|
|
412
412
|
let Disposable;
|
|
@@ -444,8 +444,8 @@ var EventEmitter = class {
|
|
|
444
444
|
});
|
|
445
445
|
if (!dispatch) return;
|
|
446
446
|
for (let index = 0; index < this._deliveryQueue.length; index++) {
|
|
447
|
-
const { listener, event
|
|
448
|
-
listener.call(null, event
|
|
447
|
+
const { listener, event } = this._deliveryQueue[index];
|
|
448
|
+
listener.call(null, event);
|
|
449
449
|
}
|
|
450
450
|
this._deliveryQueue = void 0;
|
|
451
451
|
}
|
|
@@ -454,7 +454,6 @@ var EventEmitter = class {
|
|
|
454
454
|
if (this._deliveryQueue) this._deliveryQueue = [];
|
|
455
455
|
}
|
|
456
456
|
};
|
|
457
|
-
|
|
458
457
|
//#endregion
|
|
459
458
|
//#region src/upstream/testServerConnection.ts
|
|
460
459
|
var WebSocketTestServerTransport = class {
|
|
@@ -635,7 +634,6 @@ var TestServerConnection = class {
|
|
|
635
634
|
} catch {}
|
|
636
635
|
}
|
|
637
636
|
};
|
|
638
|
-
|
|
639
637
|
//#endregion
|
|
640
638
|
//#region src/utils/pathUtils.ts
|
|
641
639
|
/**
|
|
@@ -650,14 +648,6 @@ function escapeRegex(text) {
|
|
|
650
648
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
651
649
|
}
|
|
652
650
|
/**
|
|
653
|
-
* Path resolution strategy for TeleReporterReceiver
|
|
654
|
-
* For file paths (with extensions), return relative path (Playwright format)
|
|
655
|
-
* For directory paths (testDir, outputDir, etc.), return absolute path
|
|
656
|
-
*/
|
|
657
|
-
function resolvePathForFiles(rootDir, relativePath) {
|
|
658
|
-
return /\.\w+$/.test(relativePath) ? relativePath : path.join(rootDir, relativePath);
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
651
|
* Standard path resolution - always returns absolute path
|
|
662
652
|
*/
|
|
663
653
|
function resolveAbsolutePath(rootDir, relativePath) {
|
|
@@ -674,7 +664,6 @@ function findPlaywrightCli(workspaceFolder) {
|
|
|
674
664
|
throw new Error("Playwright installation not found. Please install @playwright/test");
|
|
675
665
|
}
|
|
676
666
|
}
|
|
677
|
-
|
|
678
667
|
//#endregion
|
|
679
668
|
//#region src/utils/processUtils.ts
|
|
680
669
|
/**
|
|
@@ -711,21 +700,9 @@ function createTestServerEnv(baseEnv, customEnv, cwd) {
|
|
|
711
700
|
FORCE_COLOR: "1"
|
|
712
701
|
}, cwd);
|
|
713
702
|
}
|
|
714
|
-
|
|
715
703
|
//#endregion
|
|
716
704
|
//#region src/utils/reporterFactory.ts
|
|
717
705
|
/**
|
|
718
|
-
* Create TeleReporterReceiver for listing files
|
|
719
|
-
* Uses special path resolution for preserving relative paths for files
|
|
720
|
-
*/
|
|
721
|
-
function createFileListReceiver(reporter) {
|
|
722
|
-
return new TeleReporterReceiver(reporter, {
|
|
723
|
-
mergeProjects: true,
|
|
724
|
-
mergeTestCases: true,
|
|
725
|
-
resolvePath: resolvePathForFiles
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
/**
|
|
729
706
|
* Create TeleReporterReceiver for listing tests
|
|
730
707
|
* Uses standard absolute path resolution
|
|
731
708
|
*/
|
|
@@ -742,7 +719,6 @@ function createTestListReceiver(reporter) {
|
|
|
742
719
|
function processReport(receiver, report) {
|
|
743
720
|
for (const message of report) receiver.dispatch(message);
|
|
744
721
|
}
|
|
745
|
-
|
|
746
722
|
//#endregion
|
|
747
723
|
//#region src/playwrightTestServer.ts
|
|
748
724
|
/**
|
|
@@ -781,11 +757,16 @@ var PlaywrightTestServer = class {
|
|
|
781
757
|
}
|
|
782
758
|
async listFiles() {
|
|
783
759
|
const connection = await this._ensureConnection();
|
|
784
|
-
const result = {
|
|
785
|
-
|
|
760
|
+
const result = {
|
|
761
|
+
rootDir: this._config.workspaceFolder,
|
|
762
|
+
projects: []
|
|
763
|
+
};
|
|
764
|
+
const { report, status } = await connection.listFiles({});
|
|
786
765
|
const workspaceFolder = this._config.workspaceFolder;
|
|
787
|
-
processReport(
|
|
788
|
-
onConfigure: (_config) => {
|
|
766
|
+
processReport(createTestListReceiver({
|
|
767
|
+
onConfigure: (_config) => {
|
|
768
|
+
result.rootDir = _config.rootDir;
|
|
769
|
+
},
|
|
789
770
|
onBegin(rootSuite) {
|
|
790
771
|
for (const projectSuite of rootSuite.suites) {
|
|
791
772
|
const project = projectSuite.project();
|
|
@@ -797,24 +778,31 @@ var PlaywrightTestServer = class {
|
|
|
797
778
|
use: project.use || {},
|
|
798
779
|
files
|
|
799
780
|
});
|
|
800
|
-
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, "/"));
|
|
801
782
|
}
|
|
802
783
|
},
|
|
803
784
|
onError(error) {
|
|
804
785
|
result.error = error;
|
|
805
786
|
}
|
|
806
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}`);
|
|
807
790
|
return result;
|
|
808
791
|
}
|
|
809
792
|
/**
|
|
810
793
|
* Get raw test report without processing through TeleReporterReceiver
|
|
811
794
|
* This preserves the complete test.describe hierarchy
|
|
812
795
|
*/
|
|
813
|
-
async getRawTestReport(locations, token) {
|
|
796
|
+
async getRawTestReport(locations, token, projects) {
|
|
814
797
|
if (token?.isCancellationRequested) return [];
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
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
|
+
}
|
|
818
806
|
return report;
|
|
819
807
|
}
|
|
820
808
|
async listTests(locations, reporter, token) {
|
|
@@ -912,206 +900,382 @@ var PlaywrightTestServer = class {
|
|
|
912
900
|
this._disposeTestServer();
|
|
913
901
|
}
|
|
914
902
|
};
|
|
915
|
-
|
|
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
|
+
}
|
|
916
1008
|
//#endregion
|
|
917
1009
|
//#region src/api.ts
|
|
918
|
-
|
|
919
|
-
|
|
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([
|
|
920
1033
|
workspaceFolder,
|
|
921
|
-
|
|
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: []
|
|
922
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`;
|
|
923
1090
|
try {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const jsonString = JSON.stringify(output, null, 2);
|
|
929
|
-
if (outputFile) {
|
|
930
|
-
await fs.writeFile(outputFile, jsonString, "utf-8");
|
|
931
|
-
console.log(`Results saved to: ${outputFile}`);
|
|
932
|
-
} else console.log(jsonString);
|
|
933
|
-
testServer.close();
|
|
934
|
-
} catch (error) {
|
|
935
|
-
console.error("Error:", error);
|
|
936
|
-
testServer.close();
|
|
1091
|
+
await fs.writeFile(temporary, value);
|
|
1092
|
+
await fs.rename(temporary, file);
|
|
1093
|
+
} finally {
|
|
1094
|
+
await fs.unlink(temporary).catch(() => {});
|
|
937
1095
|
}
|
|
938
1096
|
}
|
|
939
|
-
async function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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)
|
|
943
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);
|
|
944
1187
|
try {
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const testDirRelative = project.testDir || "";
|
|
955
|
-
for (const file of project.files) {
|
|
956
|
-
const fullPath = testDirRelative ? path.join(testDirRelative, file).replace(/\\/g, "/") : file.replace(/\\/g, "/");
|
|
957
|
-
if (fullPath.startsWith(normalizedInputPath + "/") || fullPath === normalizedInputPath) expandedPaths.push(fullPath);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
} else expandedPaths.push(normalizedInputPath);
|
|
961
|
-
} catch {
|
|
962
|
-
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);
|
|
963
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);
|
|
964
1202
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
type: "test",
|
|
975
|
-
title: entry.title,
|
|
976
|
-
testId: entry.testId,
|
|
977
|
-
location: {
|
|
978
|
-
file: filePath,
|
|
979
|
-
line: entry.location.line,
|
|
980
|
-
column: entry.location.column
|
|
981
|
-
},
|
|
982
|
-
tags: entry.tags || [],
|
|
983
|
-
annotations: entry.annotations || [],
|
|
984
|
-
expectedStatus: "passed",
|
|
985
|
-
timeout: 0
|
|
986
|
-
};
|
|
987
|
-
} else {
|
|
988
|
-
const filePath = entry.location && testDir ? path.join(testDir, entry.location.file).replace(/\\/g, "/") : entry.location?.file.replace(/\\/g, "/");
|
|
989
|
-
const node = {
|
|
990
|
-
type: "suite",
|
|
991
|
-
title: entry.title,
|
|
992
|
-
file: filePath,
|
|
993
|
-
children: []
|
|
994
|
-
};
|
|
995
|
-
if (entry.entries && Array.isArray(entry.entries)) for (const childEntry of entry.entries) node.children.push(parseEntry(childEntry, testDir));
|
|
996
|
-
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);
|
|
997
1212
|
}
|
|
1213
|
+
updatedByProject.set(project.title, files);
|
|
998
1214
|
}
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const project = message.params.project;
|
|
1006
|
-
const projectNode = {
|
|
1007
|
-
type: "suite",
|
|
1008
|
-
title: project.name,
|
|
1009
|
-
children: []
|
|
1010
|
-
};
|
|
1011
|
-
let testDir = "";
|
|
1012
|
-
if (project.testDir) testDir = path.isAbsolute(project.testDir) ? path.relative(workspaceFolder, project.testDir).replace(/\\/g, "/") : project.testDir.replace(/\\/g, "/");
|
|
1013
|
-
if (project.suites && Array.isArray(project.suites)) for (const suite of project.suites) {
|
|
1014
|
-
const suiteNode = parseEntry(suite, testDir);
|
|
1015
|
-
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));
|
|
1016
1221
|
}
|
|
1017
|
-
|
|
1222
|
+
updatedByProject.set(project.name, files);
|
|
1018
1223
|
}
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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();
|
|
1028
1237
|
}
|
|
1029
1238
|
}
|
|
1030
|
-
async function
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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);
|
|
1035
1248
|
try {
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const filePath = entry.location.file.replace(/\\/g, "/");
|
|
1058
|
-
return {
|
|
1059
|
-
type: "test",
|
|
1060
|
-
title: entry.title,
|
|
1061
|
-
testId: entry.testId,
|
|
1062
|
-
location: {
|
|
1063
|
-
file: filePath,
|
|
1064
|
-
line: entry.location.line,
|
|
1065
|
-
column: entry.location.column
|
|
1066
|
-
},
|
|
1067
|
-
tags: entry.tags || [],
|
|
1068
|
-
annotations: entry.annotations || [],
|
|
1069
|
-
expectedStatus: "passed",
|
|
1070
|
-
timeout: 0
|
|
1071
|
-
};
|
|
1072
|
-
} else {
|
|
1073
|
-
const filePath = entry.location?.file.replace(/\\/g, "/");
|
|
1074
|
-
const node = {
|
|
1075
|
-
type: "suite",
|
|
1076
|
-
title: entry.title,
|
|
1077
|
-
file: filePath,
|
|
1078
|
-
children: []
|
|
1079
|
-
};
|
|
1080
|
-
if (entry.entries && Array.isArray(entry.entries)) for (const childEntry of entry.entries) node.children.push(parseEntry(childEntry, testDir));
|
|
1081
|
-
return node;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
const rootNode = {
|
|
1085
|
-
type: "suite",
|
|
1086
|
-
title: "Root",
|
|
1087
|
-
children: []
|
|
1088
|
-
};
|
|
1089
|
-
for (const message of report) if (message.method === "onProject") {
|
|
1090
|
-
const project = message.params.project;
|
|
1091
|
-
const projectNode = {
|
|
1092
|
-
type: "suite",
|
|
1093
|
-
title: project.name,
|
|
1094
|
-
children: []
|
|
1095
|
-
};
|
|
1096
|
-
let testDir = "";
|
|
1097
|
-
if (project.testDir) testDir = path.isAbsolute(project.testDir) ? path.relative(workspaceFolder, project.testDir).replace(/\\/g, "/") : project.testDir.replace(/\\/g, "/");
|
|
1098
|
-
if (project.suites && Array.isArray(project.suites)) for (const suite of project.suites) {
|
|
1099
|
-
const suiteNode = parseEntry(suite, testDir);
|
|
1100
|
-
projectNode.children.push(suiteNode);
|
|
1101
|
-
}
|
|
1102
|
-
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);
|
|
1103
1270
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
} else console.log(jsonString);
|
|
1109
|
-
testServer.close();
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
console.error("Error:", error);
|
|
1112
|
-
testServer.close();
|
|
1271
|
+
if (!expandedPaths.size) return;
|
|
1272
|
+
await outputJson(consumeTestReport(await server.getRawTestReport([...expandedPaths])), outputFile);
|
|
1273
|
+
} finally {
|
|
1274
|
+
server.close();
|
|
1113
1275
|
}
|
|
1114
1276
|
}
|
|
1115
|
-
|
|
1277
|
+
async function treeStructure(workspaceFolder = process.cwd(), configFile = "playwright.config.ts", cancellationToken, outputFile) {
|
|
1278
|
+
await outputJson(await collectFullTree(workspaceFolder, configFile, cancellationToken), outputFile);
|
|
1279
|
+
}
|
|
1116
1280
|
//#endregion
|
|
1117
|
-
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,10 +1,9 @@
|
|
|
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";
|
|
6
6
|
import ora from "ora";
|
|
7
|
-
|
|
8
7
|
//#region src/cli.ts
|
|
9
8
|
/**
|
|
10
9
|
* Playwright Test Explorer CLI
|
|
@@ -41,8 +40,8 @@ program.command("list-tests").description("List all test cases in specified file
|
|
|
41
40
|
process.exit(1);
|
|
42
41
|
}
|
|
43
42
|
});
|
|
44
|
-
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) => {
|
|
45
|
-
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();
|
|
46
45
|
try {
|
|
47
46
|
const workspaceFolder = path.resolve(options.workspace);
|
|
48
47
|
const token = new CancellationToken();
|
|
@@ -52,8 +51,15 @@ program.command("tree").description("Display test structure as a tree").option("
|
|
|
52
51
|
process.exit(0);
|
|
53
52
|
});
|
|
54
53
|
const outputFile = options.output ? path.resolve(options.output) : void 0;
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
57
63
|
} catch (error) {
|
|
58
64
|
spinner.fail(chalk.red("Failed to build test tree"));
|
|
59
65
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
@@ -90,10 +96,12 @@ ${chalk.bold("Examples:")}
|
|
|
90
96
|
${chalk.cyan("# Save tree structure to JSON file")}
|
|
91
97
|
$ pw-test-explorer tree -o tree.json
|
|
92
98
|
|
|
99
|
+
${chalk.cyan("# Incrementally update a saved tree from Git changes")}
|
|
100
|
+
$ pw-test-explorer tree --incremental -o tree.json
|
|
101
|
+
|
|
93
102
|
${chalk.cyan("# Display tree for specific workspace")}
|
|
94
103
|
$ pw-test-explorer tree -w /path/to/project
|
|
95
104
|
`);
|
|
96
105
|
program.parse();
|
|
97
|
-
|
|
98
106
|
//#endregion
|
|
99
|
-
export {
|
|
107
|
+
export {};
|
package/dist/index.d.mts
CHANGED
|
@@ -25,7 +25,6 @@ interface Event<T> {
|
|
|
25
25
|
}
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/playwrightTestServer.d.ts
|
|
28
|
-
|
|
29
28
|
declare class CancellationToken {
|
|
30
29
|
private _isCancelled;
|
|
31
30
|
private _onCancelEmitter;
|
|
@@ -35,8 +34,19 @@ declare class CancellationToken {
|
|
|
35
34
|
}
|
|
36
35
|
//#endregion
|
|
37
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>;
|
|
38
48
|
declare function listTestFile(workspaceFolder?: string, configFile?: string, outputFile?: string): Promise<void>;
|
|
39
49
|
declare function listTestCase(workspaceFolder: string | undefined, configFile: string | undefined, relativePaths: string[], outputFile?: string): Promise<void>;
|
|
40
50
|
declare function treeStructure(workspaceFolder?: string, configFile?: string, cancellationToken?: CancellationToken, outputFile?: string): Promise<void>;
|
|
41
51
|
//#endregion
|
|
42
|
-
export { listTestCase, listTestFile, treeStructure };
|
|
52
|
+
export { type IncrementalTreeOptions, listTestCase, listTestFile, treeStructure, updateTreeStructure };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { n as listTestFile, r as treeStructure, t as listTestCase } from "./api-
|
|
2
|
-
|
|
3
|
-
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 };
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-test-explorer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "A CLI tool and library for exploring Playwright tests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/li-zhixin/playwright-test-explorer#readme",
|
|
8
8
|
"bugs": {
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/li-zhixin/playwright-test-explorer/issues"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/
|
|
13
|
+
"url": "git+https://github.com/li-zhixin/playwright-test-explorer.git"
|
|
14
14
|
},
|
|
15
15
|
"author": "Author Name <author.name@mail.com>",
|
|
16
16
|
"files": [
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
|
-
"commander": "^14.0.
|
|
46
|
-
"ora": "^9.
|
|
47
|
-
"ws": "^8.
|
|
45
|
+
"commander": "^14.0.3",
|
|
46
|
+
"ora": "^9.3.0",
|
|
47
|
+
"ws": "^8.20.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@playwright/test": "^1.
|
|
51
|
-
"@types/node": "^
|
|
50
|
+
"@playwright/test": "^1.58.2",
|
|
51
|
+
"@types/node": "^25.5.0",
|
|
52
52
|
"@types/ws": "^8.18.1",
|
|
53
|
-
"@vitest/coverage-v8": "4.
|
|
54
|
-
"bumpp": "^
|
|
55
|
-
"tsdown": "^0.
|
|
56
|
-
"typescript": "^
|
|
57
|
-
"vitest": "^4.
|
|
53
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
54
|
+
"bumpp": "^11.0.1",
|
|
55
|
+
"tsdown": "^0.21.7",
|
|
56
|
+
"typescript": "^6.0.2",
|
|
57
|
+
"vitest": "^4.1.2"
|
|
58
58
|
}
|
|
59
59
|
}
|