@zohodesk/testinglibrary 0.0.82-n20-experimental → 0.0.84-n20-experimental
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/build/core/playwright/builtInFixtures/actorContext.js +10 -13
- package/build/core/playwright/builtInFixtures/context.js +19 -19
- package/build/core/playwright/builtInFixtures/page.js +1 -0
- package/build/core/playwright/constants/failureTagConstants.js +19 -0
- package/build/core/playwright/constants/logCollectorConstants.js +15 -0
- package/build/core/playwright/helpers/failureTagClassifier.js +266 -0
- package/build/core/playwright/helpers/logCollector.js +17 -40
- package/build/core/playwright/helpers/runtimeLogPath.js +61 -0
- package/build/core/playwright/helpers/runtimeLogReader.js +54 -0
- package/build/core/playwright/patterns/casePatterns.js +11 -0
- package/build/core/playwright/patterns/codePatterns.js +9 -0
- package/build/core/playwright/patterns/environmentPatterns.js +15 -0
- package/build/core/playwright/patterns/index.js +33 -0
- package/build/core/playwright/patterns/toolPatterns.js +34 -0
- package/build/core/playwright/readConfigFile.js +1 -1
- package/build/core/playwright/setup/custom-reporter.js +1 -16
- package/build/core/playwright/setup/qc-custom-reporter.js +28 -1
- package/build/test/core/playwright/helpers/__tests__/failureTagClassifier.test.js +413 -0
- package/build/test/core/playwright/helpers/__tests__/logCollector.test.js +139 -0
- package/build/test/core/playwright/helpers/__tests__/runtimeLogReader.test.js +112 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/unit_reports/unit-report.html +1 -1
|
@@ -25,20 +25,20 @@ class ActorContext {
|
|
|
25
25
|
};
|
|
26
26
|
const additionalActors = (0, _additionalProfiles.additionProfiles)($tags);
|
|
27
27
|
await Promise.all(Object.entries(additionalActors).map(async ([role, actorInfo]) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
page
|
|
28
|
+
let context = await browser.newContext();
|
|
29
|
+
let page = await context.newPage();
|
|
30
|
+
let ctxTestDetails = {
|
|
31
|
+
page,
|
|
32
32
|
$tags,
|
|
33
|
-
context
|
|
33
|
+
context,
|
|
34
34
|
...actorInfo
|
|
35
35
|
};
|
|
36
|
-
await (0, _loginDefaultStepsHelper.executeDefaultLoginSteps)(
|
|
36
|
+
await (0, _loginDefaultStepsHelper.executeDefaultLoginSteps)(context, testInfo, ctxTestDetails, actorInfo);
|
|
37
37
|
this.actorsObj[role] = {
|
|
38
38
|
role,
|
|
39
39
|
browser,
|
|
40
|
-
context
|
|
41
|
-
page
|
|
40
|
+
context,
|
|
41
|
+
page,
|
|
42
42
|
executionContext: {
|
|
43
43
|
actorInfo
|
|
44
44
|
}
|
|
@@ -69,10 +69,7 @@ var _default = exports.default = {
|
|
|
69
69
|
}, use, testInfo) => {
|
|
70
70
|
const ctxObject = new ActorContext();
|
|
71
71
|
await ctxObject.setup(browser, $tags, testInfo, context, page, executionContext);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} finally {
|
|
75
|
-
await ctxObject.cleanup();
|
|
76
|
-
}
|
|
72
|
+
await use(ctxObject);
|
|
73
|
+
await ctxObject.cleanup();
|
|
77
74
|
}
|
|
78
75
|
};
|
|
@@ -8,6 +8,8 @@ exports.default = void 0;
|
|
|
8
8
|
var _path = _interopRequireDefault(require("path"));
|
|
9
9
|
var _readConfigFile = require("../readConfigFile");
|
|
10
10
|
var _logCollector = require("../helpers/logCollector");
|
|
11
|
+
var _failureTagClassifier = require("../helpers/failureTagClassifier");
|
|
12
|
+
var _runtimeLogPath = require("../helpers/runtimeLogPath");
|
|
11
13
|
const {
|
|
12
14
|
testSetup,
|
|
13
15
|
uatDirectory,
|
|
@@ -17,12 +19,6 @@ const {
|
|
|
17
19
|
// Keep runtime logs OUTSIDE reportPath (HTML reporter wipes it on onEnd) and
|
|
18
20
|
// outside test-results (Playwright outputDir is cleaned each run).
|
|
19
21
|
const LOGS_DIR = _path.default.join(uatDirectory, 'runtime-logs');
|
|
20
|
-
const MAX_FILENAME_LENGTH = 120;
|
|
21
|
-
function sanitizeForFilename(name) {
|
|
22
|
-
if (!name) return 'test';
|
|
23
|
-
const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, MAX_FILENAME_LENGTH);
|
|
24
|
-
return cleaned || 'test';
|
|
25
|
-
}
|
|
26
22
|
async function performDefaultContextSteps({
|
|
27
23
|
context
|
|
28
24
|
}) {
|
|
@@ -44,7 +40,6 @@ var _default = exports.default = {
|
|
|
44
40
|
});
|
|
45
41
|
const collector = new _logCollector.LogCollector(browserLogs.limits);
|
|
46
42
|
collector.attachToContext(context);
|
|
47
|
-
testInfo.runtimeLogs = collector;
|
|
48
43
|
try {
|
|
49
44
|
await use(context);
|
|
50
45
|
} finally {
|
|
@@ -52,20 +47,25 @@ var _default = exports.default = {
|
|
|
52
47
|
const failureOnly = browserLogs.mode === 'failure-only';
|
|
53
48
|
const isFailure = testInfo.status && testInfo.status !== testInfo.expectedStatus;
|
|
54
49
|
const shouldFlush = !failureOnly || isFailure;
|
|
55
|
-
const summary = collector.toSummary();
|
|
56
|
-
let filePath = null;
|
|
57
50
|
if (shouldFlush) {
|
|
58
|
-
const base =
|
|
59
|
-
|
|
51
|
+
const base = (0, _runtimeLogPath.resolveRuntimeLogBaseName)({
|
|
52
|
+
workerIndex: testInfo.workerIndex,
|
|
53
|
+
retry: testInfo.retry,
|
|
54
|
+
title: testInfo.title,
|
|
55
|
+
titlePath: testInfo.titlePath
|
|
56
|
+
});
|
|
57
|
+
collector.flushToFile(LOGS_DIR, base);
|
|
58
|
+
}
|
|
59
|
+
if (isFailure) {
|
|
60
|
+
const metadata = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)(collector.events);
|
|
61
|
+
const attachment = (0, _failureTagClassifier.buildFailureTagMetadataAttachment)(metadata);
|
|
62
|
+
if (attachment) {
|
|
63
|
+
await testInfo.attach(attachment.name, {
|
|
64
|
+
contentType: attachment.contentType,
|
|
65
|
+
body: attachment.body
|
|
66
|
+
});
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
|
-
await testInfo.attach('runtime-log', {
|
|
62
|
-
contentType: 'application/json',
|
|
63
|
-
body: Buffer.from(JSON.stringify({
|
|
64
|
-
schemaVersion: 1,
|
|
65
|
-
summary,
|
|
66
|
-
file: filePath
|
|
67
|
-
}))
|
|
68
|
-
});
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.FAILURE_TAG_SUFFIX = exports.FAILURE_TAG_METADATA_SCHEMA_VERSION = exports.FAILURE_TAG_METADATA_ATTACHMENT = exports.FAILURE_TAGS = void 0;
|
|
7
|
+
const FAILURE_TAGS = exports.FAILURE_TAGS = {
|
|
8
|
+
CASE: 'CASE',
|
|
9
|
+
CODE: 'CODE',
|
|
10
|
+
ENVIRONMENT: 'ENVIRONMENT',
|
|
11
|
+
FLAKY: 'FLAKY',
|
|
12
|
+
TOOL: 'TOOL'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Suffix appended to a category when merging into the existing spec.tags array,
|
|
16
|
+
// e.g. "CODE" -> "CODE_failure". The flaky category is emitted as-is ("FLAKY").
|
|
17
|
+
const FAILURE_TAG_SUFFIX = exports.FAILURE_TAG_SUFFIX = '_failure';
|
|
18
|
+
const FAILURE_TAG_METADATA_ATTACHMENT = exports.FAILURE_TAG_METADATA_ATTACHMENT = 'failure-tag-metadata';
|
|
19
|
+
const FAILURE_TAG_METADATA_SCHEMA_VERSION = exports.FAILURE_TAG_METADATA_SCHEMA_VERSION = 1;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.IGNORE_URL_PATTERNS = exports.IGNORE_MESSAGE_PATTERNS = void 0;
|
|
7
|
+
// Ignore patterns for the runtime-log LogCollector. Matching events are dropped here so it is never recorded.
|
|
8
|
+
|
|
9
|
+
// Matched against console-error text, pageerror messages and requestfailed
|
|
10
|
+
|
|
11
|
+
const IGNORE_MESSAGE_PATTERNS = exports.IGNORE_MESSAGE_PATTERNS = [/Access is denied for this document/i, /enableDarkTheme is not a function/i, /enableZohoSearchAskZia is not a function/i, /enableSMSBridge is not a function/i, /Unexpected token '<'[\s\S]*not valid JSON/i, /blocked by CORS policy/i, /ERR_BLOCKED_BY_ORB/i];
|
|
12
|
+
|
|
13
|
+
// Matched against request/response/requestfailed URLs and console-error source URLs.
|
|
14
|
+
|
|
15
|
+
const IGNORE_URL_PATTERNS = exports.IGNORE_URL_PATTERNS = [/\.(png|jpe?g|gif|svg|webp|woff2?|ttf|css|map)(\?|$)/i, /google-analytics\.com|googletagmanager\.com|sentry\.io/i, /getHamburgerMenu\.do/i, /_chat\/unreadzmsg\.do/i, /_chat\/chatbarsync\.do/i, /agentAvailabilityConfig/i, /notifyAvailabilityStatus/i, /murphysdk/i, /phonebridge/i, /inproductsdk/i, /zquartz-tracker/i, /zohosearch\/.*gshandler/i, /stratuscdn\.com/i, /zohochat/i];
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.buildFailureTagMetadataAttachment = buildFailureTagMetadataAttachment;
|
|
8
|
+
exports.classifyFailureDetails = classifyFailureDetails;
|
|
9
|
+
exports.classifyFailureDetailsFromEvents = classifyFailureDetailsFromEvents;
|
|
10
|
+
exports.classifyFailureDetailsFromResult = classifyFailureDetailsFromResult;
|
|
11
|
+
exports.classifyFailureTags = classifyFailureTags;
|
|
12
|
+
exports.mergeFailureTags = mergeFailureTags;
|
|
13
|
+
exports.upsertFailureTagMetadataAttachment = upsertFailureTagMetadataAttachment;
|
|
14
|
+
var _path = _interopRequireDefault(require("path"));
|
|
15
|
+
var _failureTagConstants = require("../constants/failureTagConstants");
|
|
16
|
+
var _patterns = require("../patterns");
|
|
17
|
+
var _readConfigFile = require("../readConfigFile");
|
|
18
|
+
var _runtimeLogReader = require("./runtimeLogReader");
|
|
19
|
+
// A failing test is tagged purely on presence: if its runtime log contains an
|
|
20
|
+
// environment pattern it gets `ENVIRONMENT`, if it contains a code pattern it
|
|
21
|
+
// gets `CODE`, and if its stdout/stderr contains a tool pattern it gets `TOOL`
|
|
22
|
+
// or a case pattern it gets `CASE` (any may apply). A `CASE` match also adds
|
|
23
|
+
// `CODE` for now via case-implied metadata. `FLAKY` comes from the Playwright
|
|
24
|
+
// retry outcome, not the log.
|
|
25
|
+
|
|
26
|
+
const FAILING_RESULT_STATUSES = new Set(['failed', 'timedOut', 'interrupted']);
|
|
27
|
+
const UNEXPECTED_OUTCOME = 'unexpected';
|
|
28
|
+
const FLAKY_OUTCOME = 'flaky';
|
|
29
|
+
const RUNTIME_LOG_SOURCE = 'runtime-log';
|
|
30
|
+
const MAX_MATCH_TEXT_LENGTH = 500;
|
|
31
|
+
const TAG_ORDER = [_failureTagConstants.FAILURE_TAGS.FLAKY, _failureTagConstants.FAILURE_TAGS.ENVIRONMENT, _failureTagConstants.FAILURE_TAGS.CASE, _failureTagConstants.FAILURE_TAGS.CODE, _failureTagConstants.FAILURE_TAGS.TOOL];
|
|
32
|
+
const RUNTIME_LOG_RULES = [{
|
|
33
|
+
tag: _failureTagConstants.FAILURE_TAGS.ENVIRONMENT,
|
|
34
|
+
patterns: _patterns.ENVIRONMENT_PATTERNS
|
|
35
|
+
}, {
|
|
36
|
+
tag: _failureTagConstants.FAILURE_TAGS.CODE,
|
|
37
|
+
patterns: _patterns.CODE_PATTERNS
|
|
38
|
+
}];
|
|
39
|
+
const STDIO_RULES = [{
|
|
40
|
+
tag: _failureTagConstants.FAILURE_TAGS.TOOL,
|
|
41
|
+
patterns: _patterns.TOOL_PATTERNS
|
|
42
|
+
}, {
|
|
43
|
+
tag: _failureTagConstants.FAILURE_TAGS.CASE,
|
|
44
|
+
patterns: _patterns.CASE_PATTERNS,
|
|
45
|
+
impliesCode: true
|
|
46
|
+
}];
|
|
47
|
+
function resolveLogsDir(logsDir) {
|
|
48
|
+
if (logsDir) {
|
|
49
|
+
return logsDir;
|
|
50
|
+
}
|
|
51
|
+
const {
|
|
52
|
+
uatDirectory
|
|
53
|
+
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
54
|
+
return _path.default.join(uatDirectory, 'runtime-logs');
|
|
55
|
+
}
|
|
56
|
+
function patternLabel(pattern) {
|
|
57
|
+
return pattern.source;
|
|
58
|
+
}
|
|
59
|
+
function testPattern(pattern, text) {
|
|
60
|
+
if (pattern.global || pattern.sticky) {
|
|
61
|
+
pattern.lastIndex = 0;
|
|
62
|
+
}
|
|
63
|
+
return pattern.test(text);
|
|
64
|
+
}
|
|
65
|
+
function truncateText(text) {
|
|
66
|
+
if (!text || text.length <= MAX_MATCH_TEXT_LENGTH) {
|
|
67
|
+
return text || '';
|
|
68
|
+
}
|
|
69
|
+
return `${text.slice(0, MAX_MATCH_TEXT_LENGTH)}...`;
|
|
70
|
+
}
|
|
71
|
+
function eventToText(event) {
|
|
72
|
+
var _event$location;
|
|
73
|
+
switch (event.kind) {
|
|
74
|
+
case 'pageerror':
|
|
75
|
+
return `pageerror ${event.name || ''} ${event.message || ''}`;
|
|
76
|
+
case 'console':
|
|
77
|
+
return `console ${event.text || ''} ${((_event$location = event.location) === null || _event$location === void 0 ? void 0 : _event$location.url) || ''}`;
|
|
78
|
+
case 'requestfailed':
|
|
79
|
+
return `requestfailed ${event.resourceType || ''} ${event.method || ''} ${event.url || ''} ${event.failure || ''}`;
|
|
80
|
+
case 'response':
|
|
81
|
+
return `response ${event.status || ''} ${event.method || ''} ${event.url || ''}`;
|
|
82
|
+
default:
|
|
83
|
+
return JSON.stringify(event);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function findEventMatch(events, patterns, tag, source) {
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
const text = eventToText(event);
|
|
89
|
+
for (const pattern of patterns) {
|
|
90
|
+
if (testPattern(pattern, text)) {
|
|
91
|
+
return {
|
|
92
|
+
tag,
|
|
93
|
+
pattern: patternLabel(pattern),
|
|
94
|
+
source,
|
|
95
|
+
text: truncateText(text)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function stdioToText(entries = []) {
|
|
103
|
+
return entries.map(entry => {
|
|
104
|
+
if (typeof entry === 'string') {
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
if (!entry || typeof entry !== 'object') {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
if (entry.text) {
|
|
111
|
+
return entry.text;
|
|
112
|
+
}
|
|
113
|
+
if (entry.buffer) {
|
|
114
|
+
try {
|
|
115
|
+
return Buffer.from(entry.buffer, 'base64').toString('utf8');
|
|
116
|
+
} catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return '';
|
|
121
|
+
}).join('\n');
|
|
122
|
+
}
|
|
123
|
+
function buildStdioText(result = {}) {
|
|
124
|
+
return `${stdioToText(result.stdout)}\n${stdioToText(result.stderr)}`;
|
|
125
|
+
}
|
|
126
|
+
function findStdioMatch(stdio, patterns, tag) {
|
|
127
|
+
for (const pattern of patterns) {
|
|
128
|
+
if (testPattern(pattern, stdio)) {
|
|
129
|
+
return {
|
|
130
|
+
tag,
|
|
131
|
+
pattern: patternLabel(pattern),
|
|
132
|
+
source: 'stdio',
|
|
133
|
+
text: truncateText(stdio)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function collectRuntimeLogMatches(events, matches) {
|
|
140
|
+
for (const rule of RUNTIME_LOG_RULES) {
|
|
141
|
+
const match = findEventMatch(events, rule.patterns, rule.tag, RUNTIME_LOG_SOURCE);
|
|
142
|
+
if (match) {
|
|
143
|
+
matches.push(match);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function collectStdioMatches(stdio, matches) {
|
|
148
|
+
for (const rule of STDIO_RULES) {
|
|
149
|
+
const match = findStdioMatch(stdio, rule.patterns, rule.tag);
|
|
150
|
+
if (!match) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
matches.push(match);
|
|
154
|
+
if (rule.impliesCode) {
|
|
155
|
+
implyCodeFromCase(matches, match);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function implyCodeFromCase(matches, caseMatch) {
|
|
160
|
+
if (matches.some(match => match.tag === _failureTagConstants.FAILURE_TAGS.CODE)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
matches.push({
|
|
164
|
+
tag: _failureTagConstants.FAILURE_TAGS.CODE,
|
|
165
|
+
pattern: null,
|
|
166
|
+
source: 'case-implied',
|
|
167
|
+
text: caseMatch.text
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function classifyFailureDetailsFromEvents(events = []) {
|
|
171
|
+
const matches = [];
|
|
172
|
+
collectRuntimeLogMatches(events, matches);
|
|
173
|
+
return buildFailureTagMetadata(matches);
|
|
174
|
+
}
|
|
175
|
+
function classifyFailureDetailsFromResult(result, testTitle, logsDir) {
|
|
176
|
+
const resolvedLogsDir = resolveLogsDir(logsDir);
|
|
177
|
+
const events = (0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, testTitle, resolvedLogsDir);
|
|
178
|
+
const matches = [];
|
|
179
|
+
collectRuntimeLogMatches(events, matches);
|
|
180
|
+
collectStdioMatches(buildStdioText(result), matches);
|
|
181
|
+
return buildFailureTagMetadata(matches);
|
|
182
|
+
}
|
|
183
|
+
function buildFailureTagMetadata(matches = []) {
|
|
184
|
+
const tags = TAG_ORDER.filter(tag => matches.some(match => match.tag === tag));
|
|
185
|
+
return {
|
|
186
|
+
schemaVersion: _failureTagConstants.FAILURE_TAG_METADATA_SCHEMA_VERSION,
|
|
187
|
+
tags,
|
|
188
|
+
matches
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function classifyFailureDetails(testDetails = [], testTitle = 'test', logsDir) {
|
|
192
|
+
const resolvedLogsDir = resolveLogsDir(logsDir);
|
|
193
|
+
const matches = [];
|
|
194
|
+
for (const detail of testDetails) {
|
|
195
|
+
if (detail.status === FLAKY_OUTCOME) {
|
|
196
|
+
matches.push({
|
|
197
|
+
tag: _failureTagConstants.FAILURE_TAGS.FLAKY,
|
|
198
|
+
pattern: null,
|
|
199
|
+
source: 'retry-outcome',
|
|
200
|
+
text: null
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (detail.status !== UNEXPECTED_OUTCOME) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const failingResult = pickFailingResult(detail.results);
|
|
208
|
+
if (!failingResult) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const resultDetails = classifyFailureDetailsFromResult(failingResult, testTitle, resolvedLogsDir);
|
|
212
|
+
matches.push(...resultDetails.matches);
|
|
213
|
+
}
|
|
214
|
+
const uniqueMatches = [];
|
|
215
|
+
for (const match of matches) {
|
|
216
|
+
if (!uniqueMatches.some(existing => existing.tag === match.tag)) {
|
|
217
|
+
uniqueMatches.push(match);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return buildFailureTagMetadata(uniqueMatches);
|
|
221
|
+
}
|
|
222
|
+
function classifyFailureTags(testDetails = [], testTitle = 'test', logsDir) {
|
|
223
|
+
return classifyFailureDetails(testDetails, testTitle, logsDir).tags;
|
|
224
|
+
}
|
|
225
|
+
function pickFailingResult(results = []) {
|
|
226
|
+
const failing = results.filter(result => FAILING_RESULT_STATUSES.has(result.status));
|
|
227
|
+
if (failing.length === 0) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return failing[failing.length - 1];
|
|
231
|
+
}
|
|
232
|
+
function formatFailureTag(tag) {
|
|
233
|
+
if (tag === _failureTagConstants.FAILURE_TAGS.FLAKY) {
|
|
234
|
+
return tag;
|
|
235
|
+
}
|
|
236
|
+
return `${tag}${_failureTagConstants.FAILURE_TAG_SUFFIX}`;
|
|
237
|
+
}
|
|
238
|
+
function mergeFailureTags(existingTags = [], failureTags = []) {
|
|
239
|
+
const formatted = failureTags.map(formatFailureTag);
|
|
240
|
+
const merged = [...formatted];
|
|
241
|
+
for (const tag of existingTags) {
|
|
242
|
+
if (!merged.includes(tag)) {
|
|
243
|
+
merged.push(tag);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return merged;
|
|
247
|
+
}
|
|
248
|
+
function buildFailureTagMetadataAttachment(details) {
|
|
249
|
+
var _details$tags;
|
|
250
|
+
if (!(details !== null && details !== void 0 && (_details$tags = details.tags) !== null && _details$tags !== void 0 && _details$tags.length)) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
name: _failureTagConstants.FAILURE_TAG_METADATA_ATTACHMENT,
|
|
255
|
+
contentType: 'application/json',
|
|
256
|
+
body: Buffer.from(JSON.stringify(details, null, 2))
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function upsertFailureTagMetadataAttachment(attachments = [], details) {
|
|
260
|
+
const metadataAttachment = buildFailureTagMetadataAttachment(details);
|
|
261
|
+
if (!metadataAttachment) {
|
|
262
|
+
return attachments;
|
|
263
|
+
}
|
|
264
|
+
const withoutMetadata = attachments.filter(item => item.name !== _failureTagConstants.FAILURE_TAG_METADATA_ATTACHMENT);
|
|
265
|
+
return [...withoutMetadata, metadataAttachment];
|
|
266
|
+
}
|
|
@@ -7,16 +7,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
7
7
|
exports.LogCollector = void 0;
|
|
8
8
|
var _path = _interopRequireDefault(require("path"));
|
|
9
9
|
var _fileUtils = require("../../../utils/fileUtils");
|
|
10
|
+
var _logCollectorConstants = require("../constants/logCollectorConstants");
|
|
10
11
|
// Per-context Playwright runtime event collector — network failures,
|
|
11
12
|
// console errors, page errors and crashes. Buffered in-memory and flushed
|
|
12
|
-
//
|
|
13
|
+
// to the project runtime-logs folder per test.
|
|
13
14
|
|
|
14
15
|
const DEFAULT_CONFIG = {
|
|
15
16
|
maxEvents: 2000,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
ignoreMessagePatterns: [/n\.enableDarkTheme is not a function/],
|
|
19
|
-
ignoreUrlPatterns: [/\.(png|jpe?g|gif|svg|webp|woff2?|ttf|css|map)(\?|$)/i, /google-analytics\.com|googletagmanager\.com|sentry\.io/i]
|
|
17
|
+
ignoreMessagePatterns: _logCollectorConstants.IGNORE_MESSAGE_PATTERNS,
|
|
18
|
+
ignoreUrlPatterns: _logCollectorConstants.IGNORE_URL_PATTERNS
|
|
20
19
|
};
|
|
21
20
|
class LogCollector {
|
|
22
21
|
constructor(config = {}) {
|
|
@@ -25,21 +24,10 @@ class LogCollector {
|
|
|
25
24
|
...config
|
|
26
25
|
};
|
|
27
26
|
this.events = [];
|
|
28
|
-
this.truncated = false;
|
|
29
|
-
this.counts = {
|
|
30
|
-
console: 0,
|
|
31
|
-
consoleError: 0,
|
|
32
|
-
pageError: 0,
|
|
33
|
-
request: 0,
|
|
34
|
-
response: 0,
|
|
35
|
-
failed: 0,
|
|
36
|
-
crash: 0
|
|
37
|
-
};
|
|
38
27
|
this._bound = [];
|
|
39
28
|
}
|
|
40
29
|
_push(evt) {
|
|
41
30
|
if (this.events.length >= this.config.maxEvents) {
|
|
42
|
-
this.truncated = true;
|
|
43
31
|
this.events.shift();
|
|
44
32
|
}
|
|
45
33
|
this.events.push({
|
|
@@ -48,6 +36,9 @@ class LogCollector {
|
|
|
48
36
|
});
|
|
49
37
|
}
|
|
50
38
|
_shouldIgnore(url) {
|
|
39
|
+
if (!url) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
51
42
|
return this.config.ignoreUrlPatterns.some(re => re.test(url));
|
|
52
43
|
}
|
|
53
44
|
_shouldIgnoreMessage(message) {
|
|
@@ -63,18 +54,11 @@ class LogCollector {
|
|
|
63
54
|
attachToContext(context) {
|
|
64
55
|
context.pages().forEach(p => this._attachPage(p));
|
|
65
56
|
this._bind(context, 'page', p => this._attachPage(p));
|
|
66
|
-
this._bind(context, 'request', req => {
|
|
67
|
-
if (this._shouldIgnore(req.url())) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
this.counts.request++;
|
|
71
|
-
});
|
|
72
57
|
this._bind(context, 'response', res => {
|
|
73
58
|
const url = res.url();
|
|
74
59
|
if (this._shouldIgnore(url)) {
|
|
75
60
|
return;
|
|
76
61
|
}
|
|
77
|
-
this.counts.response++;
|
|
78
62
|
if (res.status() >= 400) {
|
|
79
63
|
this._push({
|
|
80
64
|
kind: 'response',
|
|
@@ -87,12 +71,16 @@ class LogCollector {
|
|
|
87
71
|
});
|
|
88
72
|
this._bind(context, 'requestfailed', req => {
|
|
89
73
|
var _req$failure;
|
|
90
|
-
|
|
74
|
+
const url = req.url();
|
|
75
|
+
const failure = (_req$failure = req.failure()) === null || _req$failure === void 0 ? void 0 : _req$failure.errorText;
|
|
76
|
+
if (this._shouldIgnore(url) || this._shouldIgnoreMessage(failure)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
91
79
|
this._push({
|
|
92
80
|
kind: 'requestfailed',
|
|
93
81
|
method: req.method(),
|
|
94
|
-
url
|
|
95
|
-
failure
|
|
82
|
+
url,
|
|
83
|
+
failure,
|
|
96
84
|
resourceType: req.resourceType()
|
|
97
85
|
});
|
|
98
86
|
});
|
|
@@ -100,27 +88,25 @@ class LogCollector {
|
|
|
100
88
|
_attachPage(page) {
|
|
101
89
|
this._bind(page, 'console', msg => {
|
|
102
90
|
const type = msg.type();
|
|
103
|
-
this.counts.console++;
|
|
104
91
|
if (type !== 'error') {
|
|
105
92
|
return;
|
|
106
93
|
}
|
|
107
94
|
const text = msg.text();
|
|
108
|
-
|
|
95
|
+
const location = msg.location();
|
|
96
|
+
if (this._shouldIgnoreMessage(text) || this._shouldIgnore(location === null || location === void 0 ? void 0 : location.url)) {
|
|
109
97
|
return;
|
|
110
98
|
}
|
|
111
|
-
this.counts.consoleError++;
|
|
112
99
|
this._push({
|
|
113
100
|
kind: 'console',
|
|
114
101
|
level: type,
|
|
115
102
|
text,
|
|
116
|
-
location
|
|
103
|
+
location
|
|
117
104
|
});
|
|
118
105
|
});
|
|
119
106
|
this._bind(page, 'pageerror', err => {
|
|
120
107
|
if (this._shouldIgnoreMessage(err.message)) {
|
|
121
108
|
return;
|
|
122
109
|
}
|
|
123
|
-
this.counts.pageError++;
|
|
124
110
|
this._push({
|
|
125
111
|
kind: 'pageerror',
|
|
126
112
|
name: err.name,
|
|
@@ -129,7 +115,6 @@ class LogCollector {
|
|
|
129
115
|
});
|
|
130
116
|
});
|
|
131
117
|
this._bind(page, 'crash', () => {
|
|
132
|
-
this.counts.crash++;
|
|
133
118
|
this._push({
|
|
134
119
|
kind: 'crash',
|
|
135
120
|
url: page.url()
|
|
@@ -146,13 +131,6 @@ class LogCollector {
|
|
|
146
131
|
}
|
|
147
132
|
this._bound.length = 0;
|
|
148
133
|
}
|
|
149
|
-
toSummary() {
|
|
150
|
-
return {
|
|
151
|
-
...this.counts,
|
|
152
|
-
total: this.events.length,
|
|
153
|
-
truncated: this.truncated
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
134
|
flushToFile(dir, baseName) {
|
|
157
135
|
if (this.events.length === 0) {
|
|
158
136
|
return null;
|
|
@@ -160,7 +138,6 @@ class LogCollector {
|
|
|
160
138
|
const file = _path.default.join(dir, `${baseName}.jsonl`);
|
|
161
139
|
const body = this.events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
162
140
|
(0, _fileUtils.writeFileContents)(file, body);
|
|
163
|
-
this.events.length = 0;
|
|
164
141
|
return file;
|
|
165
142
|
}
|
|
166
143
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.resolveRuntimeLogBaseName = resolveRuntimeLogBaseName;
|
|
8
|
+
exports.resolveRuntimeLogFilePath = resolveRuntimeLogFilePath;
|
|
9
|
+
exports.sanitizeForFilename = sanitizeForFilename;
|
|
10
|
+
var _path = _interopRequireDefault(require("path"));
|
|
11
|
+
const MAX_FILENAME_LENGTH = 200;
|
|
12
|
+
const EXAMPLE_TITLE = /^Example\s*#\d+$/i;
|
|
13
|
+
const SPEC_FILE = /\.(js|ts)$/;
|
|
14
|
+
function sanitizeForFilename(name) {
|
|
15
|
+
if (!name) return 'test';
|
|
16
|
+
const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
17
|
+
return cleaned || 'test';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// playwright-bdd nests Example rows under test.describe(scenario.name).
|
|
21
|
+
function extractOutlineScenarioName(titlePath = []) {
|
|
22
|
+
const segments = titlePath.filter(Boolean);
|
|
23
|
+
if (segments.length < 2 || !EXAMPLE_TITLE.test(segments[segments.length - 1])) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
27
|
+
const segment = segments[i];
|
|
28
|
+
if (segment && !SPEC_FILE.test(segment)) {
|
|
29
|
+
return segment;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function resolveRuntimeLogBaseName({
|
|
35
|
+
workerIndex = 0,
|
|
36
|
+
retry = 0,
|
|
37
|
+
title = 'test',
|
|
38
|
+
titlePath = []
|
|
39
|
+
}) {
|
|
40
|
+
const scenarioName = extractOutlineScenarioName(titlePath);
|
|
41
|
+
const label = scenarioName ? `${scenarioName} ${title}` : title;
|
|
42
|
+
const base = `w${workerIndex}-${sanitizeForFilename(label)}-${retry}`;
|
|
43
|
+
if (base.length <= MAX_FILENAME_LENGTH) {
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
return base.slice(0, MAX_FILENAME_LENGTH).replace(/-$/, '');
|
|
47
|
+
}
|
|
48
|
+
function resolveRuntimeLogFilePath(logsDir, {
|
|
49
|
+
workerIndex,
|
|
50
|
+
retry,
|
|
51
|
+
title,
|
|
52
|
+
titlePath = []
|
|
53
|
+
}) {
|
|
54
|
+
const base = resolveRuntimeLogBaseName({
|
|
55
|
+
workerIndex,
|
|
56
|
+
retry,
|
|
57
|
+
title,
|
|
58
|
+
titlePath
|
|
59
|
+
});
|
|
60
|
+
return _path.default.join(logsDir, `${base}.jsonl`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.readRuntimeLogEvents = readRuntimeLogEvents;
|
|
8
|
+
exports.readRuntimeLogEventsForResult = readRuntimeLogEventsForResult;
|
|
9
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
10
|
+
var _fileUtils = require("../../../utils/fileUtils");
|
|
11
|
+
var _runtimeLogPath = require("./runtimeLogPath");
|
|
12
|
+
// Reads per-test runtime browser logs from the project runtime-logs folder.
|
|
13
|
+
// Used by the failure-tag classifier to correlate browser events with a test failure.
|
|
14
|
+
|
|
15
|
+
function parseJsonlContent(raw) {
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const events = [];
|
|
21
|
+
for (const line of trimmed.split('\n')) {
|
|
22
|
+
const lineTrimmed = line.trim();
|
|
23
|
+
if (!lineTrimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
events.push(JSON.parse(lineTrimmed));
|
|
28
|
+
} catch {
|
|
29
|
+
// skip malformed line
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return events;
|
|
33
|
+
}
|
|
34
|
+
function readRuntimeLogEvents(filePath) {
|
|
35
|
+
if (!filePath || !_fs.default.existsSync(filePath)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const rawContent = (0, _fileUtils.readFileContents)(filePath);
|
|
39
|
+
if (typeof rawContent !== 'string') {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return parseJsonlContent(rawContent);
|
|
43
|
+
}
|
|
44
|
+
function readRuntimeLogEventsForResult(result, testTitle, logsDir) {
|
|
45
|
+
if (!logsDir) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const filePath = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(logsDir, {
|
|
49
|
+
workerIndex: (result === null || result === void 0 ? void 0 : result.workerIndex) ?? 0,
|
|
50
|
+
retry: (result === null || result === void 0 ? void 0 : result.retry) ?? 0,
|
|
51
|
+
title: testTitle
|
|
52
|
+
});
|
|
53
|
+
return readRuntimeLogEvents(filePath);
|
|
54
|
+
}
|