testchimp-runner-core 0.0.87 → 0.0.89
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/dist/execution-service.d.ts +10 -7
- package/dist/execution-service.d.ts.map +1 -1
- package/dist/execution-service.js +399 -94
- package/dist/execution-service.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/progress-reporter.d.ts +28 -0
- package/dist/progress-reporter.d.ts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/test-file-parser.d.ts +101 -0
- package/dist/utils/test-file-parser.d.ts.map +1 -0
- package/dist/utils/test-file-parser.js +702 -0
- package/dist/utils/test-file-parser.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for parsing test files to extract hooks and tests
|
|
4
|
+
* Supports Playwright hooks: test.beforeAll, test.afterAll, test.beforeEach, test.afterEach
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.TestFileParser = void 0;
|
|
44
|
+
const parser_1 = require("@babel/parser");
|
|
45
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
46
|
+
const t = __importStar(require("@babel/types"));
|
|
47
|
+
const generator_1 = __importDefault(require("@babel/generator"));
|
|
48
|
+
class TestFileParser {
|
|
49
|
+
/**
|
|
50
|
+
* Generate full test name with suite prefix
|
|
51
|
+
* @param suitePath - Array of suite names from root to current suite
|
|
52
|
+
* @param testName - Original test name
|
|
53
|
+
* @returns Full test name with suite prefix (e.g., "LoginSuite__testLogin")
|
|
54
|
+
*/
|
|
55
|
+
static generateTestFullName(suitePath, testName) {
|
|
56
|
+
if (suitePath.length === 0) {
|
|
57
|
+
return testName;
|
|
58
|
+
}
|
|
59
|
+
return `${suitePath.join('__')}__${testName}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Find all parent describe blocks for a given AST path
|
|
63
|
+
* @param path - Babel AST path
|
|
64
|
+
* @returns Array of suite names from root to current (empty if not in any describe block)
|
|
65
|
+
*/
|
|
66
|
+
static findSuitePath(path) {
|
|
67
|
+
const suitePath = [];
|
|
68
|
+
let currentPath = path.parentPath;
|
|
69
|
+
while (currentPath) {
|
|
70
|
+
// Check if current path is a test.describe() call
|
|
71
|
+
if (currentPath.isCallExpression()) {
|
|
72
|
+
const callee = currentPath.node.callee;
|
|
73
|
+
if (t.isMemberExpression(callee)) {
|
|
74
|
+
const object = callee.object;
|
|
75
|
+
const property = callee.property;
|
|
76
|
+
if (t.isIdentifier(object) && object.name === 'test' &&
|
|
77
|
+
t.isIdentifier(property) && property.name === 'describe') {
|
|
78
|
+
// Extract suite name from first argument
|
|
79
|
+
if (currentPath.node.arguments.length >= 1) {
|
|
80
|
+
const suiteNameArg = currentPath.node.arguments[0];
|
|
81
|
+
let suiteName = null;
|
|
82
|
+
if (t.isStringLiteral(suiteNameArg)) {
|
|
83
|
+
suiteName = suiteNameArg.value;
|
|
84
|
+
}
|
|
85
|
+
else if (t.isTemplateLiteral(suiteNameArg)) {
|
|
86
|
+
if (suiteNameArg.quasis.length > 0) {
|
|
87
|
+
suiteName = suiteNameArg.quasis[0].value.raw;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (suiteName) {
|
|
91
|
+
suitePath.unshift(suiteName); // Add to beginning (root first)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
currentPath = currentPath.parentPath;
|
|
98
|
+
}
|
|
99
|
+
return suitePath;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse a test file to extract hooks and tests, supporting test.describe() blocks
|
|
103
|
+
* @param script - The test file content
|
|
104
|
+
* @param testNames - Optional array of test names to filter (supports both original names and full names with suite prefix)
|
|
105
|
+
* @returns Parsed structure with hooks and tests
|
|
106
|
+
*/
|
|
107
|
+
static parseTestFile(script, testNames) {
|
|
108
|
+
const result = {
|
|
109
|
+
fileHooks: {
|
|
110
|
+
beforeAll: [],
|
|
111
|
+
afterAll: [],
|
|
112
|
+
beforeEach: [],
|
|
113
|
+
afterEach: []
|
|
114
|
+
},
|
|
115
|
+
tests: [],
|
|
116
|
+
suites: []
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
// Parse the script with Babel
|
|
120
|
+
const ast = (0, parser_1.parse)(script, {
|
|
121
|
+
sourceType: 'module',
|
|
122
|
+
plugins: ['typescript', 'classProperties', 'decorators-legacy'],
|
|
123
|
+
allowImportExportEverywhere: true
|
|
124
|
+
});
|
|
125
|
+
// Track if we should filter tests
|
|
126
|
+
const shouldFilter = testNames && testNames.length > 0;
|
|
127
|
+
const testNameSet = shouldFilter ? new Set(testNames.map(name => name.trim())) : null;
|
|
128
|
+
// Map to store suites by their path (for nested suite tracking)
|
|
129
|
+
const suiteMap = new Map();
|
|
130
|
+
/**
|
|
131
|
+
* Recursively parse a suite (describe block) and its nested suites
|
|
132
|
+
*/
|
|
133
|
+
const parseSuite = (describePath, parentSuitePath) => {
|
|
134
|
+
if (!describePath.isCallExpression()) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const callee = describePath.node.callee;
|
|
138
|
+
if (!t.isMemberExpression(callee)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const object = callee.object;
|
|
142
|
+
const property = callee.property;
|
|
143
|
+
if (!t.isIdentifier(object) || object.name !== 'test' ||
|
|
144
|
+
!t.isIdentifier(property) || property.name !== 'describe') {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
// Extract suite name
|
|
148
|
+
if (describePath.node.arguments.length < 2) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const suiteNameArg = describePath.node.arguments[0];
|
|
152
|
+
let suiteName = null;
|
|
153
|
+
if (t.isStringLiteral(suiteNameArg)) {
|
|
154
|
+
suiteName = suiteNameArg.value;
|
|
155
|
+
}
|
|
156
|
+
else if (t.isTemplateLiteral(suiteNameArg)) {
|
|
157
|
+
if (suiteNameArg.quasis.length > 0) {
|
|
158
|
+
suiteName = suiteNameArg.quasis[0].value.raw;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!suiteName) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// Build current suite path
|
|
165
|
+
const currentSuitePath = [...parentSuitePath, suiteName];
|
|
166
|
+
const suitePathKey = currentSuitePath.join('__');
|
|
167
|
+
// Check if suite already exists (shouldn't happen, but safety check)
|
|
168
|
+
if (suiteMap.has(suitePathKey)) {
|
|
169
|
+
return suiteMap.get(suitePathKey);
|
|
170
|
+
}
|
|
171
|
+
// Create new suite
|
|
172
|
+
const suite = {
|
|
173
|
+
name: suiteName,
|
|
174
|
+
suitePath: currentSuitePath,
|
|
175
|
+
beforeAll: [],
|
|
176
|
+
afterAll: [],
|
|
177
|
+
beforeEach: [],
|
|
178
|
+
afterEach: [],
|
|
179
|
+
tests: [],
|
|
180
|
+
nestedSuites: []
|
|
181
|
+
};
|
|
182
|
+
suiteMap.set(suitePathKey, suite);
|
|
183
|
+
// Get callback function (second argument)
|
|
184
|
+
const callback = describePath.node.arguments[1];
|
|
185
|
+
if (!t.isFunctionExpression(callback) && !t.isArrowFunctionExpression(callback)) {
|
|
186
|
+
return suite;
|
|
187
|
+
}
|
|
188
|
+
// Traverse the callback body to find hooks, tests, and nested describe blocks
|
|
189
|
+
const callbackBody = callback.body;
|
|
190
|
+
if (!t.isBlockStatement(callbackBody)) {
|
|
191
|
+
return suite;
|
|
192
|
+
}
|
|
193
|
+
// Traverse the suite's callback body
|
|
194
|
+
// Traverse the block statement directly - Babel can handle this
|
|
195
|
+
(0, traverse_1.default)(callbackBody, {
|
|
196
|
+
CallExpression(path) {
|
|
197
|
+
const callee = path.node.callee;
|
|
198
|
+
// Check for test.beforeAll, test.afterAll, test.beforeEach, test.afterEach
|
|
199
|
+
if (t.isMemberExpression(callee)) {
|
|
200
|
+
const object = callee.object;
|
|
201
|
+
const property = callee.property;
|
|
202
|
+
if (t.isIdentifier(object) && object.name === 'test' && t.isIdentifier(property)) {
|
|
203
|
+
const hookName = property.name;
|
|
204
|
+
// Check if this is a hook
|
|
205
|
+
if (hookName === 'beforeAll' || hookName === 'afterAll' ||
|
|
206
|
+
hookName === 'beforeEach' || hookName === 'afterEach') {
|
|
207
|
+
// Get the callback function
|
|
208
|
+
if (path.node.arguments.length >= 1) {
|
|
209
|
+
const hookCallback = path.node.arguments[0];
|
|
210
|
+
if (t.isFunctionExpression(hookCallback) || t.isArrowFunctionExpression(hookCallback)) {
|
|
211
|
+
let hookBody;
|
|
212
|
+
if (t.isBlockStatement(hookCallback.body)) {
|
|
213
|
+
hookBody = (0, generator_1.default)(hookCallback.body, { comments: false }).code;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const exprCode = (0, generator_1.default)(hookCallback.body, { comments: false }).code;
|
|
217
|
+
hookBody = `{ return ${exprCode}; }`;
|
|
218
|
+
}
|
|
219
|
+
const hook = {
|
|
220
|
+
code: hookBody,
|
|
221
|
+
name: hookName,
|
|
222
|
+
suitePath: currentSuitePath,
|
|
223
|
+
scope: 'suite'
|
|
224
|
+
};
|
|
225
|
+
// Add to suite's appropriate array
|
|
226
|
+
if (hookName === 'beforeAll') {
|
|
227
|
+
suite.beforeAll.push(hook);
|
|
228
|
+
}
|
|
229
|
+
else if (hookName === 'afterAll') {
|
|
230
|
+
suite.afterAll.push(hook);
|
|
231
|
+
}
|
|
232
|
+
else if (hookName === 'beforeEach') {
|
|
233
|
+
suite.beforeEach.push(hook);
|
|
234
|
+
}
|
|
235
|
+
else if (hookName === 'afterEach') {
|
|
236
|
+
suite.afterEach.push(hook);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Check for test() calls
|
|
245
|
+
if (t.isIdentifier(callee) && callee.name === 'test') {
|
|
246
|
+
if (path.node.arguments.length >= 2) {
|
|
247
|
+
const testNameArg = path.node.arguments[0];
|
|
248
|
+
const testCallback = path.node.arguments[1];
|
|
249
|
+
// Extract test name
|
|
250
|
+
let testName = null;
|
|
251
|
+
if (t.isStringLiteral(testNameArg)) {
|
|
252
|
+
testName = testNameArg.value;
|
|
253
|
+
}
|
|
254
|
+
else if (t.isTemplateLiteral(testNameArg)) {
|
|
255
|
+
if (testNameArg.quasis.length > 0) {
|
|
256
|
+
testName = testNameArg.quasis[0].value.raw;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (testName) {
|
|
260
|
+
const fullName = TestFileParser.generateTestFullName(currentSuitePath, testName);
|
|
261
|
+
// Filter tests if testNames provided (support both original and full names)
|
|
262
|
+
if (shouldFilter && testNameSet) {
|
|
263
|
+
if (!testNameSet.has(testName) && !testNameSet.has(fullName)) {
|
|
264
|
+
return; // Skip this test
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Extract test body
|
|
268
|
+
if (t.isFunctionExpression(testCallback) || t.isArrowFunctionExpression(testCallback)) {
|
|
269
|
+
let testBody;
|
|
270
|
+
let statements;
|
|
271
|
+
if (t.isBlockStatement(testCallback.body)) {
|
|
272
|
+
testBody = (0, generator_1.default)(testCallback.body, { comments: false }).code;
|
|
273
|
+
// Extract ALL statements from BlockStatement.body (includes variables)
|
|
274
|
+
const bodyStatements = testCallback.body.body;
|
|
275
|
+
statements = bodyStatements.map((stmt, index) => {
|
|
276
|
+
const code = (0, generator_1.default)(stmt, { comments: false }).code;
|
|
277
|
+
const isVar = t.isVariableDeclaration(stmt);
|
|
278
|
+
// Extract intent comment (leading comment, exclude @Screen/@State)
|
|
279
|
+
let intentComment;
|
|
280
|
+
if (stmt.leadingComments && stmt.leadingComments.length > 0) {
|
|
281
|
+
for (const comment of stmt.leadingComments) {
|
|
282
|
+
const commentText = comment.value.trim();
|
|
283
|
+
if (!commentText.includes('@Screen') && !commentText.includes('@State')) {
|
|
284
|
+
intentComment = commentText;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Extract screen-state annotation (trailing or next statement's leading)
|
|
290
|
+
let screenStateAnnotation;
|
|
291
|
+
if (stmt.trailingComments && stmt.trailingComments.length > 0) {
|
|
292
|
+
for (const comment of stmt.trailingComments) {
|
|
293
|
+
const commentText = comment.value.trim();
|
|
294
|
+
if (commentText.includes('@Screen') || commentText.includes('@State')) {
|
|
295
|
+
screenStateAnnotation = commentText;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Fallback: check next statement's leading @Screen/@State comment
|
|
301
|
+
if (!screenStateAnnotation && index + 1 < bodyStatements.length) {
|
|
302
|
+
const nextStmt = bodyStatements[index + 1];
|
|
303
|
+
if (nextStmt.leadingComments && nextStmt.leadingComments.length > 0) {
|
|
304
|
+
for (const comment of nextStmt.leadingComments) {
|
|
305
|
+
const commentText = comment.value.trim();
|
|
306
|
+
if (commentText.includes('@Screen') || commentText.includes('@State')) {
|
|
307
|
+
screenStateAnnotation = commentText;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { code, isVariableDeclaration: isVar, intentComment, screenStateAnnotation };
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// Expression body - single statement
|
|
318
|
+
const exprCode = (0, generator_1.default)(testCallback.body, { comments: false }).code;
|
|
319
|
+
testBody = `{ return ${exprCode}; }`;
|
|
320
|
+
statements = [{
|
|
321
|
+
code: exprCode,
|
|
322
|
+
isVariableDeclaration: false
|
|
323
|
+
}];
|
|
324
|
+
}
|
|
325
|
+
suite.tests.push({
|
|
326
|
+
name: testName,
|
|
327
|
+
code: testBody,
|
|
328
|
+
suitePath: currentSuitePath,
|
|
329
|
+
fullName: fullName,
|
|
330
|
+
statements: statements
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Check for nested test.describe() calls
|
|
338
|
+
if (t.isMemberExpression(callee)) {
|
|
339
|
+
const object = callee.object;
|
|
340
|
+
const property = callee.property;
|
|
341
|
+
if (t.isIdentifier(object) && object.name === 'test' &&
|
|
342
|
+
t.isIdentifier(property) && property.name === 'describe') {
|
|
343
|
+
// This is a nested describe block - parse it recursively
|
|
344
|
+
const nestedSuite = parseSuite(path, currentSuitePath);
|
|
345
|
+
if (nestedSuite) {
|
|
346
|
+
suite.nestedSuites.push(nestedSuite);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
return suite;
|
|
354
|
+
};
|
|
355
|
+
// Traverse the AST to find top-level hooks, tests, and describe blocks
|
|
356
|
+
(0, traverse_1.default)(ast, {
|
|
357
|
+
CallExpression(path) {
|
|
358
|
+
const callee = path.node.callee;
|
|
359
|
+
// Check for test.describe() at top level
|
|
360
|
+
if (t.isMemberExpression(callee)) {
|
|
361
|
+
const object = callee.object;
|
|
362
|
+
const property = callee.property;
|
|
363
|
+
if (t.isIdentifier(object) && object.name === 'test' &&
|
|
364
|
+
t.isIdentifier(property) && property.name === 'describe') {
|
|
365
|
+
// Check if this is at the top level (not nested in another describe)
|
|
366
|
+
const suitePath = TestFileParser.findSuitePath(path);
|
|
367
|
+
if (suitePath.length === 0) {
|
|
368
|
+
// This is a top-level describe block
|
|
369
|
+
const suite = parseSuite(path, []);
|
|
370
|
+
if (suite) {
|
|
371
|
+
result.suites.push(suite);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Check for file-level hooks (test.beforeAll, etc.) - not inside any describe block
|
|
378
|
+
if (t.isMemberExpression(callee)) {
|
|
379
|
+
const object = callee.object;
|
|
380
|
+
const property = callee.property;
|
|
381
|
+
if (t.isIdentifier(object) && object.name === 'test' && t.isIdentifier(property)) {
|
|
382
|
+
const hookName = property.name;
|
|
383
|
+
if (hookName === 'beforeAll' || hookName === 'afterAll' ||
|
|
384
|
+
hookName === 'beforeEach' || hookName === 'afterEach') {
|
|
385
|
+
// Check if this hook is at file level (not inside any describe block)
|
|
386
|
+
const suitePath = TestFileParser.findSuitePath(path);
|
|
387
|
+
if (suitePath.length === 0) {
|
|
388
|
+
// This is a file-level hook
|
|
389
|
+
if (path.node.arguments.length >= 1) {
|
|
390
|
+
const callback = path.node.arguments[0];
|
|
391
|
+
if (t.isFunctionExpression(callback) || t.isArrowFunctionExpression(callback)) {
|
|
392
|
+
let hookBody;
|
|
393
|
+
if (t.isBlockStatement(callback.body)) {
|
|
394
|
+
hookBody = (0, generator_1.default)(callback.body, { comments: false }).code;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const exprCode = (0, generator_1.default)(callback.body, { comments: false }).code;
|
|
398
|
+
hookBody = `{ return ${exprCode}; }`;
|
|
399
|
+
}
|
|
400
|
+
const hook = {
|
|
401
|
+
code: hookBody,
|
|
402
|
+
name: hookName,
|
|
403
|
+
suitePath: [],
|
|
404
|
+
scope: 'file'
|
|
405
|
+
};
|
|
406
|
+
// Add to appropriate array
|
|
407
|
+
if (hookName === 'beforeAll') {
|
|
408
|
+
result.fileHooks.beforeAll.push(hook);
|
|
409
|
+
}
|
|
410
|
+
else if (hookName === 'afterAll') {
|
|
411
|
+
result.fileHooks.afterAll.push(hook);
|
|
412
|
+
}
|
|
413
|
+
else if (hookName === 'beforeEach') {
|
|
414
|
+
result.fileHooks.beforeEach.push(hook);
|
|
415
|
+
}
|
|
416
|
+
else if (hookName === 'afterEach') {
|
|
417
|
+
result.fileHooks.afterEach.push(hook);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Check for file-level test() calls (not inside any describe block)
|
|
427
|
+
if (t.isIdentifier(callee) && callee.name === 'test') {
|
|
428
|
+
// Check if this test is at file level
|
|
429
|
+
const suitePath = TestFileParser.findSuitePath(path);
|
|
430
|
+
if (suitePath.length === 0) {
|
|
431
|
+
// This is a file-level test
|
|
432
|
+
if (path.node.arguments.length >= 2) {
|
|
433
|
+
const testNameArg = path.node.arguments[0];
|
|
434
|
+
const testCallback = path.node.arguments[1];
|
|
435
|
+
// Extract test name
|
|
436
|
+
let testName = null;
|
|
437
|
+
if (t.isStringLiteral(testNameArg)) {
|
|
438
|
+
testName = testNameArg.value;
|
|
439
|
+
}
|
|
440
|
+
else if (t.isTemplateLiteral(testNameArg)) {
|
|
441
|
+
if (testNameArg.quasis.length > 0) {
|
|
442
|
+
testName = testNameArg.quasis[0].value.raw;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (testName) {
|
|
446
|
+
const fullName = TestFileParser.generateTestFullName([], testName);
|
|
447
|
+
// Filter tests if testNames provided
|
|
448
|
+
if (shouldFilter && testNameSet) {
|
|
449
|
+
if (!testNameSet.has(testName) && !testNameSet.has(fullName)) {
|
|
450
|
+
return; // Skip this test
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Extract test body
|
|
454
|
+
if (t.isFunctionExpression(testCallback) || t.isArrowFunctionExpression(testCallback)) {
|
|
455
|
+
let testBody;
|
|
456
|
+
let statements;
|
|
457
|
+
if (t.isBlockStatement(testCallback.body)) {
|
|
458
|
+
testBody = (0, generator_1.default)(testCallback.body, { comments: false }).code;
|
|
459
|
+
// Extract ALL statements from BlockStatement.body (includes variables)
|
|
460
|
+
const bodyStatements = testCallback.body.body;
|
|
461
|
+
statements = bodyStatements.map((stmt, index) => {
|
|
462
|
+
const code = (0, generator_1.default)(stmt, { comments: false }).code;
|
|
463
|
+
const isVar = t.isVariableDeclaration(stmt);
|
|
464
|
+
// Extract intent comment (leading comment, exclude @Screen/@State)
|
|
465
|
+
let intentComment;
|
|
466
|
+
if (stmt.leadingComments && stmt.leadingComments.length > 0) {
|
|
467
|
+
for (const comment of stmt.leadingComments) {
|
|
468
|
+
const commentText = comment.value.trim();
|
|
469
|
+
if (!commentText.includes('@Screen') && !commentText.includes('@State')) {
|
|
470
|
+
intentComment = commentText;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Extract screen-state annotation (trailing or next statement's leading)
|
|
476
|
+
let screenStateAnnotation;
|
|
477
|
+
if (stmt.trailingComments && stmt.trailingComments.length > 0) {
|
|
478
|
+
for (const comment of stmt.trailingComments) {
|
|
479
|
+
const commentText = comment.value.trim();
|
|
480
|
+
if (commentText.includes('@Screen') || commentText.includes('@State')) {
|
|
481
|
+
screenStateAnnotation = commentText;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Fallback: check next statement's leading @Screen/@State comment
|
|
487
|
+
if (!screenStateAnnotation && index + 1 < bodyStatements.length) {
|
|
488
|
+
const nextStmt = bodyStatements[index + 1];
|
|
489
|
+
if (nextStmt.leadingComments && nextStmt.leadingComments.length > 0) {
|
|
490
|
+
for (const comment of nextStmt.leadingComments) {
|
|
491
|
+
const commentText = comment.value.trim();
|
|
492
|
+
if (commentText.includes('@Screen') || commentText.includes('@State')) {
|
|
493
|
+
screenStateAnnotation = commentText;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { code, isVariableDeclaration: isVar, intentComment, screenStateAnnotation };
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// Expression body - single statement
|
|
504
|
+
const exprCode = (0, generator_1.default)(testCallback.body, { comments: false }).code;
|
|
505
|
+
testBody = `{ return ${exprCode}; }`;
|
|
506
|
+
statements = [{
|
|
507
|
+
code: exprCode,
|
|
508
|
+
isVariableDeclaration: false
|
|
509
|
+
}];
|
|
510
|
+
}
|
|
511
|
+
result.tests.push({
|
|
512
|
+
name: testName,
|
|
513
|
+
code: testBody,
|
|
514
|
+
suitePath: [],
|
|
515
|
+
fullName: fullName,
|
|
516
|
+
statements: statements
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
throw new Error(`Failed to parse test file: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
}
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Flatten suite structure into execution-ready format
|
|
533
|
+
* Separates per-test hooks (beforeEach/afterEach) from per-suite hooks (beforeAll/afterAll)
|
|
534
|
+
* @param parsed - Parsed test file structure
|
|
535
|
+
* @returns Flattened structure ready for execution
|
|
536
|
+
*/
|
|
537
|
+
static flattenForExecution(parsed) {
|
|
538
|
+
const result = {
|
|
539
|
+
fileLevelHooks: {
|
|
540
|
+
beforeAll: parsed.fileHooks.beforeAll,
|
|
541
|
+
afterAll: parsed.fileHooks.afterAll,
|
|
542
|
+
beforeEach: parsed.fileHooks.beforeEach,
|
|
543
|
+
afterEach: parsed.fileHooks.afterEach
|
|
544
|
+
},
|
|
545
|
+
tests: [],
|
|
546
|
+
suites: []
|
|
547
|
+
};
|
|
548
|
+
let testIndex = 0;
|
|
549
|
+
/**
|
|
550
|
+
* Recursively collect tests and hooks from a suite and its nested suites
|
|
551
|
+
* Returns the test indices collected (including from nested suites)
|
|
552
|
+
*/
|
|
553
|
+
const collectFromSuite = (suite, parentBeforeEach, parentAfterEach) => {
|
|
554
|
+
// Collect beforeEach/afterEach hooks from this suite (parent hooks first)
|
|
555
|
+
const suiteBeforeEach = [...parentBeforeEach, ...suite.beforeEach];
|
|
556
|
+
const suiteAfterEach = [...suite.afterEach, ...parentAfterEach]; // Reverse order for afterEach
|
|
557
|
+
// Track test indices for this suite (includes tests from this suite and nested suites)
|
|
558
|
+
const suiteTestIndices = [];
|
|
559
|
+
// Add tests from this suite
|
|
560
|
+
for (const test of suite.tests) {
|
|
561
|
+
suiteTestIndices.push(testIndex);
|
|
562
|
+
result.tests.push({
|
|
563
|
+
test: test,
|
|
564
|
+
suitePath: test.suitePath || [],
|
|
565
|
+
suiteBeforeEachHooks: suiteBeforeEach,
|
|
566
|
+
suiteAfterEachHooks: suiteAfterEach
|
|
567
|
+
});
|
|
568
|
+
testIndex++;
|
|
569
|
+
}
|
|
570
|
+
// Process nested suites and collect their test indices
|
|
571
|
+
for (const nestedSuite of suite.nestedSuites) {
|
|
572
|
+
const nestedTestIndices = collectFromSuite(nestedSuite, suiteBeforeEach, suiteAfterEach);
|
|
573
|
+
// Include nested suite test indices in parent suite's test indices
|
|
574
|
+
// (needed for afterAll execution - parent suite's afterAll runs after all nested tests)
|
|
575
|
+
suiteTestIndices.push(...nestedTestIndices);
|
|
576
|
+
}
|
|
577
|
+
// Add suite info (for beforeAll/afterAll execution)
|
|
578
|
+
// Include suite if it has hooks OR if it has tests (direct or nested)
|
|
579
|
+
if (suite.beforeAll.length > 0 || suite.afterAll.length > 0 || suiteTestIndices.length > 0) {
|
|
580
|
+
result.suites.push({
|
|
581
|
+
suitePath: suite.suitePath,
|
|
582
|
+
beforeAll: suite.beforeAll,
|
|
583
|
+
afterAll: suite.afterAll,
|
|
584
|
+
testIndices: suiteTestIndices
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return suiteTestIndices;
|
|
588
|
+
};
|
|
589
|
+
// Add file-level tests
|
|
590
|
+
for (const test of parsed.tests) {
|
|
591
|
+
result.tests.push({
|
|
592
|
+
test: test,
|
|
593
|
+
suitePath: [],
|
|
594
|
+
suiteBeforeEachHooks: [],
|
|
595
|
+
suiteAfterEachHooks: []
|
|
596
|
+
});
|
|
597
|
+
testIndex++;
|
|
598
|
+
}
|
|
599
|
+
// Process all top-level suites
|
|
600
|
+
for (const suite of parsed.suites) {
|
|
601
|
+
collectFromSuite(suite, [], []);
|
|
602
|
+
}
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Construct a test script with imports and a single test using AST
|
|
607
|
+
* This is more robust than string interpolation
|
|
608
|
+
* @param originalScript - The full original script (to extract imports)
|
|
609
|
+
* @param testName - The test name
|
|
610
|
+
* @param testBodyCode - The test body code (already extracted)
|
|
611
|
+
* @returns A complete script with imports and the test
|
|
612
|
+
*/
|
|
613
|
+
static constructTestScriptWithImports(originalScript, testName, testBodyCode) {
|
|
614
|
+
try {
|
|
615
|
+
// Parse original script to extract import statements as AST nodes
|
|
616
|
+
const originalAst = (0, parser_1.parse)(originalScript, {
|
|
617
|
+
sourceType: 'module',
|
|
618
|
+
plugins: ['typescript', 'classProperties', 'decorators-legacy'],
|
|
619
|
+
allowImportExportEverywhere: true
|
|
620
|
+
});
|
|
621
|
+
// Collect import declarations
|
|
622
|
+
const importDeclarations = [];
|
|
623
|
+
(0, traverse_1.default)(originalAst, {
|
|
624
|
+
ImportDeclaration(path) {
|
|
625
|
+
importDeclarations.push(path.node);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
// Parse the test body code to get it as an AST node
|
|
629
|
+
// The test body is a block statement, so we need to parse it
|
|
630
|
+
let testBody;
|
|
631
|
+
try {
|
|
632
|
+
// Try parsing as a block statement
|
|
633
|
+
const bodyAst = (0, parser_1.parse)(`{ ${testBodyCode} }`, {
|
|
634
|
+
sourceType: 'module',
|
|
635
|
+
plugins: ['typescript', 'classProperties', 'decorators-legacy'],
|
|
636
|
+
allowReturnOutsideFunction: true
|
|
637
|
+
});
|
|
638
|
+
// Extract the block statement from the parsed code
|
|
639
|
+
if (bodyAst.program.body.length > 0 && t.isBlockStatement(bodyAst.program.body[0])) {
|
|
640
|
+
testBody = bodyAst.program.body[0];
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
// Fallback: create a block with the statements
|
|
644
|
+
const statements = bodyAst.program.body.filter(stmt => !t.isImportDeclaration(stmt));
|
|
645
|
+
testBody = t.blockStatement(statements);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch (parseError) {
|
|
649
|
+
// If parsing fails, try parsing as statements
|
|
650
|
+
const bodyAst = (0, parser_1.parse)(testBodyCode, {
|
|
651
|
+
sourceType: 'module',
|
|
652
|
+
plugins: ['typescript', 'classProperties', 'decorators-legacy'],
|
|
653
|
+
allowReturnOutsideFunction: true,
|
|
654
|
+
allowAwaitOutsideFunction: true
|
|
655
|
+
});
|
|
656
|
+
const statements = bodyAst.program.body.filter(stmt => !t.isImportDeclaration(stmt));
|
|
657
|
+
testBody = t.blockStatement(statements);
|
|
658
|
+
}
|
|
659
|
+
// Create the test call expression
|
|
660
|
+
// test('testName', async ({ page, browser, context }) => { ... })
|
|
661
|
+
const testIdentifier = t.identifier('test');
|
|
662
|
+
const testNameLiteral = t.stringLiteral(testName);
|
|
663
|
+
// Create async arrow function with parameters
|
|
664
|
+
const pageParam = t.objectPattern([
|
|
665
|
+
t.objectProperty(t.identifier('page'), t.identifier('page'), false, true),
|
|
666
|
+
t.objectProperty(t.identifier('browser'), t.identifier('browser'), false, true),
|
|
667
|
+
t.objectProperty(t.identifier('context'), t.identifier('context'), false, true)
|
|
668
|
+
]);
|
|
669
|
+
const asyncArrowFunction = t.arrowFunctionExpression([pageParam], testBody, true // async
|
|
670
|
+
);
|
|
671
|
+
// Create the test() call
|
|
672
|
+
const testCall = t.callExpression(testIdentifier, [
|
|
673
|
+
testNameLiteral,
|
|
674
|
+
asyncArrowFunction
|
|
675
|
+
]);
|
|
676
|
+
// Create a new program with imports + test call
|
|
677
|
+
const programBody = [
|
|
678
|
+
...importDeclarations,
|
|
679
|
+
t.expressionStatement(testCall)
|
|
680
|
+
];
|
|
681
|
+
const newProgram = t.program(programBody);
|
|
682
|
+
// Generate code from AST
|
|
683
|
+
const output = (0, generator_1.default)(newProgram, {
|
|
684
|
+
retainLines: false,
|
|
685
|
+
compact: false
|
|
686
|
+
});
|
|
687
|
+
return output.code;
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
// Fallback to string interpolation if AST construction fails
|
|
691
|
+
const importStatements = originalScript.match(/import\s+.*?from\s+['"]([^'"]+)['"];?/g) || [];
|
|
692
|
+
const importsCode = importStatements.length > 0
|
|
693
|
+
? importStatements.join('\n') + '\n\n'
|
|
694
|
+
: '';
|
|
695
|
+
// Escape test name properly for fallback
|
|
696
|
+
const escapedName = testName.replace(/'/g, "\\'").replace(/\n/g, '\\n');
|
|
697
|
+
return `${importsCode}test('${escapedName}', async ({ page, browser, context }) => {\n${testBodyCode}\n});`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
exports.TestFileParser = TestFileParser;
|
|
702
|
+
//# sourceMappingURL=test-file-parser.js.map
|