@empiricalrun/test-run 0.1.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.
- package/CHANGELOG.md +7 -0
- package/README.md +21 -0
- package/dist/bin/index.d.ts +3 -0
- package/dist/bin/index.d.ts.map +1 -0
- package/dist/bin/index.js +39 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +155 -0
- package/package.json +34 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# test-run
|
|
2
|
+
|
|
3
|
+
Library to run playwright tests.
|
|
4
|
+
|
|
5
|
+
This package helps to run all the dependent tests for a particular test case.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
npx @empiricalrun/test-run -n "<test-name>" "<playwright-options>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The playwright options are passed as is to the playwright test command.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
npx @empiricalrun/test-run -n "foo" --retries 0
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This will run the test named "foo" in the `tests` directory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const __1 = require("..");
|
|
6
|
+
(async function main() {
|
|
7
|
+
// TODO: add support for suites
|
|
8
|
+
commander_1.program
|
|
9
|
+
.option("-n, --name <test-name>", "Name of the test to run")
|
|
10
|
+
.option("-d, --dir <test-dir>", "Path to the test directory")
|
|
11
|
+
.allowUnknownOption();
|
|
12
|
+
commander_1.program.parse(process.argv);
|
|
13
|
+
// Accessing the extracted parameters
|
|
14
|
+
const options = commander_1.program.opts();
|
|
15
|
+
if (!options.name) {
|
|
16
|
+
console.error("Please provide a test name");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const optionsToStrip = [
|
|
20
|
+
"-n",
|
|
21
|
+
"--name",
|
|
22
|
+
"-d",
|
|
23
|
+
"--dir",
|
|
24
|
+
options.name,
|
|
25
|
+
options.dir,
|
|
26
|
+
];
|
|
27
|
+
const pwOptions = process.argv
|
|
28
|
+
.slice(2)
|
|
29
|
+
.filter((arg) => !optionsToStrip.includes(arg));
|
|
30
|
+
if (pwOptions.includes("--forbid-only")) {
|
|
31
|
+
console.error("forbid-only option is not supported");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
await (0, __1.runTest)({
|
|
35
|
+
name: options.name,
|
|
36
|
+
dir: options.dir || "tests",
|
|
37
|
+
pwOptions: pwOptions.join(" "),
|
|
38
|
+
});
|
|
39
|
+
})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAW5C;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,iBAAiB,iBAoDxE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runTest = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @export
|
|
12
|
+
* @param {TestRunParameters} { name, dir, pwOptions }
|
|
13
|
+
*/
|
|
14
|
+
async function runTest({ name, dir, pwOptions }) {
|
|
15
|
+
console.log("getting all files in directory:", dir);
|
|
16
|
+
const files = await (0, utils_1.getAllFilePaths)(dir);
|
|
17
|
+
let matchingFilePath = "";
|
|
18
|
+
// find the first file that contains the test block
|
|
19
|
+
// TODO: add suites support
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const match = await (0, utils_1.hasTestBlock)({ filePath: file, scenarioName: name });
|
|
22
|
+
if (match) {
|
|
23
|
+
matchingFilePath = file;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!matchingFilePath) {
|
|
28
|
+
console.error("No test block found for the given test name");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const testNode = await (0, utils_1.getTestCaseNode)({
|
|
32
|
+
filePath: matchingFilePath,
|
|
33
|
+
scenarioName: name,
|
|
34
|
+
});
|
|
35
|
+
const parentDescribe = (0, utils_1.findFirstSerialDescribeBlock)(testNode);
|
|
36
|
+
const isFileMarkedSerial = await (0, utils_1.hasTopLevelDescribeConfigureWithSerialMode)(matchingFilePath);
|
|
37
|
+
console.log("Identified test block:", !!testNode);
|
|
38
|
+
console.log("Is parent describe block marked serial:", !!parentDescribe);
|
|
39
|
+
console.log("Is file marked serial:", isFileMarkedSerial);
|
|
40
|
+
const currentFileContent = await fs_extra_1.default.readFile(matchingFilePath, "utf-8");
|
|
41
|
+
// if the file is not marked serial, we need to mark the test or describe block as only
|
|
42
|
+
if (!isFileMarkedSerial) {
|
|
43
|
+
await (0, utils_1.markTestAsOnly)(matchingFilePath, testNode.getText(), parentDescribe?.getText() || "");
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const env = Object({ ...process.env });
|
|
47
|
+
const pwRunCmd = `npx playwright test ${matchingFilePath} ${pwOptions}`;
|
|
48
|
+
console.log("Playwright test command:", pwRunCmd);
|
|
49
|
+
await (0, utils_1.cmd)(pwRunCmd.split(" "), {
|
|
50
|
+
env,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
console.error("Error executing playwright test", e);
|
|
55
|
+
}
|
|
56
|
+
// revert the changes made to the file to mark tests as only
|
|
57
|
+
await fs_extra_1.default.writeFile(matchingFilePath, currentFileContent);
|
|
58
|
+
if (!matchingFilePath) {
|
|
59
|
+
console.error("No test block found for the given test name");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.runTest = runTest;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Node } from "ts-morph";
|
|
2
|
+
export declare function cmd(command: string[], options: {
|
|
3
|
+
env?: Record<string, string>;
|
|
4
|
+
}): Promise<number>;
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
* @param {string} [directoryPath=""]
|
|
9
|
+
* @return {*} {Promise<string[]>}
|
|
10
|
+
*/
|
|
11
|
+
export declare function getAllFilePaths(directoryPath?: string): Promise<string[]>;
|
|
12
|
+
export declare function getTestCaseNode({ filePath, scenarioName, }: {
|
|
13
|
+
filePath: string;
|
|
14
|
+
scenarioName: string;
|
|
15
|
+
}): Promise<Node | undefined>;
|
|
16
|
+
export declare function hasTestBlock({ filePath, scenarioName, }: {
|
|
17
|
+
filePath: string;
|
|
18
|
+
scenarioName: string;
|
|
19
|
+
}): Promise<boolean>;
|
|
20
|
+
export declare function findFirstSerialDescribeBlock(node: Node | undefined): Node | undefined;
|
|
21
|
+
export declare function hasTopLevelDescribeConfigureWithSerialMode(filePath: string): Promise<boolean>;
|
|
22
|
+
export declare function markTestAsOnly(filePath: string, testBlock: string, parentDescribeBlock?: string): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAuB,MAAM,UAAU,CAAC;AAErD,wBAAgB,GAAG,CACjB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,CAAC,CA2BjB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,aAAa,GAAE,MAAW,GACzB,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBnB;AAED,wBAAsB,eAAe,CAAC,EACpC,QAAQ,EACR,YAAY,GACb,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAc5B;AAED,wBAAsB,YAAY,CAAC,EACjC,QAAQ,EACR,YAAY,GACb,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,OAAO,CAAC,CAMnB;AAED,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,IAAI,GAAG,SAAS,GACrB,IAAI,GAAG,SAAS,CA2BlB;AAED,wBAAsB,0CAA0C,CAC9D,QAAQ,EAAE,MAAM,oBA+BjB;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,mBAAmB,CAAC,EAAE,MAAM,iBAkB7B"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.markTestAsOnly = exports.hasTopLevelDescribeConfigureWithSerialMode = exports.findFirstSerialDescribeBlock = exports.hasTestBlock = exports.getTestCaseNode = exports.getAllFilePaths = exports.cmd = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const ts_morph_1 = require("ts-morph");
|
|
11
|
+
function cmd(command, options) {
|
|
12
|
+
let errorLogs = [];
|
|
13
|
+
return new Promise((resolveFunc, rejectFunc) => {
|
|
14
|
+
let p = (0, child_process_1.spawn)(command[0], command.slice(1), {
|
|
15
|
+
env: { ...process.env, ...options.env },
|
|
16
|
+
});
|
|
17
|
+
p.stdout.on("data", (x) => {
|
|
18
|
+
const log = x.toString();
|
|
19
|
+
if (log.includes("Error")) {
|
|
20
|
+
errorLogs.push(log);
|
|
21
|
+
}
|
|
22
|
+
process.stdout.write(log);
|
|
23
|
+
});
|
|
24
|
+
p.stderr.on("data", (x) => {
|
|
25
|
+
const log = x.toString();
|
|
26
|
+
process.stderr.write(x.toString());
|
|
27
|
+
errorLogs.push(log);
|
|
28
|
+
});
|
|
29
|
+
p.on("exit", (code) => {
|
|
30
|
+
if (code != 0) {
|
|
31
|
+
// assuming last log is the error message before exiting
|
|
32
|
+
rejectFunc(errorLogs.slice(-3).join("\n"));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
resolveFunc(code);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
exports.cmd = cmd;
|
|
41
|
+
/**
|
|
42
|
+
*
|
|
43
|
+
*
|
|
44
|
+
* @param {string} [directoryPath=""]
|
|
45
|
+
* @return {*} {Promise<string[]>}
|
|
46
|
+
*/
|
|
47
|
+
async function getAllFilePaths(directoryPath = "") {
|
|
48
|
+
let filePaths = [];
|
|
49
|
+
try {
|
|
50
|
+
const files = await fs_extra_1.default.readdir(directoryPath);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const filePath = path_1.default.join(directoryPath, file);
|
|
53
|
+
const stat = await fs_extra_1.default.lstat(filePath);
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
// If it's a directory, recursively get file paths from the directory
|
|
56
|
+
const nestedFiles = await getAllFilePaths(filePath);
|
|
57
|
+
filePaths = filePaths.concat(nestedFiles);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// If it's a file, push its path to the array
|
|
61
|
+
filePaths.push(filePath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error("Error reading directory:", err);
|
|
67
|
+
}
|
|
68
|
+
return filePaths;
|
|
69
|
+
}
|
|
70
|
+
exports.getAllFilePaths = getAllFilePaths;
|
|
71
|
+
async function getTestCaseNode({ filePath, scenarioName, }) {
|
|
72
|
+
const project = new ts_morph_1.Project();
|
|
73
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
74
|
+
const sourceFile = project.createSourceFile("test.ts", content);
|
|
75
|
+
sourceFile.getDescendants;
|
|
76
|
+
const testFunctionNode = sourceFile.getFirstDescendant((node) => !!(node.isKind(ts_morph_1.SyntaxKind.CallExpression) &&
|
|
77
|
+
node.getExpression().getText() === "test" &&
|
|
78
|
+
node.getArguments()[0]?.getText().includes(scenarioName)));
|
|
79
|
+
return testFunctionNode;
|
|
80
|
+
}
|
|
81
|
+
exports.getTestCaseNode = getTestCaseNode;
|
|
82
|
+
async function hasTestBlock({ filePath, scenarioName, }) {
|
|
83
|
+
const testNode = await getTestCaseNode({
|
|
84
|
+
filePath,
|
|
85
|
+
scenarioName,
|
|
86
|
+
});
|
|
87
|
+
return !!testNode;
|
|
88
|
+
}
|
|
89
|
+
exports.hasTestBlock = hasTestBlock;
|
|
90
|
+
function findFirstSerialDescribeBlock(node) {
|
|
91
|
+
let currentNode = node;
|
|
92
|
+
// Traverse upwards until we find a 'describe' block with 'serial: true'
|
|
93
|
+
while (currentNode) {
|
|
94
|
+
const parentDescribe = currentNode.getFirstAncestorByKind(ts_morph_1.SyntaxKind.CallExpression);
|
|
95
|
+
if (parentDescribe) {
|
|
96
|
+
const isDescribe = parentDescribe.getFirstChild()?.getText() === "test.describe";
|
|
97
|
+
if (isDescribe) {
|
|
98
|
+
const configureCall = parentDescribe
|
|
99
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
|
|
100
|
+
.find((call) => call.getText().includes("configure"));
|
|
101
|
+
if (configureCall) {
|
|
102
|
+
// Check if 'serial: true' exists
|
|
103
|
+
if (configureCall.getText().includes("serial")) {
|
|
104
|
+
return parentDescribe;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
currentNode = parentDescribe; // Move up the tree
|
|
110
|
+
}
|
|
111
|
+
return undefined; // Return undefined if no 'describe' with serial: true is found
|
|
112
|
+
}
|
|
113
|
+
exports.findFirstSerialDescribeBlock = findFirstSerialDescribeBlock;
|
|
114
|
+
async function hasTopLevelDescribeConfigureWithSerialMode(filePath) {
|
|
115
|
+
const project = new ts_morph_1.Project();
|
|
116
|
+
const content = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
117
|
+
const sourceFile = project.createSourceFile("test.ts", content);
|
|
118
|
+
const statements = sourceFile.getStatements();
|
|
119
|
+
for (const statement of statements) {
|
|
120
|
+
// Check if the statement is an expression statement
|
|
121
|
+
if (statement.getKind() === ts_morph_1.SyntaxKind.ExpressionStatement) {
|
|
122
|
+
const expression = statement.getFirstChildByKind(ts_morph_1.SyntaxKind.CallExpression);
|
|
123
|
+
if (expression) {
|
|
124
|
+
const expressionText = expression.getExpression().getText();
|
|
125
|
+
// Check if it's `test.describe.configure`
|
|
126
|
+
if (expressionText === "test.describe.configure") {
|
|
127
|
+
// Check if arguments exist and if the first argument is an object literal with mode serial
|
|
128
|
+
const args = expression.getArguments();
|
|
129
|
+
const firstArgText = args[0]?.getText();
|
|
130
|
+
if (firstArgText &&
|
|
131
|
+
firstArgText.includes("serial") &&
|
|
132
|
+
firstArgText.includes("mode")) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
exports.hasTopLevelDescribeConfigureWithSerialMode = hasTopLevelDescribeConfigureWithSerialMode;
|
|
142
|
+
async function markTestAsOnly(filePath, testBlock, parentDescribeBlock) {
|
|
143
|
+
const fileContent = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
144
|
+
let updatedTestFileContent = fileContent;
|
|
145
|
+
if (!parentDescribeBlock) {
|
|
146
|
+
const updatedTestBlock = testBlock.replace("test(", "test.only(");
|
|
147
|
+
updatedTestFileContent = fileContent.replace(testBlock, updatedTestBlock);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const describeMarkedAsOnly = parentDescribeBlock.replace("test.describe(", "test.describe.only(");
|
|
151
|
+
updatedTestFileContent = fileContent.replace(parentDescribeBlock, describeMarkedAsOnly);
|
|
152
|
+
}
|
|
153
|
+
await fs_extra_1.default.writeFile(filePath, updatedTestFileContent);
|
|
154
|
+
}
|
|
155
|
+
exports.markTestAsOnly = markTestAsOnly;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@empiricalrun/test-run",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"registry": "https://registry.npmjs.org/",
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"@empiricalrun/test-run": "dist/bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/empirical-run/empirical.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "Empirical Team <hey@empirical.run>",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^12.1.0",
|
|
19
|
+
"fs-extra": "^11.2.0",
|
|
20
|
+
"ts-morph": "^23.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/fs-extra": "^11.0.4",
|
|
24
|
+
"@types/node": "^22.5.5"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsc --build --watch",
|
|
28
|
+
"build": "tsc --build",
|
|
29
|
+
"clean": "tsc --build --clean",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest"
|
|
33
|
+
}
|
|
34
|
+
}
|