@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.
Files changed (37) hide show
  1. package/README.md +43 -0
  2. package/bin/aqa-pulse.js +49 -0
  3. package/dist/backend/generate-source-facts.d.ts +2 -0
  4. package/dist/backend/generate-source-facts.js +607 -0
  5. package/dist/backend/merge-reports.d.ts +19 -0
  6. package/dist/backend/merge-reports.js +314 -0
  7. package/dist/backend/upload-report-artifacts.d.ts +9 -0
  8. package/dist/backend/upload-report-artifacts.js +772 -0
  9. package/dist/backend/upload-report.d.ts +13 -0
  10. package/dist/backend/upload-report.js +338 -0
  11. package/dist/dashboard-utils.d.ts +437 -0
  12. package/dist/dashboard-utils.js +2627 -0
  13. package/dist/history-utils.d.ts +72 -0
  14. package/dist/history-utils.js +267 -0
  15. package/dist/shared/business-assumptions.d.ts +14 -0
  16. package/dist/shared/business-assumptions.js +61 -0
  17. package/dist/shared/dashboard-helpers.d.ts +63 -0
  18. package/dist/shared/dashboard-helpers.js +429 -0
  19. package/dist/shared/dashboard-metric-info.d.ts +61 -0
  20. package/dist/shared/dashboard-metric-info.js +15 -0
  21. package/dist/shared/error-utils.d.ts +1 -0
  22. package/dist/shared/error-utils.js +6 -0
  23. package/dist/shared/formatting.d.ts +3 -0
  24. package/dist/shared/formatting.js +42 -0
  25. package/dist/shared/i18n/ru.d.ts +558 -0
  26. package/dist/shared/i18n/ru.js +577 -0
  27. package/dist/shared/metric-info.d.ts +5 -0
  28. package/dist/shared/metric-info.js +210 -0
  29. package/dist/shared/navigation.d.ts +31 -0
  30. package/dist/shared/navigation.js +99 -0
  31. package/dist/shared/test-history-helpers.d.ts +51 -0
  32. package/dist/shared/test-history-helpers.js +294 -0
  33. package/dist/shared/test-history-metric-info.d.ts +17 -0
  34. package/dist/shared/test-history-metric-info.js +20 -0
  35. package/dist/shared/text-utils.d.ts +2 -0
  36. package/dist/shared/text-utils.js +15 -0
  37. package/package.json +37 -0
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHistoryRowAnchor = buildHistoryRowAnchor;
4
+ exports.formatRunsLabel = formatRunsLabel;
5
+ exports.getRunWord = getRunWord;
6
+ exports.formatTemplate = formatTemplate;
7
+ exports.isStableHistoryItem = isStableHistoryItem;
8
+ exports.isUnstableHistoryItem = isUnstableHistoryItem;
9
+ exports.getUnstableHistoryItems = getUnstableHistoryItems;
10
+ exports.getUnstableEventLabel = getUnstableEventLabel;
11
+ exports.findLatestStableRecovery = findLatestStableRecovery;
12
+ exports.findCurrentStabilityStreak = findCurrentStabilityStreak;
13
+ exports.findUnstableStreakBeforeRecovery = findUnstableStreakBeforeRecovery;
14
+ exports.formatCurrentStabilityDescription = formatCurrentStabilityDescription;
15
+ exports.normalizeAnchorLookupValue = normalizeAnchorLookupValue;
16
+ exports.buildStepAnchor = buildStepAnchor;
17
+ exports.buildDiagnosticStepTree = buildDiagnosticStepTree;
18
+ exports.findIncidentStepAnchor = findIncidentStepAnchor;
19
+ exports.getAttachmentReference = getAttachmentReference;
20
+ exports.buildAttachmentHref = buildAttachmentHref;
21
+ exports.isImageAttachment = isImageAttachment;
22
+ exports.isMarkdownAttachment = isMarkdownAttachment;
23
+ exports.canInlineMarkdownPreview = canInlineMarkdownPreview;
24
+ const dashboard_helpers_1 = require("./dashboard-helpers");
25
+ const ru_1 = require("./i18n/ru");
26
+ const HISTORY_TEXT = ru_1.ru.testHistory;
27
+ function buildHistoryRowAnchor(runId) {
28
+ const normalizedId = runId
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9]+/g, '-')
31
+ .replace(/^-+|-+$/g, '');
32
+ return normalizedId.length > 0 ? `history-row-${normalizedId}` : 'history-row-run';
33
+ }
34
+ function formatRunsLabel(count) {
35
+ return `${count} ${getRunWord(count)}`;
36
+ }
37
+ function getRunWord(count) {
38
+ const remainder100 = count % 100;
39
+ const remainder10 = count % 10;
40
+ if (remainder100 >= 11 && remainder100 <= 14) {
41
+ return 'прогонов';
42
+ }
43
+ if (remainder10 === 1) {
44
+ return 'прогон';
45
+ }
46
+ if (remainder10 >= 2 && remainder10 <= 4) {
47
+ return 'прогона';
48
+ }
49
+ return 'прогонов';
50
+ }
51
+ function formatTemplate(template, values) {
52
+ return Object.entries(values).reduce((result, [key, value]) => result.replace(new RegExp(`\\{${key}\\}`, 'g'), value), template);
53
+ }
54
+ function isStableHistoryItem(item) {
55
+ return item.status === 'passed' && !item.flaky && !item.errorMessage;
56
+ }
57
+ function isUnstableHistoryItem(item) {
58
+ return Boolean(item.errorMessage)
59
+ || item.flaky
60
+ || item.status === 'failed'
61
+ || item.status === 'timedout'
62
+ || item.status === 'interrupted';
63
+ }
64
+ function getUnstableHistoryItems(history) {
65
+ return history.filter(isUnstableHistoryItem);
66
+ }
67
+ function getUnstableEventLabel(item) {
68
+ if (item.errorMessage) {
69
+ return HISTORY_TEXT.labels.error;
70
+ }
71
+ if (item.flaky) {
72
+ return HISTORY_TEXT.labels.flaky;
73
+ }
74
+ return (0, dashboard_helpers_1.formatStatusLabel)(item.status, false);
75
+ }
76
+ function findLatestStableRecovery(history) {
77
+ for (let index = 0; index < history.length - 1; index += 1) {
78
+ const currentItem = history[index];
79
+ const previousOlderItem = history[index + 1];
80
+ if (isStableHistoryItem(currentItem) && isUnstableHistoryItem(previousOlderItem)) {
81
+ return {
82
+ recovery: currentItem,
83
+ previousUnstable: previousOlderItem,
84
+ };
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function findCurrentStabilityStreak(history) {
90
+ if (history.length === 0) {
91
+ return {
92
+ count: 0,
93
+ latestStable: null,
94
+ oldestStable: null,
95
+ previousUnstable: null,
96
+ };
97
+ }
98
+ let count = 0;
99
+ for (const item of history) {
100
+ if (!isStableHistoryItem(item)) {
101
+ break;
102
+ }
103
+ count += 1;
104
+ }
105
+ if (count === 0) {
106
+ return {
107
+ count: 0,
108
+ latestStable: null,
109
+ oldestStable: null,
110
+ previousUnstable: history[0] ?? null,
111
+ };
112
+ }
113
+ return {
114
+ count,
115
+ latestStable: history[0] ?? null,
116
+ oldestStable: history[count - 1] ?? null,
117
+ previousUnstable: history[count] ?? null,
118
+ };
119
+ }
120
+ function findUnstableStreakBeforeRecovery(history) {
121
+ const recoveryPair = findLatestStableRecovery(history);
122
+ if (!recoveryPair) {
123
+ return null;
124
+ }
125
+ const recoveryIndex = history.findIndex((item) => item.runId === recoveryPair.recovery.runId);
126
+ if (recoveryIndex < 0 || recoveryIndex === history.length - 1) {
127
+ return null;
128
+ }
129
+ const unstableItems = [];
130
+ for (let index = recoveryIndex + 1; index < history.length; index += 1) {
131
+ const currentItem = history[index];
132
+ if (!isUnstableHistoryItem(currentItem)) {
133
+ break;
134
+ }
135
+ unstableItems.push(currentItem);
136
+ }
137
+ if (unstableItems.length === 0) {
138
+ return null;
139
+ }
140
+ return {
141
+ count: unstableItems.length,
142
+ recovery: recoveryPair.recovery,
143
+ latestUnstable: unstableItems[0],
144
+ oldestUnstable: unstableItems[unstableItems.length - 1],
145
+ };
146
+ }
147
+ function formatCurrentStabilityDescription(streak) {
148
+ if (streak.count === 0) {
149
+ return HISTORY_TEXT.texts.streakNotStarted;
150
+ }
151
+ if (!streak.previousUnstable) {
152
+ return formatTemplate(HISTORY_TEXT.texts.streakWholeHistory, { count: String(streak.count) });
153
+ }
154
+ return formatTemplate(HISTORY_TEXT.texts.streakAfterEvent, {
155
+ count: String(streak.count),
156
+ event: getUnstableEventLabel(streak.previousUnstable),
157
+ });
158
+ }
159
+ function normalizeAnchorLookupValue(value) {
160
+ if (typeof value !== 'string') {
161
+ return null;
162
+ }
163
+ const normalized = value.trim().replace(/\s+/g, ' ').toLowerCase();
164
+ return normalized.length > 0 ? normalized : null;
165
+ }
166
+ function buildStepAnchor(runId, attemptNumber, stepIndex) {
167
+ const runSlug = runId.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'run';
168
+ return `step-${runSlug}-a${attemptNumber}-s${stepIndex + 1}`;
169
+ }
170
+ function buildDiagnosticStepTree(steps) {
171
+ const roots = [];
172
+ const nodeStack = [];
173
+ steps.forEach((step, stepIndex) => {
174
+ const node = {
175
+ step,
176
+ stepIndex,
177
+ children: [],
178
+ };
179
+ while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1].depth >= step.depth) {
180
+ nodeStack.pop();
181
+ }
182
+ const parentNode = nodeStack[nodeStack.length - 1]?.node ?? null;
183
+ if (parentNode) {
184
+ parentNode.children.push(node);
185
+ }
186
+ else {
187
+ roots.push(node);
188
+ }
189
+ nodeStack.push({ depth: step.depth, node });
190
+ });
191
+ return roots;
192
+ }
193
+ function findIncidentStepAnchor(history, input) {
194
+ const normalizedTarget = normalizeAnchorLookupValue(input.failureStepTitle);
195
+ if (!normalizedTarget) {
196
+ return null;
197
+ }
198
+ const latestRun = history[0];
199
+ const latestUnstable = getUnstableHistoryItems(history)[0];
200
+ const scopedCandidates = typeof input.failureStepRunId === 'string' && input.failureStepRunId.length > 0
201
+ ? history.filter((item) => item.runId === input.failureStepRunId)
202
+ : [];
203
+ const candidates = (scopedCandidates.length > 0 ? scopedCandidates : [latestRun, latestUnstable]).filter((item, index, collection) => Boolean(item) && collection.findIndex((candidate) => candidate?.runId === item?.runId) === index);
204
+ const normalizedCategory = normalizeAnchorLookupValue(input.failureStepCategory ?? null);
205
+ const normalizedErrorMessage = normalizeAnchorLookupValue(input.failureStepErrorMessage ?? null);
206
+ let bestMatch = null;
207
+ for (const item of candidates) {
208
+ for (const attempt of item.attemptDetails) {
209
+ if (typeof input.failureStepAttempt === 'number' && attempt.attempt !== input.failureStepAttempt) {
210
+ continue;
211
+ }
212
+ for (const [stepIndex, step] of attempt.steps.entries()) {
213
+ if (normalizeAnchorLookupValue(step.title) !== normalizedTarget) {
214
+ continue;
215
+ }
216
+ let score = 1;
217
+ if (typeof input.failureStepOffsetMs === 'number' && step.offsetMs === input.failureStepOffsetMs) {
218
+ score += 16;
219
+ }
220
+ if (normalizedCategory && normalizeAnchorLookupValue(step.category) === normalizedCategory) {
221
+ score += 8;
222
+ }
223
+ if (normalizedErrorMessage && normalizeAnchorLookupValue(step.errorMessage) === normalizedErrorMessage) {
224
+ score += 12;
225
+ }
226
+ if (typeof input.failureStepAttempt === 'number' && attempt.attempt === input.failureStepAttempt) {
227
+ score += 10;
228
+ }
229
+ if (typeof input.failureStepRunId === 'string' && item.runId === input.failureStepRunId) {
230
+ score += 10;
231
+ }
232
+ const anchor = buildStepAnchor(item.runId, attempt.attempt, stepIndex);
233
+ if (!bestMatch || score > bestMatch.score) {
234
+ bestMatch = { anchor, score };
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return bestMatch?.anchor ?? null;
240
+ }
241
+ function getAttachmentReference(attachment) {
242
+ return attachment.path ?? attachment.url ?? attachment.name;
243
+ }
244
+ function buildAttachmentHref(runId, attachment, artifactBasePath) {
245
+ if (attachment.url) {
246
+ return attachment.url;
247
+ }
248
+ if (!artifactBasePath || !attachment.path) {
249
+ return null;
250
+ }
251
+ const normalizedRunId = normalizeArtifactRunDirectory(runId);
252
+ const normalizedPath = attachment.path.replace(/\\/g, '/').replace(/^\/+/, '');
253
+ if (normalizedPath.includes('..') || /^(?:[a-z][a-z0-9+.-]*:|[a-z]:\/)/i.test(normalizedPath)) {
254
+ return null;
255
+ }
256
+ const resolvedArtifactPath = normalizedPath.startsWith(`${normalizedRunId}/`)
257
+ ? normalizedPath
258
+ : `${normalizedRunId}/${normalizedPath}`;
259
+ return `${artifactBasePath}/${encodeURIComponent(runId)}?path=${encodeURIComponent(resolvedArtifactPath)}`;
260
+ }
261
+ function isImageAttachment(attachment) {
262
+ const contentType = attachment.contentType?.toLowerCase() ?? '';
263
+ if (contentType.startsWith('image/')) {
264
+ return true;
265
+ }
266
+ return /\.(avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(getAttachmentReference(attachment));
267
+ }
268
+ function isMarkdownAttachment(attachment) {
269
+ const contentType = attachment.contentType?.toLowerCase() ?? '';
270
+ if (contentType.includes('markdown')) {
271
+ return true;
272
+ }
273
+ return /\.(md|markdown|mdx)$/i.test(getAttachmentReference(attachment));
274
+ }
275
+ function canInlineMarkdownPreview(href) {
276
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(href)) {
277
+ return true;
278
+ }
279
+ if (typeof window === 'undefined') {
280
+ return false;
281
+ }
282
+ try {
283
+ return new URL(href, window.location.href).origin === window.location.origin;
284
+ }
285
+ catch {
286
+ return false;
287
+ }
288
+ }
289
+ function normalizeArtifactRunDirectory(runId) {
290
+ return runId
291
+ .toLowerCase()
292
+ .replace(/[^a-z0-9]+/g, '-')
293
+ .replace(/^-+|-+$/g, '') || 'run';
294
+ }
@@ -0,0 +1,17 @@
1
+ export declare const TEST_HISTORY_METRIC_DESCRIPTIONS: {
2
+ readonly totalRuns: "Количество сохранённых прогонов, в которых найден именно этот тест с учётом текущих фильтров.";
3
+ readonly failedRuns: "Количество прогонов, в которых тест завершился неуспешно: failed, timedout или interrupted.";
4
+ readonly flakyRuns: "Количество прогонов, где тест был отмечен как flaky: падал на одной из попыток, но в итоге завершился успешно.";
5
+ readonly latestStatus: "Финальный статус теста в самом свежем найденном прогоне.";
6
+ readonly passRate: "Доля прогонов этого теста со статусом passed среди всех найденных запусков.";
7
+ readonly failRate: "Доля прогонов этого теста с неуспешным результатом: failed, timedout или interrupted.";
8
+ readonly flakyScore: "Сводная оценка нестабильности теста на шкале 0–100 с учётом fail rate, паттерна нестабильности и MTBF.";
9
+ readonly mtbf: "Среднее время между нестабильными прогонами теста. Чем больше значение, тем реже тест становится нестабильным.";
10
+ readonly timeline: "Хронологическая история прогонов теста с ключевыми метаданными, длительностью, повторами и ошибками.";
11
+ readonly archiveGaps: "Список run id, для которых запись есть в history.json, но архив исходного data.json уже недоступен.";
12
+ readonly latestEvent: "Самый свежий нестабильный эпизод для теста: последнее падение с текстом ошибки или последний flaky-прогон, если более свежей ошибки нет.";
13
+ readonly latestRecovery: "Самый свежий стабильный прогон после нестабильной серии. Показывается только если после падения или flaky был зафиксирован чистый passed-run без flakiness.";
14
+ readonly previousUnstableEvents: "Несколько предыдущих нестабильных эпизодов до самого свежего нестабильного прогона. Полезно для быстрого просмотра паттерна проблем без прокрутки всей таблицы.";
15
+ readonly currentStabilityStreak: "Текущая серия подряд идущих стабильных прогонов от самого свежего запуска назад. Стабильным считается только passed-run без flakiness и без текста ошибки.";
16
+ readonly unstableStreakBeforeRecovery: "Длина нестабильной серии непосредственно перед последним стабильным восстановлением. Помогает понять, какой по глубине был проблемный период до восстановления.";
17
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEST_HISTORY_METRIC_DESCRIPTIONS = void 0;
4
+ exports.TEST_HISTORY_METRIC_DESCRIPTIONS = {
5
+ totalRuns: 'Количество сохранённых прогонов, в которых найден именно этот тест с учётом текущих фильтров.',
6
+ failedRuns: 'Количество прогонов, в которых тест завершился неуспешно: failed, timedout или interrupted.',
7
+ flakyRuns: 'Количество прогонов, где тест был отмечен как flaky: падал на одной из попыток, но в итоге завершился успешно.',
8
+ latestStatus: 'Финальный статус теста в самом свежем найденном прогоне.',
9
+ passRate: 'Доля прогонов этого теста со статусом passed среди всех найденных запусков.',
10
+ failRate: 'Доля прогонов этого теста с неуспешным результатом: failed, timedout или interrupted.',
11
+ flakyScore: 'Сводная оценка нестабильности теста на шкале 0–100 с учётом fail rate, паттерна нестабильности и MTBF.',
12
+ mtbf: 'Среднее время между нестабильными прогонами теста. Чем больше значение, тем реже тест становится нестабильным.',
13
+ timeline: 'Хронологическая история прогонов теста с ключевыми метаданными, длительностью, повторами и ошибками.',
14
+ archiveGaps: 'Список run id, для которых запись есть в history.json, но архив исходного data.json уже недоступен.',
15
+ latestEvent: 'Самый свежий нестабильный эпизод для теста: последнее падение с текстом ошибки или последний flaky-прогон, если более свежей ошибки нет.',
16
+ latestRecovery: 'Самый свежий стабильный прогон после нестабильной серии. Показывается только если после падения или flaky был зафиксирован чистый passed-run без flakiness.',
17
+ previousUnstableEvents: 'Несколько предыдущих нестабильных эпизодов до самого свежего нестабильного прогона. Полезно для быстрого просмотра паттерна проблем без прокрутки всей таблицы.',
18
+ currentStabilityStreak: 'Текущая серия подряд идущих стабильных прогонов от самого свежего запуска назад. Стабильным считается только passed-run без flakiness и без текста ошибки.',
19
+ unstableStreakBeforeRecovery: 'Длина нестабильной серии непосредственно перед последним стабильным восстановлением. Помогает понять, какой по глубине был проблемный период до восстановления.',
20
+ };
@@ -0,0 +1,2 @@
1
+ export declare function escapeHtml(value: unknown): string;
2
+ export declare function normalizeOptionalText(value: string | null | undefined): string | null;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.escapeHtml = escapeHtml;
4
+ exports.normalizeOptionalText = normalizeOptionalText;
5
+ function escapeHtml(value) {
6
+ return String(value ?? '')
7
+ .replace(/&/g, '&amp;')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;')
11
+ .replace(/'/g, '&#39;');
12
+ }
13
+ function normalizeOptionalText(value) {
14
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
15
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@aqa-pulse/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI для загрузки Playwright reports в AQA Pulse",
5
+ "license": "UNLICENSED",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "aqa-pulse": "bin/aqa-pulse.js"
9
+ },
10
+ "files": [
11
+ "bin/**/*",
12
+ "dist/**/*",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "npm --prefix ../../aqa-pulse run compile && node ./scripts/build.js",
17
+ "pack:check": "npm run build && npm pack --dry-run",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "adm-zip": "^0.5.16"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "typescript": {
25
+ "optional": true
26
+ },
27
+ "playwright": {
28
+ "optional": true
29
+ },
30
+ "playwright-core": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "engines": {
35
+ "node": ">=22"
36
+ }
37
+ }