@aqa-pulse/cli 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/README.md +43 -0
- package/bin/aqa-pulse.js +49 -0
- package/dist/backend/generate-source-facts.d.ts +2 -0
- package/dist/backend/generate-source-facts.js +607 -0
- package/dist/backend/merge-reports.d.ts +19 -0
- package/dist/backend/merge-reports.js +314 -0
- package/dist/backend/upload-report-artifacts.d.ts +9 -0
- package/dist/backend/upload-report-artifacts.js +772 -0
- package/dist/backend/upload-report.d.ts +13 -0
- package/dist/backend/upload-report.js +338 -0
- package/dist/dashboard-utils.d.ts +437 -0
- package/dist/dashboard-utils.js +2627 -0
- package/dist/history-utils.d.ts +72 -0
- package/dist/history-utils.js +267 -0
- package/dist/shared/business-assumptions.d.ts +14 -0
- package/dist/shared/business-assumptions.js +61 -0
- package/dist/shared/dashboard-helpers.d.ts +63 -0
- package/dist/shared/dashboard-helpers.js +429 -0
- package/dist/shared/dashboard-metric-info.d.ts +61 -0
- package/dist/shared/dashboard-metric-info.js +15 -0
- package/dist/shared/error-utils.d.ts +1 -0
- package/dist/shared/error-utils.js +6 -0
- package/dist/shared/formatting.d.ts +3 -0
- package/dist/shared/formatting.js +42 -0
- package/dist/shared/i18n/ru.d.ts +558 -0
- package/dist/shared/i18n/ru.js +577 -0
- package/dist/shared/metric-info.d.ts +5 -0
- package/dist/shared/metric-info.js +210 -0
- package/dist/shared/navigation.d.ts +31 -0
- package/dist/shared/navigation.js +99 -0
- package/dist/shared/test-history-helpers.d.ts +51 -0
- package/dist/shared/test-history-helpers.js +294 -0
- package/dist/shared/test-history-metric-info.d.ts +17 -0
- package/dist/shared/test-history-metric-info.js +20 -0
- package/dist/shared/text-utils.d.ts +2 -0
- package/dist/shared/text-utils.js +15 -0
- package/package.json +37 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildPrecomputedSourceFacts = buildPrecomputedSourceFacts;
|
|
37
|
+
/**
|
|
38
|
+
* Назначение файла: собирает сведения об исходном коде
|
|
39
|
+
* для аналитики и обогащения данных дашборда.
|
|
40
|
+
*/
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const dashboard_utils_1 = require("../dashboard-utils");
|
|
44
|
+
const error_utils_1 = require("../shared/error-utils");
|
|
45
|
+
const DIRECT_LOCATOR_METHODS = new Set([
|
|
46
|
+
'locator',
|
|
47
|
+
'getByRole',
|
|
48
|
+
'getByLabel',
|
|
49
|
+
'getByTestId',
|
|
50
|
+
'getByText',
|
|
51
|
+
'getByPlaceholder',
|
|
52
|
+
'getByAltText',
|
|
53
|
+
'getByTitle',
|
|
54
|
+
'$',
|
|
55
|
+
'$$',
|
|
56
|
+
]);
|
|
57
|
+
const STABLE_LOCATOR_METHODS = new Set(['getByRole', 'getByLabel', 'getByTestId']);
|
|
58
|
+
const TEXT_LOCATOR_METHODS = new Set(['getByText', 'getByPlaceholder', 'getByAltText', 'getByTitle']);
|
|
59
|
+
const SMART_WAIT_METHODS = new Set(['waitForSelector', 'waitForResponse', 'waitForNavigation', 'waitForURL', 'waitForLoadState']);
|
|
60
|
+
const PAGE_ACTION_METHODS = new Set(['click', 'dblclick', 'tap', 'fill', 'press', 'check', 'uncheck', 'selectOption', 'goto', 'reload', 'setInputFiles', 'dragTo', 'hover']);
|
|
61
|
+
const SHARED_MUTATION_METHODS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'set', 'add', 'delete', 'clear']);
|
|
62
|
+
if (require.main === module) {
|
|
63
|
+
void main();
|
|
64
|
+
}
|
|
65
|
+
async function main() {
|
|
66
|
+
try {
|
|
67
|
+
const cliOptions = parseCliOptions(process.argv.slice(2));
|
|
68
|
+
const resolvedOptions = resolveCliOptions(cliOptions);
|
|
69
|
+
const report = (0, dashboard_utils_1.loadReporterReport)(resolvedOptions.reportPath);
|
|
70
|
+
const sourceFacts = buildPrecomputedSourceFacts(report, resolvedOptions.repoRoot, resolvedOptions.reportPath);
|
|
71
|
+
fs.mkdirSync(path.dirname(resolvedOptions.outPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(resolvedOptions.outPath, `${JSON.stringify(sourceFacts, null, 2)}\n`, 'utf8');
|
|
73
|
+
if (resolvedOptions.json) {
|
|
74
|
+
process.stdout.write(`${JSON.stringify({
|
|
75
|
+
reportPath: resolvedOptions.reportPath,
|
|
76
|
+
repoRoot: resolvedOptions.repoRoot,
|
|
77
|
+
outPath: resolvedOptions.outPath,
|
|
78
|
+
files: sourceFacts.files.length,
|
|
79
|
+
tests: sourceFacts.files.reduce((total, fileFacts) => total + fileFacts.tests.length, 0),
|
|
80
|
+
}, null, 2)}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log('AQA Pulse source facts generated.');
|
|
84
|
+
console.log(`Report path: ${resolvedOptions.reportPath}`);
|
|
85
|
+
console.log(`Repo root: ${resolvedOptions.repoRoot}`);
|
|
86
|
+
console.log(`Output path: ${resolvedOptions.outPath}`);
|
|
87
|
+
console.log(`Files analyzed: ${sourceFacts.files.length}`);
|
|
88
|
+
console.log(`Tests analyzed: ${sourceFacts.files.reduce((total, fileFacts) => total + fileFacts.tests.length, 0)}`);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`Ошибка generate-source-facts: ${(0, error_utils_1.getErrorMessage)(error)}`);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function parseCliOptions(args) {
|
|
96
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
97
|
+
printHelp();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
const options = { json: false };
|
|
101
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
102
|
+
const currentArg = args[index];
|
|
103
|
+
if (currentArg === '--json') {
|
|
104
|
+
options.json = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const nextArg = args[index + 1];
|
|
108
|
+
if (!nextArg || nextArg.startsWith('--')) {
|
|
109
|
+
throw new Error(`Для аргумента ${currentArg} нужно передать значение.`);
|
|
110
|
+
}
|
|
111
|
+
if (currentArg === '--report' || currentArg === '--report-path') {
|
|
112
|
+
options.reportPath = nextArg;
|
|
113
|
+
index += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (currentArg === '--out') {
|
|
117
|
+
options.outPath = nextArg;
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (currentArg === '--repo-root') {
|
|
122
|
+
options.repoRoot = nextArg;
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Неизвестный аргумент: ${currentArg}`);
|
|
127
|
+
}
|
|
128
|
+
return options;
|
|
129
|
+
}
|
|
130
|
+
function resolveCliOptions(options) {
|
|
131
|
+
const reportPath = path.resolve(requireNonEmptyText(options.reportPath ?? process.env.PW_LLM_REPORT, '--report / PW_LLM_REPORT'));
|
|
132
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
133
|
+
const outPath = path.resolve(options.outPath ?? process.env.AQA_PULSE_SOURCE_FACTS_PATH ?? path.join(path.dirname(reportPath), 'source-facts.json'));
|
|
134
|
+
return {
|
|
135
|
+
reportPath,
|
|
136
|
+
outPath,
|
|
137
|
+
repoRoot,
|
|
138
|
+
json: options.json,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function buildPrecomputedSourceFacts(report, repoRoot, reportPath) {
|
|
142
|
+
const typeScriptModule = getTypeScriptModule(repoRoot, reportPath);
|
|
143
|
+
if (!typeScriptModule) {
|
|
144
|
+
throw new Error('Для generate-source-facts нужен пакет typescript. Установи его в test repo как devDependency.');
|
|
145
|
+
}
|
|
146
|
+
const filePaths = [...new Set((report.tests ?? [])
|
|
147
|
+
.map((test) => typeof test.location?.file === 'string' ? test.location.file.trim() : '')
|
|
148
|
+
.filter((filePath) => filePath.length > 0))];
|
|
149
|
+
const files = [];
|
|
150
|
+
for (const filePath of filePaths) {
|
|
151
|
+
const resolvedFilePath = resolveSourceFilePath(filePath, repoRoot, reportPath);
|
|
152
|
+
if (!resolvedFilePath) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const fileFacts = analyzeSourceFile(resolvedFilePath, filePath, typeScriptModule);
|
|
156
|
+
if (fileFacts && fileFacts.tests.length > 0) {
|
|
157
|
+
files.push(fileFacts);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
schemaVersion: 1,
|
|
162
|
+
analyzerVersion: 'aqa-pulse-server generate-source-facts v1',
|
|
163
|
+
files,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function getTypeScriptModule(repoRoot, reportPath) {
|
|
167
|
+
const resolveRoots = [
|
|
168
|
+
repoRoot,
|
|
169
|
+
path.dirname(reportPath),
|
|
170
|
+
process.cwd(),
|
|
171
|
+
path.resolve(process.cwd(), '..', 'aqa-pulse'),
|
|
172
|
+
];
|
|
173
|
+
for (const resolveRoot of resolveRoots) {
|
|
174
|
+
try {
|
|
175
|
+
const modulePath = require.resolve('typescript', { paths: [resolveRoot] });
|
|
176
|
+
return require(modulePath);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function resolveSourceFilePath(filePath, repoRoot, reportPath) {
|
|
185
|
+
const normalizedPath = filePath.trim();
|
|
186
|
+
if (!normalizedPath) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const candidatePaths = new Set();
|
|
190
|
+
const reportDirectory = path.dirname(reportPath);
|
|
191
|
+
if (path.isAbsolute(normalizedPath)) {
|
|
192
|
+
candidatePaths.add(normalizedPath);
|
|
193
|
+
}
|
|
194
|
+
candidatePaths.add(path.resolve(repoRoot, normalizedPath));
|
|
195
|
+
candidatePaths.add(path.resolve(reportDirectory, normalizedPath));
|
|
196
|
+
candidatePaths.add(path.resolve(process.cwd(), normalizedPath));
|
|
197
|
+
for (const candidatePath of candidatePaths) {
|
|
198
|
+
if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile()) {
|
|
199
|
+
return candidatePath;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
function analyzeSourceFile(resolvedFilePath, reportFilePath, typeScriptModule) {
|
|
205
|
+
try {
|
|
206
|
+
const sourceText = fs.readFileSync(resolvedFilePath, 'utf8');
|
|
207
|
+
const sourceFile = typeScriptModule.createSourceFile(resolvedFilePath, sourceText, typeScriptModule.ScriptTarget.Latest, true, resolveScriptKind(resolvedFilePath, typeScriptModule));
|
|
208
|
+
let hasPomImports = false;
|
|
209
|
+
const pomImportIdentifiers = new Set();
|
|
210
|
+
let beforeAllCount = 0;
|
|
211
|
+
let beforeEachCount = 0;
|
|
212
|
+
let serialModeCount = 0;
|
|
213
|
+
let topLevelMutableStateCount = 0;
|
|
214
|
+
const topLevelMutableIdentifiers = new Set();
|
|
215
|
+
const tests = [];
|
|
216
|
+
for (const statement of sourceFile.statements) {
|
|
217
|
+
if (typeScriptModule.isImportDeclaration(statement)) {
|
|
218
|
+
const importPath = statement.moduleSpecifier.getText(sourceFile).slice(1, -1);
|
|
219
|
+
if (isPomImportPath(importPath)) {
|
|
220
|
+
hasPomImports = true;
|
|
221
|
+
if (statement.importClause?.name) {
|
|
222
|
+
pomImportIdentifiers.add(statement.importClause.name.text);
|
|
223
|
+
}
|
|
224
|
+
if (statement.importClause?.namedBindings && typeScriptModule.isNamedImports(statement.importClause.namedBindings)) {
|
|
225
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
226
|
+
pomImportIdentifiers.add(element.name.text);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (typeScriptModule.isVariableStatement(statement)) {
|
|
233
|
+
const declarationFlags = statement.declarationList.flags;
|
|
234
|
+
const isConst = (declarationFlags & typeScriptModule.NodeFlags.Const) !== 0;
|
|
235
|
+
if (!isConst) {
|
|
236
|
+
topLevelMutableStateCount += statement.declarationList.declarations.length;
|
|
237
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
238
|
+
if (typeScriptModule.isIdentifier(declaration.name)) {
|
|
239
|
+
topLevelMutableIdentifiers.add(declaration.name.text);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
visitCallExpressions(sourceFile, typeScriptModule, (callExpression) => {
|
|
246
|
+
if (getCallExpressionName(callExpression, typeScriptModule) === 'beforeAll') {
|
|
247
|
+
beforeAllCount += 1;
|
|
248
|
+
}
|
|
249
|
+
if (getCallExpressionName(callExpression, typeScriptModule) === 'beforeEach') {
|
|
250
|
+
beforeEachCount += 1;
|
|
251
|
+
}
|
|
252
|
+
if (isSerialConfigureCall(callExpression, sourceFile, typeScriptModule)) {
|
|
253
|
+
serialModeCount += 1;
|
|
254
|
+
}
|
|
255
|
+
if (!isTestDeclarationCall(callExpression, typeScriptModule)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const callback = getTestCallback(callExpression, typeScriptModule);
|
|
259
|
+
if (!callback) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
tests.push(collectSourceTestFacts(callback, callExpression, sourceFile, typeScriptModule, {
|
|
263
|
+
hasPomImports,
|
|
264
|
+
pomImportIdentifiers,
|
|
265
|
+
topLevelMutableIdentifiers,
|
|
266
|
+
}));
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
file: reportFilePath,
|
|
270
|
+
tests,
|
|
271
|
+
hasPomImports,
|
|
272
|
+
beforeAllCount,
|
|
273
|
+
beforeEachCount,
|
|
274
|
+
serialModeCount,
|
|
275
|
+
topLevelMutableStateCount,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function collectSourceTestFacts(callback, testDeclaration, sourceFile, typeScriptModule, context) {
|
|
283
|
+
let assertionCount = 0;
|
|
284
|
+
let smartWaitCount = 0;
|
|
285
|
+
let hardWaitCount = 0;
|
|
286
|
+
let stepCount = 0;
|
|
287
|
+
let directLocatorCount = 0;
|
|
288
|
+
let directPageActionCount = 0;
|
|
289
|
+
let stableSelectorCount = 0;
|
|
290
|
+
let textSelectorCount = 0;
|
|
291
|
+
let fragileSelectorCount = 0;
|
|
292
|
+
let pomReferenceCount = 0;
|
|
293
|
+
let pomFixtureReferenceCount = 0;
|
|
294
|
+
let sharedStateMutationCount = 0;
|
|
295
|
+
const pomFixtureNames = new Set(getPomFixtureNames(callback, typeScriptModule));
|
|
296
|
+
visitCallExpressions(callback.body, typeScriptModule, (callExpression) => {
|
|
297
|
+
if (isExpectCall(callExpression, typeScriptModule)) {
|
|
298
|
+
assertionCount += 1;
|
|
299
|
+
smartWaitCount += 1;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (isTestStepCall(callExpression, typeScriptModule)) {
|
|
303
|
+
stepCount += 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const callName = getCallExpressionName(callExpression, typeScriptModule);
|
|
307
|
+
if (!callName) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (callName === 'waitForTimeout') {
|
|
311
|
+
hardWaitCount += 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (SMART_WAIT_METHODS.has(callName)) {
|
|
315
|
+
smartWaitCount += 1;
|
|
316
|
+
}
|
|
317
|
+
if (isDirectPageActionCall(callExpression, typeScriptModule)) {
|
|
318
|
+
directPageActionCount += 1;
|
|
319
|
+
}
|
|
320
|
+
if (isPomInteractionCall(callExpression, typeScriptModule, context.pomImportIdentifiers, pomFixtureNames)) {
|
|
321
|
+
if (isFixtureBackedPomCall(callExpression, typeScriptModule, pomFixtureNames)) {
|
|
322
|
+
pomFixtureReferenceCount += 1;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
pomReferenceCount += 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (isSharedMutableMutationCall(callExpression, typeScriptModule, context.topLevelMutableIdentifiers)) {
|
|
329
|
+
sharedStateMutationCount += 1;
|
|
330
|
+
}
|
|
331
|
+
if (DIRECT_LOCATOR_METHODS.has(callName)) {
|
|
332
|
+
directLocatorCount += 1;
|
|
333
|
+
if (STABLE_LOCATOR_METHODS.has(callName)) {
|
|
334
|
+
stableSelectorCount += 1;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (TEXT_LOCATOR_METHODS.has(callName)) {
|
|
338
|
+
textSelectorCount += 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const selectorKind = classifySelectorLiteral(readFirstStringArgument(callExpression, sourceFile, typeScriptModule));
|
|
342
|
+
if (selectorKind === 'stable') {
|
|
343
|
+
stableSelectorCount += 1;
|
|
344
|
+
}
|
|
345
|
+
else if (selectorKind === 'text') {
|
|
346
|
+
textSelectorCount += 1;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
fragileSelectorCount += 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
visitNodes(callback.body, typeScriptModule, (node) => {
|
|
354
|
+
if (typeScriptModule.isIdentifier(node) && context.pomImportIdentifiers.has(node.text)) {
|
|
355
|
+
pomReferenceCount += 1;
|
|
356
|
+
}
|
|
357
|
+
if (typeScriptModule.isIdentifier(node) && pomFixtureNames.has(node.text)) {
|
|
358
|
+
pomFixtureReferenceCount += 1;
|
|
359
|
+
}
|
|
360
|
+
if (isSharedMutableAssignment(node, typeScriptModule, context.topLevelMutableIdentifiers)) {
|
|
361
|
+
sharedStateMutationCount += 1;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
const startLine = sourceFile.getLineAndCharacterOfPosition(callback.getStart(sourceFile)).line + 1;
|
|
365
|
+
const endLine = sourceFile.getLineAndCharacterOfPosition(callback.getEnd()).line + 1;
|
|
366
|
+
return {
|
|
367
|
+
startLine,
|
|
368
|
+
endLine,
|
|
369
|
+
title: readTestTitle(testDeclaration, sourceFile, typeScriptModule),
|
|
370
|
+
assertionCount,
|
|
371
|
+
smartWaitCount,
|
|
372
|
+
hardWaitCount,
|
|
373
|
+
stepCount,
|
|
374
|
+
directLocatorCount,
|
|
375
|
+
directPageActionCount,
|
|
376
|
+
stableSelectorCount,
|
|
377
|
+
textSelectorCount,
|
|
378
|
+
fragileSelectorCount,
|
|
379
|
+
pomReferenceCount,
|
|
380
|
+
pomFixtureReferenceCount,
|
|
381
|
+
sharedStateMutationCount,
|
|
382
|
+
usesPom: evaluatePomUsage({
|
|
383
|
+
hasPomImports: context.hasPomImports,
|
|
384
|
+
pomReferenceCount,
|
|
385
|
+
pomFixtureReferenceCount,
|
|
386
|
+
directLocatorCount,
|
|
387
|
+
directPageActionCount,
|
|
388
|
+
}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function resolveScriptKind(filePath, typeScriptModule) {
|
|
392
|
+
if (/\.tsx$/i.test(filePath)) {
|
|
393
|
+
return typeScriptModule.ScriptKind.TSX;
|
|
394
|
+
}
|
|
395
|
+
if (/\.jsx$/i.test(filePath)) {
|
|
396
|
+
return typeScriptModule.ScriptKind.JSX;
|
|
397
|
+
}
|
|
398
|
+
if (/\.js$/i.test(filePath) || /\.mjs$/i.test(filePath) || /\.cjs$/i.test(filePath)) {
|
|
399
|
+
return typeScriptModule.ScriptKind.JS;
|
|
400
|
+
}
|
|
401
|
+
return typeScriptModule.ScriptKind.TS;
|
|
402
|
+
}
|
|
403
|
+
function visitCallExpressions(node, typeScriptModule, callback) {
|
|
404
|
+
const visit = (currentNode) => {
|
|
405
|
+
if (typeScriptModule.isCallExpression(currentNode)) {
|
|
406
|
+
callback(currentNode);
|
|
407
|
+
}
|
|
408
|
+
typeScriptModule.forEachChild(currentNode, visit);
|
|
409
|
+
};
|
|
410
|
+
visit(node);
|
|
411
|
+
}
|
|
412
|
+
function visitNodes(node, typeScriptModule, callback) {
|
|
413
|
+
const visit = (currentNode) => {
|
|
414
|
+
callback(currentNode);
|
|
415
|
+
typeScriptModule.forEachChild(currentNode, visit);
|
|
416
|
+
};
|
|
417
|
+
visit(node);
|
|
418
|
+
}
|
|
419
|
+
function isPomImportPath(importPath) {
|
|
420
|
+
return /(^|\/)(pages?|page-objects?|pageobjects?|pom|screen-objects?|page-models?)(\/|$)/i.test(importPath);
|
|
421
|
+
}
|
|
422
|
+
function isTestDeclarationCall(callExpression, typeScriptModule) {
|
|
423
|
+
const expression = callExpression.expression;
|
|
424
|
+
if (typeScriptModule.isIdentifier(expression)) {
|
|
425
|
+
return expression.text === 'test' || expression.text === 'it';
|
|
426
|
+
}
|
|
427
|
+
if (typeScriptModule.isPropertyAccessExpression(expression) && typeScriptModule.isIdentifier(expression.expression)) {
|
|
428
|
+
return (expression.expression.text === 'test' || expression.expression.text === 'it')
|
|
429
|
+
&& ['only', 'skip', 'fixme', 'fail'].includes(expression.name.text);
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
function getTestCallback(callExpression, typeScriptModule) {
|
|
434
|
+
const callbackCandidate = [...callExpression.arguments]
|
|
435
|
+
.reverse()
|
|
436
|
+
.find((argument) => typeScriptModule.isArrowFunction(argument) || typeScriptModule.isFunctionExpression(argument));
|
|
437
|
+
if (!callbackCandidate) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
return callbackCandidate;
|
|
441
|
+
}
|
|
442
|
+
function readTestTitle(callExpression, sourceFile, typeScriptModule) {
|
|
443
|
+
const firstArgument = callExpression.arguments[0];
|
|
444
|
+
if (!firstArgument) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
if (typeScriptModule.isStringLiteral(firstArgument) || typeScriptModule.isNoSubstitutionTemplateLiteral(firstArgument)) {
|
|
448
|
+
return firstArgument.text;
|
|
449
|
+
}
|
|
450
|
+
const rawText = firstArgument.getText(sourceFile).trim();
|
|
451
|
+
return rawText.length > 0 ? rawText : null;
|
|
452
|
+
}
|
|
453
|
+
function getPomFixtureNames(callback, typeScriptModule) {
|
|
454
|
+
const firstParameter = callback.parameters[0];
|
|
455
|
+
if (!firstParameter || !typeScriptModule.isObjectBindingPattern(firstParameter.name)) {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
return firstParameter.name.elements
|
|
459
|
+
.map((element) => typeScriptModule.isIdentifier(element.name) ? element.name.text : null)
|
|
460
|
+
.filter((value) => typeof value === 'string')
|
|
461
|
+
.filter((value) => isPomLikeIdentifier(value));
|
|
462
|
+
}
|
|
463
|
+
function isTestStepCall(callExpression, typeScriptModule) {
|
|
464
|
+
return typeScriptModule.isPropertyAccessExpression(callExpression.expression)
|
|
465
|
+
&& typeScriptModule.isIdentifier(callExpression.expression.expression)
|
|
466
|
+
&& callExpression.expression.expression.text === 'test'
|
|
467
|
+
&& callExpression.expression.name.text === 'step';
|
|
468
|
+
}
|
|
469
|
+
function isExpectCall(callExpression, typeScriptModule) {
|
|
470
|
+
if (typeScriptModule.isIdentifier(callExpression.expression)) {
|
|
471
|
+
return callExpression.expression.text === 'expect';
|
|
472
|
+
}
|
|
473
|
+
return typeScriptModule.isPropertyAccessExpression(callExpression.expression)
|
|
474
|
+
&& typeScriptModule.isIdentifier(callExpression.expression.expression)
|
|
475
|
+
&& callExpression.expression.expression.text === 'expect'
|
|
476
|
+
&& ['soft', 'poll'].includes(callExpression.expression.name.text);
|
|
477
|
+
}
|
|
478
|
+
function isSerialConfigureCall(callExpression, sourceFile, typeScriptModule) {
|
|
479
|
+
if (!typeScriptModule.isPropertyAccessExpression(callExpression.expression)) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
const objectExpression = callExpression.expression.expression;
|
|
483
|
+
if (!typeScriptModule.isIdentifier(objectExpression) || objectExpression.text !== 'test' || callExpression.expression.name.text !== 'describe') {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return callExpression.arguments.some((argument) => argument.getText(sourceFile).includes('serial'));
|
|
487
|
+
}
|
|
488
|
+
function getCallExpressionName(callExpression, typeScriptModule) {
|
|
489
|
+
const expression = callExpression.expression;
|
|
490
|
+
if (typeScriptModule.isIdentifier(expression)) {
|
|
491
|
+
return expression.text;
|
|
492
|
+
}
|
|
493
|
+
if (typeScriptModule.isPropertyAccessExpression(expression)) {
|
|
494
|
+
return expression.name.text;
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
function isDirectPageActionCall(callExpression, typeScriptModule) {
|
|
499
|
+
return typeScriptModule.isPropertyAccessExpression(callExpression.expression)
|
|
500
|
+
&& typeScriptModule.isIdentifier(callExpression.expression.expression)
|
|
501
|
+
&& callExpression.expression.expression.text === 'page'
|
|
502
|
+
&& PAGE_ACTION_METHODS.has(callExpression.expression.name.text);
|
|
503
|
+
}
|
|
504
|
+
function isPomInteractionCall(callExpression, typeScriptModule, pomImportIdentifiers, pomFixtureNames) {
|
|
505
|
+
if (!typeScriptModule.isPropertyAccessExpression(callExpression.expression)) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
const target = callExpression.expression.expression;
|
|
509
|
+
if (!typeScriptModule.isIdentifier(target)) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return pomImportIdentifiers.has(target.text)
|
|
513
|
+
|| pomFixtureNames.has(target.text)
|
|
514
|
+
|| isPomLikeIdentifier(target.text);
|
|
515
|
+
}
|
|
516
|
+
function isFixtureBackedPomCall(callExpression, typeScriptModule, pomFixtureNames) {
|
|
517
|
+
return typeScriptModule.isPropertyAccessExpression(callExpression.expression)
|
|
518
|
+
&& typeScriptModule.isIdentifier(callExpression.expression.expression)
|
|
519
|
+
&& pomFixtureNames.has(callExpression.expression.expression.text);
|
|
520
|
+
}
|
|
521
|
+
function isSharedMutableMutationCall(callExpression, typeScriptModule, topLevelMutableIdentifiers) {
|
|
522
|
+
if (!typeScriptModule.isPropertyAccessExpression(callExpression.expression)) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
const target = callExpression.expression.expression;
|
|
526
|
+
return typeScriptModule.isIdentifier(target)
|
|
527
|
+
&& topLevelMutableIdentifiers.has(target.text)
|
|
528
|
+
&& SHARED_MUTATION_METHODS.has(callExpression.expression.name.text);
|
|
529
|
+
}
|
|
530
|
+
function isSharedMutableAssignment(node, typeScriptModule, topLevelMutableIdentifiers) {
|
|
531
|
+
if (typeScriptModule.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind, typeScriptModule)) {
|
|
532
|
+
return typeScriptModule.isIdentifier(node.left) && topLevelMutableIdentifiers.has(node.left.text);
|
|
533
|
+
}
|
|
534
|
+
if (typeScriptModule.isPrefixUnaryExpression(node) || typeScriptModule.isPostfixUnaryExpression(node)) {
|
|
535
|
+
const operand = node.operand;
|
|
536
|
+
return typeScriptModule.isIdentifier(operand)
|
|
537
|
+
&& topLevelMutableIdentifiers.has(operand.text)
|
|
538
|
+
&& (node.operator === typeScriptModule.SyntaxKind.PlusPlusToken || node.operator === typeScriptModule.SyntaxKind.MinusMinusToken);
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
function isAssignmentOperator(kind, typeScriptModule) {
|
|
543
|
+
return kind >= typeScriptModule.SyntaxKind.FirstAssignment && kind <= typeScriptModule.SyntaxKind.LastAssignment;
|
|
544
|
+
}
|
|
545
|
+
function isPomLikeIdentifier(value) {
|
|
546
|
+
return /(?:page|screen|modal|dialog|drawer|form|flow|widget|section|panel|steps|po|model)$/i.test(value)
|
|
547
|
+
&& value.toLowerCase() !== 'page';
|
|
548
|
+
}
|
|
549
|
+
function evaluatePomUsage(input) {
|
|
550
|
+
const pomSignals = input.pomReferenceCount + input.pomFixtureReferenceCount;
|
|
551
|
+
const directSignals = input.directLocatorCount + input.directPageActionCount;
|
|
552
|
+
if (pomSignals >= 2 && directSignals <= 4) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
if (input.pomFixtureReferenceCount > 0 && directSignals <= 3) {
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
if (input.hasPomImports && pomSignals > 0 && input.directLocatorCount <= 1 && input.directPageActionCount <= 2) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
function readFirstStringArgument(callExpression, sourceFile, typeScriptModule) {
|
|
564
|
+
const firstArgument = callExpression.arguments[0];
|
|
565
|
+
if (!firstArgument) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
if (typeScriptModule.isStringLiteral(firstArgument) || typeScriptModule.isNoSubstitutionTemplateLiteral(firstArgument)) {
|
|
569
|
+
return firstArgument.text;
|
|
570
|
+
}
|
|
571
|
+
const rawText = firstArgument.getText(sourceFile);
|
|
572
|
+
return rawText.length > 0 ? rawText : null;
|
|
573
|
+
}
|
|
574
|
+
function classifySelectorLiteral(selector) {
|
|
575
|
+
if (!selector) {
|
|
576
|
+
return 'fragile';
|
|
577
|
+
}
|
|
578
|
+
const normalizedSelector = selector.trim().toLowerCase();
|
|
579
|
+
if (/data-testid|data-test|qa-id|testid/.test(normalizedSelector)) {
|
|
580
|
+
return 'stable';
|
|
581
|
+
}
|
|
582
|
+
if (/text=|has-text|:text|\btext\(/.test(normalizedSelector)) {
|
|
583
|
+
return 'text';
|
|
584
|
+
}
|
|
585
|
+
if (/^\/\/|^xpath=|nth-child|:nth|\s>\s|\.[a-z0-9_-]+\.[a-z0-9_.-]+|\[class|\.filter-option|\.btn|\.button/.test(normalizedSelector)) {
|
|
586
|
+
return 'fragile';
|
|
587
|
+
}
|
|
588
|
+
return normalizedSelector.includes('#') ? 'stable' : 'fragile';
|
|
589
|
+
}
|
|
590
|
+
function requireNonEmptyText(value, label) {
|
|
591
|
+
const normalized = typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
592
|
+
if (!normalized) {
|
|
593
|
+
throw new Error(`Нужно передать ${label}.`);
|
|
594
|
+
}
|
|
595
|
+
return normalized;
|
|
596
|
+
}
|
|
597
|
+
function printHelp() {
|
|
598
|
+
console.log('aqa-pulse-server generate-source-facts [options]');
|
|
599
|
+
console.log('');
|
|
600
|
+
console.log('Опции:');
|
|
601
|
+
console.log(' --report <path> Путь к Playwright report JSON, иначе PW_LLM_REPORT');
|
|
602
|
+
console.log(' --out <path> Куда сохранить source-facts.json, иначе AQA_PULSE_SOURCE_FACTS_PATH или <reportDir>/source-facts.json');
|
|
603
|
+
console.log(' --repo-root <path> Корень test repo для резолва location.file, иначе текущая директория');
|
|
604
|
+
console.log(' --json Печатать результат в JSON');
|
|
605
|
+
console.log('');
|
|
606
|
+
console.log('Скрипт анализирует только файлы, которые упомянуты в report.tests[*].location.file.');
|
|
607
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReporterRoot } from '../dashboard-utils';
|
|
2
|
+
interface MergeReportsOptions {
|
|
3
|
+
projectKind: 'ui' | 'api';
|
|
4
|
+
outputPath: string;
|
|
5
|
+
inputs: string[];
|
|
6
|
+
allowMissing: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface ReportEntry {
|
|
9
|
+
inputPath: string;
|
|
10
|
+
relativeInputPath: string;
|
|
11
|
+
report: ReporterRoot;
|
|
12
|
+
sourceLabel: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function mergeAqaPulseReports(options: MergeReportsOptions): {
|
|
15
|
+
reports: ReportEntry[];
|
|
16
|
+
missingInputs: string[];
|
|
17
|
+
mergedReport: ReporterRoot;
|
|
18
|
+
};
|
|
19
|
+
export {};
|