@zohodesk/testinglibrary 0.0.82-n20-experimental → 0.0.83-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 +8 -28
- package/build/core/playwright/builtInFixtures/page.js +1 -0
- package/build/core/playwright/constants/failureTagConstants.js +16 -0
- package/build/core/playwright/constants/logCollectorConstants.js +18 -0
- package/build/core/playwright/helpers/failureTagClassifier.js +166 -0
- package/build/core/playwright/helpers/logCollector.js +20 -49
- package/build/core/playwright/helpers/runtimeLogReader.js +45 -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 +26 -0
- package/build/core/playwright/patterns/toolPatterns.js +36 -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 +15 -0
- package/build/test/core/playwright/helpers/__tests__/failureTagClassifier.test.js +167 -0
- package/build/test/core/playwright/helpers/__tests__/logCollector.test.js +125 -0
- package/build/test/core/playwright/helpers/__tests__/runtimeLogReader.test.js +52 -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
|
};
|
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
3
|
Object.defineProperty(exports, "__esModule", {
|
|
5
4
|
value: true
|
|
6
5
|
});
|
|
7
6
|
exports.default = void 0;
|
|
8
|
-
var _path = _interopRequireDefault(require("path"));
|
|
9
7
|
var _readConfigFile = require("../readConfigFile");
|
|
8
|
+
var _logCollectorConstants = require("../constants/logCollectorConstants");
|
|
10
9
|
var _logCollector = require("../helpers/logCollector");
|
|
11
10
|
const {
|
|
12
11
|
testSetup,
|
|
13
|
-
uatDirectory,
|
|
14
12
|
browserLogs = {}
|
|
15
13
|
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
16
|
-
|
|
17
|
-
// Keep runtime logs OUTSIDE reportPath (HTML reporter wipes it on onEnd) and
|
|
18
|
-
// outside test-results (Playwright outputDir is cleaned each run).
|
|
19
|
-
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
14
|
async function performDefaultContextSteps({
|
|
27
15
|
context
|
|
28
16
|
}) {
|
|
@@ -44,28 +32,20 @@ var _default = exports.default = {
|
|
|
44
32
|
});
|
|
45
33
|
const collector = new _logCollector.LogCollector(browserLogs.limits);
|
|
46
34
|
collector.attachToContext(context);
|
|
47
|
-
testInfo.runtimeLogs = collector;
|
|
48
35
|
try {
|
|
49
36
|
await use(context);
|
|
50
37
|
} finally {
|
|
51
38
|
collector.detach();
|
|
52
39
|
const failureOnly = browserLogs.mode === 'failure-only';
|
|
53
40
|
const isFailure = testInfo.status && testInfo.status !== testInfo.expectedStatus;
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
41
|
+
const shouldAttach = !failureOnly || isFailure;
|
|
42
|
+
const logBody = shouldAttach ? collector.toJsonlBody() : null;
|
|
43
|
+
if (logBody) {
|
|
44
|
+
await testInfo.attach(_logCollectorConstants.RUNTIME_LOG_EVENTS_ATTACHMENT, {
|
|
45
|
+
body: logBody,
|
|
46
|
+
contentType: _logCollectorConstants.RUNTIME_LOG_EVENTS_CONTENT_TYPE
|
|
47
|
+
});
|
|
60
48
|
}
|
|
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
49
|
}
|
|
70
50
|
}
|
|
71
51
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.FAILURE_TAG_SUFFIX = exports.FAILURE_TAGS = void 0;
|
|
7
|
+
const FAILURE_TAGS = exports.FAILURE_TAGS = {
|
|
8
|
+
CODE: 'CODE',
|
|
9
|
+
ENVIRONMENT: 'ENVIRONMENT',
|
|
10
|
+
FLAKY: 'FLAKY',
|
|
11
|
+
TOOL: 'TOOL'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Suffix appended to a category when merging into the existing spec.tags array,
|
|
15
|
+
// e.g. "CODE" -> "CODE_failure". The flaky category is emitted as-is ("FLAKY").
|
|
16
|
+
const FAILURE_TAG_SUFFIX = exports.FAILURE_TAG_SUFFIX = '_failure';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.RUNTIME_LOG_EVENTS_CONTENT_TYPE = exports.RUNTIME_LOG_EVENTS_ATTACHMENT = 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
|
+
const RUNTIME_LOG_EVENTS_ATTACHMENT = exports.RUNTIME_LOG_EVENTS_ATTACHMENT = 'runtime-log-events';
|
|
10
|
+
const RUNTIME_LOG_EVENTS_CONTENT_TYPE = exports.RUNTIME_LOG_EVENTS_CONTENT_TYPE = 'text/plain';
|
|
11
|
+
|
|
12
|
+
// Matched against console-error text, pageerror messages and requestfailed
|
|
13
|
+
|
|
14
|
+
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];
|
|
15
|
+
|
|
16
|
+
// Matched against request/response/requestfailed URLs and console-error source URLs.
|
|
17
|
+
|
|
18
|
+
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,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.classifyFailureTags = classifyFailureTags;
|
|
7
|
+
exports.mergeFailureTags = mergeFailureTags;
|
|
8
|
+
var _failureTagConstants = require("../constants/failureTagConstants");
|
|
9
|
+
var _patterns = require("../patterns");
|
|
10
|
+
var _runtimeLogReader = require("./runtimeLogReader");
|
|
11
|
+
// A failing test is tagged purely on presence: if its runtime log contains an
|
|
12
|
+
// environment pattern it gets `ENVIRONMENT`, if it contains a code pattern it
|
|
13
|
+
// gets `CODE`, and if its stdout/stderr contains a tool pattern it gets `TOOL`
|
|
14
|
+
// (any may apply). `FLAKY` comes from the Playwright retry outcome, not the log.
|
|
15
|
+
|
|
16
|
+
const FAILING_RESULT_STATUSES = new Set(['failed', 'timedOut', 'interrupted']);
|
|
17
|
+
// Playwright TestCase.outcome() value for a failing (non-flaky) test.
|
|
18
|
+
const UNEXPECTED_OUTCOME = 'unexpected';
|
|
19
|
+
const FLAKY_OUTCOME = 'flaky';
|
|
20
|
+
|
|
21
|
+
// Flattens a runtime-log event into a single string for pattern matching. Field
|
|
22
|
+
// order is fixed so patterns can rely on it (e.g. resourceType before failure).
|
|
23
|
+
function eventToText(event) {
|
|
24
|
+
var _event$location;
|
|
25
|
+
switch (event.kind) {
|
|
26
|
+
case 'pageerror':
|
|
27
|
+
return `pageerror ${event.name || ''} ${event.message || ''}`;
|
|
28
|
+
case 'console':
|
|
29
|
+
return `console ${event.text || ''} ${((_event$location = event.location) === null || _event$location === void 0 ? void 0 : _event$location.url) || ''}`;
|
|
30
|
+
case 'requestfailed':
|
|
31
|
+
return `requestfailed ${event.resourceType || ''} ${event.method || ''} ${event.url || ''} ${event.failure || ''}`;
|
|
32
|
+
case 'response':
|
|
33
|
+
return `response ${event.status || ''} ${event.method || ''} ${event.url || ''}`;
|
|
34
|
+
default:
|
|
35
|
+
return JSON.stringify(event);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// True when any event matches any of the given patterns.
|
|
40
|
+
function matchesAny(events, patterns) {
|
|
41
|
+
return events.some(event => {
|
|
42
|
+
const text = eventToText(event);
|
|
43
|
+
return patterns.some(pattern => {
|
|
44
|
+
if (pattern.global || pattern.sticky) {
|
|
45
|
+
pattern.lastIndex = 0;
|
|
46
|
+
}
|
|
47
|
+
return pattern.test(text);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Flattens a result's stdout/stderr chunks into a single string. Each chunk is
|
|
53
|
+
// either a plain string, { text } or { buffer } (base64) as produced by the
|
|
54
|
+
// reporter's stdioEntry.
|
|
55
|
+
function stdioToText(entries = []) {
|
|
56
|
+
return entries.map(entry => {
|
|
57
|
+
if (typeof entry === 'string') {
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
if (!entry || typeof entry !== 'object') {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
if (entry.text) {
|
|
64
|
+
return entry.text;
|
|
65
|
+
}
|
|
66
|
+
if (entry.buffer) {
|
|
67
|
+
try {
|
|
68
|
+
return Buffer.from(entry.buffer, 'base64').toString('utf8');
|
|
69
|
+
} catch {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
}).join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reads a failing result and reports which categories are present. environment
|
|
78
|
+
// and code come from the browser runtime log; tool comes from stdout/stderr.
|
|
79
|
+
function classifyResult(result) {
|
|
80
|
+
const events = (0, _runtimeLogReader.readRuntimeLogEventsFromResult)(result);
|
|
81
|
+
const stdio = `${stdioToText(result.stdout)}\n${stdioToText(result.stderr)}`;
|
|
82
|
+
return {
|
|
83
|
+
environment: matchesAny(events, _patterns.ENVIRONMENT_PATTERNS),
|
|
84
|
+
code: matchesAny(events, _patterns.CODE_PATTERNS),
|
|
85
|
+
tool: _patterns.TOOL_PATTERNS.some(pattern => {
|
|
86
|
+
if (pattern.global || pattern.sticky) {
|
|
87
|
+
pattern.lastIndex = 0;
|
|
88
|
+
}
|
|
89
|
+
return pattern.test(stdio);
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function pickFailingResult(results = []) {
|
|
94
|
+
const failing = results.filter(result => FAILING_RESULT_STATUSES.has(result.status));
|
|
95
|
+
if (failing.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return failing[failing.length - 1];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Returns the ordered list of bare failure tags for a spec's test details.
|
|
102
|
+
// `testDetails` is the array of { status, results } objects under a spec.
|
|
103
|
+
function classifyFailureTags(testDetails = []) {
|
|
104
|
+
const tags = new Set();
|
|
105
|
+
let environment = false;
|
|
106
|
+
let code = false;
|
|
107
|
+
let tool = false;
|
|
108
|
+
for (const detail of testDetails) {
|
|
109
|
+
if (detail.status === FLAKY_OUTCOME) {
|
|
110
|
+
tags.add(_failureTagConstants.FAILURE_TAGS.FLAKY);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (detail.status !== UNEXPECTED_OUTCOME) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const failingResult = pickFailingResult(detail.results);
|
|
117
|
+
if (!failingResult) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const matched = classifyResult(failingResult);
|
|
121
|
+
environment = environment || matched.environment;
|
|
122
|
+
code = code || matched.code;
|
|
123
|
+
tool = tool || matched.tool;
|
|
124
|
+
}
|
|
125
|
+
if (environment) {
|
|
126
|
+
tags.add(_failureTagConstants.FAILURE_TAGS.ENVIRONMENT);
|
|
127
|
+
}
|
|
128
|
+
if (code) {
|
|
129
|
+
tags.add(_failureTagConstants.FAILURE_TAGS.CODE);
|
|
130
|
+
}
|
|
131
|
+
if (tool) {
|
|
132
|
+
tags.add(_failureTagConstants.FAILURE_TAGS.TOOL);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Stable order: flaky, environment, code, tool.
|
|
136
|
+
const ordered = [];
|
|
137
|
+
if (tags.has(_failureTagConstants.FAILURE_TAGS.FLAKY)) ordered.push(_failureTagConstants.FAILURE_TAGS.FLAKY);
|
|
138
|
+
if (tags.has(_failureTagConstants.FAILURE_TAGS.ENVIRONMENT)) ordered.push(_failureTagConstants.FAILURE_TAGS.ENVIRONMENT);
|
|
139
|
+
if (tags.has(_failureTagConstants.FAILURE_TAGS.CODE)) ordered.push(_failureTagConstants.FAILURE_TAGS.CODE);
|
|
140
|
+
if (tags.has(_failureTagConstants.FAILURE_TAGS.TOOL)) ordered.push(_failureTagConstants.FAILURE_TAGS.TOOL);
|
|
141
|
+
return ordered;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Formats a bare category into its emitted tag form (e.g. "CODE" -> "CODE_failure").
|
|
145
|
+
// The flaky category is a passed-on-retry outcome, not a failure, so it is emitted
|
|
146
|
+
// as "FLAKY" without a suffix.
|
|
147
|
+
function formatFailureTag(tag) {
|
|
148
|
+
if (tag === _failureTagConstants.FAILURE_TAGS.FLAKY) {
|
|
149
|
+
return tag;
|
|
150
|
+
}
|
|
151
|
+
return `${tag}${_failureTagConstants.FAILURE_TAG_SUFFIX}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Merges derived failure tags into an existing tags array. Failure tags are
|
|
155
|
+
// placed first, followed by the existing tags, without removing or duplicating
|
|
156
|
+
// entries.
|
|
157
|
+
function mergeFailureTags(existingTags = [], failureTags = []) {
|
|
158
|
+
const formatted = failureTags.map(formatFailureTag);
|
|
159
|
+
const merged = [...formatted];
|
|
160
|
+
for (const tag of existingTags) {
|
|
161
|
+
if (!merged.includes(tag)) {
|
|
162
|
+
merged.push(tag);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
3
|
Object.defineProperty(exports, "__esModule", {
|
|
5
4
|
value: true
|
|
6
5
|
});
|
|
7
6
|
exports.LogCollector = void 0;
|
|
8
|
-
var
|
|
9
|
-
var _fileUtils = require("../../../utils/fileUtils");
|
|
7
|
+
var _logCollectorConstants = require("../constants/logCollectorConstants");
|
|
10
8
|
// Per-context Playwright runtime event collector — network failures,
|
|
11
|
-
// console errors, page errors and crashes. Buffered in-memory and
|
|
12
|
-
// once per test via
|
|
9
|
+
// console errors, page errors and crashes. Buffered in-memory and attached
|
|
10
|
+
// once per test via testInfo.attach.
|
|
13
11
|
|
|
14
12
|
const DEFAULT_CONFIG = {
|
|
15
13
|
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]
|
|
14
|
+
ignoreMessagePatterns: _logCollectorConstants.IGNORE_MESSAGE_PATTERNS,
|
|
15
|
+
ignoreUrlPatterns: _logCollectorConstants.IGNORE_URL_PATTERNS
|
|
20
16
|
};
|
|
21
17
|
class LogCollector {
|
|
22
18
|
constructor(config = {}) {
|
|
@@ -25,21 +21,10 @@ class LogCollector {
|
|
|
25
21
|
...config
|
|
26
22
|
};
|
|
27
23
|
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
24
|
this._bound = [];
|
|
39
25
|
}
|
|
40
26
|
_push(evt) {
|
|
41
27
|
if (this.events.length >= this.config.maxEvents) {
|
|
42
|
-
this.truncated = true;
|
|
43
28
|
this.events.shift();
|
|
44
29
|
}
|
|
45
30
|
this.events.push({
|
|
@@ -48,6 +33,9 @@ class LogCollector {
|
|
|
48
33
|
});
|
|
49
34
|
}
|
|
50
35
|
_shouldIgnore(url) {
|
|
36
|
+
if (!url) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
51
39
|
return this.config.ignoreUrlPatterns.some(re => re.test(url));
|
|
52
40
|
}
|
|
53
41
|
_shouldIgnoreMessage(message) {
|
|
@@ -63,18 +51,11 @@ class LogCollector {
|
|
|
63
51
|
attachToContext(context) {
|
|
64
52
|
context.pages().forEach(p => this._attachPage(p));
|
|
65
53
|
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
54
|
this._bind(context, 'response', res => {
|
|
73
55
|
const url = res.url();
|
|
74
56
|
if (this._shouldIgnore(url)) {
|
|
75
57
|
return;
|
|
76
58
|
}
|
|
77
|
-
this.counts.response++;
|
|
78
59
|
if (res.status() >= 400) {
|
|
79
60
|
this._push({
|
|
80
61
|
kind: 'response',
|
|
@@ -87,12 +68,16 @@ class LogCollector {
|
|
|
87
68
|
});
|
|
88
69
|
this._bind(context, 'requestfailed', req => {
|
|
89
70
|
var _req$failure;
|
|
90
|
-
|
|
71
|
+
const url = req.url();
|
|
72
|
+
const failure = (_req$failure = req.failure()) === null || _req$failure === void 0 ? void 0 : _req$failure.errorText;
|
|
73
|
+
if (this._shouldIgnore(url) || this._shouldIgnoreMessage(failure)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
91
76
|
this._push({
|
|
92
77
|
kind: 'requestfailed',
|
|
93
78
|
method: req.method(),
|
|
94
|
-
url
|
|
95
|
-
failure
|
|
79
|
+
url,
|
|
80
|
+
failure,
|
|
96
81
|
resourceType: req.resourceType()
|
|
97
82
|
});
|
|
98
83
|
});
|
|
@@ -100,27 +85,25 @@ class LogCollector {
|
|
|
100
85
|
_attachPage(page) {
|
|
101
86
|
this._bind(page, 'console', msg => {
|
|
102
87
|
const type = msg.type();
|
|
103
|
-
this.counts.console++;
|
|
104
88
|
if (type !== 'error') {
|
|
105
89
|
return;
|
|
106
90
|
}
|
|
107
91
|
const text = msg.text();
|
|
108
|
-
|
|
92
|
+
const location = msg.location();
|
|
93
|
+
if (this._shouldIgnoreMessage(text) || this._shouldIgnore(location === null || location === void 0 ? void 0 : location.url)) {
|
|
109
94
|
return;
|
|
110
95
|
}
|
|
111
|
-
this.counts.consoleError++;
|
|
112
96
|
this._push({
|
|
113
97
|
kind: 'console',
|
|
114
98
|
level: type,
|
|
115
99
|
text,
|
|
116
|
-
location
|
|
100
|
+
location
|
|
117
101
|
});
|
|
118
102
|
});
|
|
119
103
|
this._bind(page, 'pageerror', err => {
|
|
120
104
|
if (this._shouldIgnoreMessage(err.message)) {
|
|
121
105
|
return;
|
|
122
106
|
}
|
|
123
|
-
this.counts.pageError++;
|
|
124
107
|
this._push({
|
|
125
108
|
kind: 'pageerror',
|
|
126
109
|
name: err.name,
|
|
@@ -129,7 +112,6 @@ class LogCollector {
|
|
|
129
112
|
});
|
|
130
113
|
});
|
|
131
114
|
this._bind(page, 'crash', () => {
|
|
132
|
-
this.counts.crash++;
|
|
133
115
|
this._push({
|
|
134
116
|
kind: 'crash',
|
|
135
117
|
url: page.url()
|
|
@@ -146,22 +128,11 @@ class LogCollector {
|
|
|
146
128
|
}
|
|
147
129
|
this._bound.length = 0;
|
|
148
130
|
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
...this.counts,
|
|
152
|
-
total: this.events.length,
|
|
153
|
-
truncated: this.truncated
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
flushToFile(dir, baseName) {
|
|
131
|
+
toJsonlBody() {
|
|
157
132
|
if (this.events.length === 0) {
|
|
158
133
|
return null;
|
|
159
134
|
}
|
|
160
|
-
|
|
161
|
-
const body = this.events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
162
|
-
(0, _fileUtils.writeFileContents)(file, body);
|
|
163
|
-
this.events.length = 0;
|
|
164
|
-
return file;
|
|
135
|
+
return this.events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
165
136
|
}
|
|
166
137
|
}
|
|
167
138
|
exports.LogCollector = LogCollector;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.readRuntimeLogEventsFromResult = readRuntimeLogEventsFromResult;
|
|
7
|
+
var _logCollectorConstants = require("../constants/logCollectorConstants");
|
|
8
|
+
// Reads runtime-log-events attachments produced by the context fixture.
|
|
9
|
+
// Used by the failure-tag classifier to correlate browser events with a test failure.
|
|
10
|
+
|
|
11
|
+
function attachmentBodyToString(body) {
|
|
12
|
+
if (!body) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
if (Buffer.isBuffer(body)) {
|
|
16
|
+
return body.toString('utf8');
|
|
17
|
+
}
|
|
18
|
+
return String(body);
|
|
19
|
+
}
|
|
20
|
+
function parseJsonlContent(raw) {
|
|
21
|
+
const trimmed = raw.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const events = [];
|
|
26
|
+
for (const line of trimmed.split('\n')) {
|
|
27
|
+
const lineTrimmed = line.trim();
|
|
28
|
+
if (!lineTrimmed) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
events.push(JSON.parse(lineTrimmed));
|
|
33
|
+
} catch {
|
|
34
|
+
// skip malformed line
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return events;
|
|
38
|
+
}
|
|
39
|
+
function readRuntimeLogEventsFromResult(result) {
|
|
40
|
+
const eventsAttachment = ((result === null || result === void 0 ? void 0 : result.attachments) || []).find(item => item.name === _logCollectorConstants.RUNTIME_LOG_EVENTS_ATTACHMENT);
|
|
41
|
+
if (!(eventsAttachment !== null && eventsAttachment !== void 0 && eventsAttachment.body)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return parseJsonlContent(attachmentBodyToString(eventsAttachment.body));
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.CODE_PATTERNS = void 0;
|
|
7
|
+
// Code patterns: real application defects surfacing as JS exceptions in the
|
|
8
|
+
// browser. Matched against a flattened text form of each runtime-log event.
|
|
9
|
+
const CODE_PATTERNS = exports.CODE_PATTERNS = [/Cannot read properties of (undefined|null)/i, /\bis not a function\b/i, /\bis not defined\b/i, /\bis not iterable\b/i];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ENVIRONMENT_PATTERNS = void 0;
|
|
7
|
+
// Environment patterns: infra / network / server breakage. Matched against a
|
|
8
|
+
// flattened text form of each runtime-log event (see eventToText).
|
|
9
|
+
const ENVIRONMENT_PATTERNS = exports.ENVIRONMENT_PATTERNS = [
|
|
10
|
+
// Main document navigation was aborted (page never loaded).
|
|
11
|
+
/^requestfailed document\b[\s\S]*ERR_ABORTED/i,
|
|
12
|
+
// Network / DNS / certificate / connection failures.
|
|
13
|
+
/ERR_TUNNEL_CONNECTION_FAILED|ERR_CERT_|ERR_CONNECTION_|ERR_NAME_NOT_RESOLVED|ERR_INTERNET_DISCONNECTED|ERR_TIMED_OUT|ERR_ADDRESS_UNREACHABLE/i,
|
|
14
|
+
// Server-side 5xx responses.
|
|
15
|
+
/^response 5\d\d\b/i, /status of 5\d\d/i];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "CODE_PATTERNS", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _codePatterns.CODE_PATTERNS;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "ENVIRONMENT_PATTERNS", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _environmentPatterns.ENVIRONMENT_PATTERNS;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "TOOL_PATTERNS", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _toolPatterns.TOOL_PATTERNS;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
var _environmentPatterns = require("./environmentPatterns");
|
|
25
|
+
var _codePatterns = require("./codePatterns");
|
|
26
|
+
var _toolPatterns = require("./toolPatterns");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.TOOL_PATTERNS = void 0;
|
|
7
|
+
// Tool patterns: failures originating from the test tooling / data-generation
|
|
8
|
+
// layer (not the app under test). Matched against the failing result's
|
|
9
|
+
// stdout + stderr text. Add new patterns here to recognise more tool errors.
|
|
10
|
+
const TOOL_PATTERNS = exports.TOOL_PATTERNS = [/status\s*:\s*DATA_GENERATION_ERROR/i, /Error Type\s*:\s*Error/i, /Error during login/i,
|
|
11
|
+
// Generator
|
|
12
|
+
/GeneratorError: Generator/i, /DataGeneratorConfigurationError: Data Generator configuration is missing for the profile/i, /HTTP error! status:/i,
|
|
13
|
+
// Configuration
|
|
14
|
+
/Config File Not Exists\. Please provide a/i, /Exception while getting the uat file from the application/i, /There is no beta feature configured with the name/i, /No config mapping found for stage:/i,
|
|
15
|
+
// Edition
|
|
16
|
+
/edition configured\./i, /portal configured in/i, /profile configured in/i,
|
|
17
|
+
// Actor
|
|
18
|
+
/file missing\./i, /Error loading actor configuration from/i, /The actors data is missing\./i, /not found in user pages\./i,
|
|
19
|
+
// Module
|
|
20
|
+
/does not exist\. We have not triggered the execution for this module/i,
|
|
21
|
+
// Auth
|
|
22
|
+
/Error while parsing cookies/i,
|
|
23
|
+
// FileMutex
|
|
24
|
+
/Watch timeout exceeded/i,
|
|
25
|
+
// Runner
|
|
26
|
+
/Method 'run\(\)' must be implemented\./i, /Invalid runner type/i,
|
|
27
|
+
// Process
|
|
28
|
+
/Child Process Exited with Code/i, /Terminating Playwright Process\.\.\./i, /Cleaning up\.\.\./i,
|
|
29
|
+
// TestData
|
|
30
|
+
/Error appending or creating the test data file:/i, /Error\(err\)/i, /Error while deleting the test data file:/i,
|
|
31
|
+
// FeatureFile
|
|
32
|
+
/Error while parsing feature files\. Please fix the above issues before running test cases/i,
|
|
33
|
+
// RPC
|
|
34
|
+
/Error: HTTP /i, /Invalid payload\. It must be a non-null object\./i, /Invalid payload\.arguments\.entityId\./i,
|
|
35
|
+
// Search
|
|
36
|
+
/Invalid or missing data in dataTable/i, /JSONPath query/i];
|
|
@@ -120,7 +120,7 @@ function combineDefaultConfigWithUserConfig(userConfiguration) {
|
|
|
120
120
|
* @property {Array} editionOrder: Order in the form of larger editions in the back. Edition with the most privelages should be last
|
|
121
121
|
* @property {testSetupConfig} testSetup: Specify page and context functions that will be called while intilaizing fixtures.
|
|
122
122
|
* @property {boolean} showCaseTimings: When true, the console reporter prints per-case start/end wall-clock times (h:mm:ss AM/PM) and a case-timings.json sidecar is written to reportPath for the HTML report addon. Default: true.
|
|
123
|
-
* @property {Object} browserLogs: Runtime browser log capture. { mode: 'failure-only' | 'always', limits?: { maxEvents,
|
|
123
|
+
* @property {Object} browserLogs: Runtime browser log capture. { mode: 'failure-only' | 'always', limits?: { maxEvents, ignoreUrlPatterns, ignoreMessagePatterns } }. ignoreMessagePatterns is a list of RegExp matched against console-error text and pageerror messages; matching events are skipped. Default: { mode: 'failure-only' }.
|
|
124
124
|
*/
|
|
125
125
|
|
|
126
126
|
/**
|
|
@@ -128,15 +128,6 @@ class JSONSummaryReporter {
|
|
|
128
128
|
const isFailure = result.status !== 'passed' && result.status !== 'skipped';
|
|
129
129
|
if (isFailure) {
|
|
130
130
|
var _result$error;
|
|
131
|
-
const runtimeAttachment = (result.attachments || []).find(a => a.name === 'runtime-log' && a.body);
|
|
132
|
-
let runtime = null;
|
|
133
|
-
if (runtimeAttachment) {
|
|
134
|
-
try {
|
|
135
|
-
runtime = JSON.parse(runtimeAttachment.body.toString('utf8'));
|
|
136
|
-
} catch {
|
|
137
|
-
// malformed attachment; skip
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
131
|
this._caseTimings.push({
|
|
141
132
|
title: fullTitle,
|
|
142
133
|
fileName,
|
|
@@ -155,13 +146,7 @@ class JSONSummaryReporter {
|
|
|
155
146
|
title: step.title,
|
|
156
147
|
error: (_step$error = step.error) === null || _step$error === void 0 ? void 0 : _step$error.message
|
|
157
148
|
};
|
|
158
|
-
})
|
|
159
|
-
...(runtime ? {
|
|
160
|
-
runtime: {
|
|
161
|
-
summary: runtime.summary,
|
|
162
|
-
logFile: runtime.file
|
|
163
|
-
}
|
|
164
|
-
} : {})
|
|
149
|
+
})
|
|
165
150
|
});
|
|
166
151
|
}
|
|
167
152
|
}
|
|
@@ -10,7 +10,21 @@ var _path = _interopRequireDefault(require("path"));
|
|
|
10
10
|
var _codeFrame = require("@babel/code-frame");
|
|
11
11
|
var _configConstants = _interopRequireDefault(require("../constants/configConstants"));
|
|
12
12
|
var _ConfigurationHelper = require("../configuration/ConfigurationHelper");
|
|
13
|
+
var _failureTagClassifier = require("../helpers/failureTagClassifier");
|
|
13
14
|
const stage = (0, _ConfigurationHelper.getRunStage)();
|
|
15
|
+
|
|
16
|
+
// Walks the built suite tree and appends derived failure tags (CODE_failure, ENVIRONMENT_failure) into each spec's tags array.
|
|
17
|
+
const applyFailureTags = node => {
|
|
18
|
+
for (const spec of node.specs ?? []) {
|
|
19
|
+
const failureTags = (0, _failureTagClassifier.classifyFailureTags)(spec.tests);
|
|
20
|
+
if (failureTags.length > 0) {
|
|
21
|
+
spec.tags = (0, _failureTagClassifier.mergeFailureTags)(spec.tags, failureTags);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const child of node.suites ?? []) {
|
|
25
|
+
applyFailureTags(child);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
14
28
|
class CustomJsonReporter {
|
|
15
29
|
constructor({
|
|
16
30
|
outputFile = 'test-results.json'
|
|
@@ -52,6 +66,7 @@ class CustomJsonReporter {
|
|
|
52
66
|
const extracted = suite.suites.map(suite => extractMergedSuite(this.report.config.rootDir, suite, this.testResultsById));
|
|
53
67
|
this.report.suites.push(...extracted);
|
|
54
68
|
});
|
|
69
|
+
this.report.suites.forEach(suite => applyFailureTags(suite));
|
|
55
70
|
const writableStream = _fs.default.createWriteStream(this.outputFile);
|
|
56
71
|
writableStream.write('{\n');
|
|
57
72
|
writableStream.write(` "config": ${JSON.stringify(this.report.config, null, 2)},\n`);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _logCollectorConstants = require("../../../../../core/playwright/constants/logCollectorConstants");
|
|
4
|
+
var toolPatterns = _interopRequireWildcard(require("../../../../../core/playwright/patterns/toolPatterns"));
|
|
5
|
+
var _failureTagClassifier = require("../../../../../core/playwright/helpers/failureTagClassifier");
|
|
6
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
7
|
+
function failingDetail(events) {
|
|
8
|
+
const attachments = [];
|
|
9
|
+
if (events.length > 0) {
|
|
10
|
+
attachments.push({
|
|
11
|
+
name: _logCollectorConstants.RUNTIME_LOG_EVENTS_ATTACHMENT,
|
|
12
|
+
contentType: _logCollectorConstants.RUNTIME_LOG_EVENTS_CONTENT_TYPE,
|
|
13
|
+
body: Buffer.from(`${events.map(e => JSON.stringify(e)).join('\n')}\n`)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
status: 'unexpected',
|
|
18
|
+
results: [{
|
|
19
|
+
status: 'failed',
|
|
20
|
+
attachments
|
|
21
|
+
}]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function failingDetailWithStdio({
|
|
25
|
+
stdout = [],
|
|
26
|
+
stderr = []
|
|
27
|
+
}) {
|
|
28
|
+
return {
|
|
29
|
+
status: 'unexpected',
|
|
30
|
+
results: [{
|
|
31
|
+
status: 'failed',
|
|
32
|
+
attachments: [],
|
|
33
|
+
stdout,
|
|
34
|
+
stderr
|
|
35
|
+
}]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
describe('classifyFailureTags', () => {
|
|
39
|
+
test('returns flaky and skips log analysis for flaky outcome', () => {
|
|
40
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([{
|
|
41
|
+
status: 'flaky',
|
|
42
|
+
results: []
|
|
43
|
+
}]);
|
|
44
|
+
expect(tags).toEqual(['FLAKY']);
|
|
45
|
+
});
|
|
46
|
+
test('tags environment when a document abort is present', () => {
|
|
47
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
48
|
+
kind: 'requestfailed',
|
|
49
|
+
url: 'https://app/events/list/commentsubtabevent',
|
|
50
|
+
failure: 'net::ERR_ABORTED',
|
|
51
|
+
resourceType: 'document'
|
|
52
|
+
}])]);
|
|
53
|
+
expect(tags).toEqual(['ENVIRONMENT']);
|
|
54
|
+
});
|
|
55
|
+
test('tags environment for a network/cert failure', () => {
|
|
56
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
57
|
+
kind: 'requestfailed',
|
|
58
|
+
url: 'https://js/gshandler.js',
|
|
59
|
+
failure: 'net::ERR_CERT_COMMON_NAME_INVALID',
|
|
60
|
+
resourceType: 'script'
|
|
61
|
+
}])]);
|
|
62
|
+
expect(tags).toEqual(['ENVIRONMENT']);
|
|
63
|
+
});
|
|
64
|
+
test('tags code when an app undefined-property error is present', () => {
|
|
65
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
66
|
+
kind: 'pageerror',
|
|
67
|
+
name: 'TypeError',
|
|
68
|
+
message: "Cannot read properties of undefined (reading 'id')"
|
|
69
|
+
}])]);
|
|
70
|
+
expect(tags).toEqual(['CODE']);
|
|
71
|
+
});
|
|
72
|
+
test('does not tag a non-document (xhr) abort as environment', () => {
|
|
73
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
74
|
+
kind: 'requestfailed',
|
|
75
|
+
url: 'https://app/api/tickets',
|
|
76
|
+
failure: 'net::ERR_ABORTED',
|
|
77
|
+
resourceType: 'xhr'
|
|
78
|
+
}])]);
|
|
79
|
+
expect(tags).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
test('tags tool when stderr reports a DATA_GENERATION_ERROR', () => {
|
|
82
|
+
const detail = failingDetailWithStdio({
|
|
83
|
+
stderr: [{
|
|
84
|
+
text: 'status : DATA_GENERATION_ERROR\n generator: comments'
|
|
85
|
+
}]
|
|
86
|
+
});
|
|
87
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
88
|
+
expect(tags).toEqual(['TOOL']);
|
|
89
|
+
});
|
|
90
|
+
test('tags tool for an "Error Type:" line in base64 stdout', () => {
|
|
91
|
+
const detail = failingDetailWithStdio({
|
|
92
|
+
stdout: [{
|
|
93
|
+
buffer: Buffer.from('Error Type: Error').toString('base64')
|
|
94
|
+
}]
|
|
95
|
+
});
|
|
96
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
97
|
+
expect(tags).toEqual(['TOOL']);
|
|
98
|
+
});
|
|
99
|
+
test('does not tag tool for benign stdout/stderr', () => {
|
|
100
|
+
const detail = failingDetailWithStdio({
|
|
101
|
+
stdout: [{
|
|
102
|
+
text: 'Generated 5 records successfully'
|
|
103
|
+
}],
|
|
104
|
+
stderr: []
|
|
105
|
+
});
|
|
106
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
107
|
+
expect(tags).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
test('does not throw for malformed stdio chunk entries', () => {
|
|
110
|
+
const detail = failingDetailWithStdio({
|
|
111
|
+
stdout: [null, undefined, 42, {
|
|
112
|
+
text: 'still fine'
|
|
113
|
+
}],
|
|
114
|
+
stderr: [false]
|
|
115
|
+
});
|
|
116
|
+
expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail])).not.toThrow();
|
|
117
|
+
expect((0, _failureTagClassifier.classifyFailureTags)([detail])).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
test('leaves untagged when no environment or code pattern is present', () => {
|
|
120
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
121
|
+
kind: 'response',
|
|
122
|
+
status: 200,
|
|
123
|
+
method: 'GET',
|
|
124
|
+
url: 'https://app/ok'
|
|
125
|
+
}, {
|
|
126
|
+
kind: 'console',
|
|
127
|
+
level: 'error',
|
|
128
|
+
text: 'Some benign log line',
|
|
129
|
+
location: {
|
|
130
|
+
url: 'https://app/page'
|
|
131
|
+
}
|
|
132
|
+
}])]);
|
|
133
|
+
expect(tags).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('mergeFailureTags', () => {
|
|
137
|
+
test('places suffixed failure tags first, then existing tags', () => {
|
|
138
|
+
expect((0, _failureTagClassifier.mergeFailureTags)(['@comments', '@calls'], ['CODE', 'ENVIRONMENT'])).toEqual(['CODE_failure', 'ENVIRONMENT_failure', '@comments', '@calls']);
|
|
139
|
+
});
|
|
140
|
+
test('emits the flaky category as FLAKY without a suffix', () => {
|
|
141
|
+
expect((0, _failureTagClassifier.mergeFailureTags)([], ['FLAKY', 'CODE'])).toEqual(['FLAKY', 'CODE_failure']);
|
|
142
|
+
});
|
|
143
|
+
test('does not duplicate an already-present failure tag', () => {
|
|
144
|
+
expect((0, _failureTagClassifier.mergeFailureTags)(['CODE_failure'], ['CODE'])).toEqual(['CODE_failure']);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('regex safety', () => {
|
|
148
|
+
test('resets stateful regex for tool pattern checks', () => {
|
|
149
|
+
const originalToolPatterns = toolPatterns.TOOL_PATTERNS.slice();
|
|
150
|
+
try {
|
|
151
|
+
toolPatterns.TOOL_PATTERNS.length = 0;
|
|
152
|
+
toolPatterns.TOOL_PATTERNS.push(/DATA_GENERATION_ERROR/g);
|
|
153
|
+
const detail = failingDetailWithStdio({
|
|
154
|
+
stderr: [{
|
|
155
|
+
text: 'status : DATA_GENERATION_ERROR'
|
|
156
|
+
}]
|
|
157
|
+
});
|
|
158
|
+
const first = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
159
|
+
const second = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
160
|
+
expect(first).toEqual(['TOOL']);
|
|
161
|
+
expect(second).toEqual(['TOOL']);
|
|
162
|
+
} finally {
|
|
163
|
+
toolPatterns.TOOL_PATTERNS.length = 0;
|
|
164
|
+
toolPatterns.TOOL_PATTERNS.push(...originalToolPatterns);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _logCollector = require("../../../../../core/playwright/helpers/logCollector");
|
|
4
|
+
function makeEmitter() {
|
|
5
|
+
const handlers = {};
|
|
6
|
+
return {
|
|
7
|
+
handlers,
|
|
8
|
+
on(event, handler) {
|
|
9
|
+
(handlers[event] = handlers[event] || []).push(handler);
|
|
10
|
+
},
|
|
11
|
+
removeListener() {},
|
|
12
|
+
emit(event, arg) {
|
|
13
|
+
(handlers[event] || []).forEach(handler => handler(arg));
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function makeContext() {
|
|
18
|
+
const page = makeEmitter();
|
|
19
|
+
const emitter = makeEmitter();
|
|
20
|
+
return {
|
|
21
|
+
page,
|
|
22
|
+
context: {
|
|
23
|
+
...emitter,
|
|
24
|
+
pages: () => [page]
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const failedRequest = (url, errorText, resourceType = 'script', method = 'GET') => ({
|
|
29
|
+
url: () => url,
|
|
30
|
+
failure: () => ({
|
|
31
|
+
errorText
|
|
32
|
+
}),
|
|
33
|
+
method: () => method,
|
|
34
|
+
resourceType: () => resourceType
|
|
35
|
+
});
|
|
36
|
+
const response = (url, status, method = 'GET') => ({
|
|
37
|
+
url: () => url,
|
|
38
|
+
status: () => status,
|
|
39
|
+
request: () => ({
|
|
40
|
+
method: () => method
|
|
41
|
+
}),
|
|
42
|
+
fromServiceWorker: () => false
|
|
43
|
+
});
|
|
44
|
+
const consoleMessage = (text, url) => ({
|
|
45
|
+
type: () => 'error',
|
|
46
|
+
text: () => text,
|
|
47
|
+
location: () => ({
|
|
48
|
+
url
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
describe('LogCollector baseline-noise stripping', () => {
|
|
52
|
+
let collector;
|
|
53
|
+
let context;
|
|
54
|
+
let page;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
collector = new _logCollector.LogCollector();
|
|
57
|
+
const ctx = makeContext();
|
|
58
|
+
context = ctx.context;
|
|
59
|
+
page = ctx.page;
|
|
60
|
+
collector.attachToContext(context);
|
|
61
|
+
});
|
|
62
|
+
test('drops ORB-blocked request failures (message pattern)', () => {
|
|
63
|
+
context.emit('requestfailed', failedRequest('https://app/some/script.js', 'net::ERR_BLOCKED_BY_ORB'));
|
|
64
|
+
expect(collector.events).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
test('drops known noisy third-party URLs (url pattern)', () => {
|
|
67
|
+
context.emit('requestfailed', failedRequest('https://cdn/murphysdk/3.5.0/murphy.min.js', 'net::ERR_ABORTED'));
|
|
68
|
+
expect(collector.events).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
test('drops noisy 4xx responses by URL (e.g. getHamburgerMenu)', () => {
|
|
71
|
+
context.emit('response', response('https://app/biz/getHamburgerMenu.do?version=stable', 404));
|
|
72
|
+
expect(collector.events).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
test('drops CORS console errors (message pattern)', () => {
|
|
75
|
+
page.emit('console', consoleMessage("Access to font at 'https://x/f.woff' has been blocked by CORS policy: ...", 'https://app/page'));
|
|
76
|
+
expect(collector.events).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
test('drops baseline pageerrors (Access is denied / enable* not a function)', () => {
|
|
79
|
+
page.emit('pageerror', {
|
|
80
|
+
name: 'SecurityError',
|
|
81
|
+
message: 'Failed to read the localStorage: Access is denied for this document.',
|
|
82
|
+
stack: ''
|
|
83
|
+
});
|
|
84
|
+
page.emit('pageerror', {
|
|
85
|
+
name: 'TypeError',
|
|
86
|
+
message: 'n.enableDarkTheme is not a function',
|
|
87
|
+
stack: ''
|
|
88
|
+
});
|
|
89
|
+
expect(collector.events).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
test('keeps a real application pageerror', () => {
|
|
92
|
+
page.emit('pageerror', {
|
|
93
|
+
name: 'TypeError',
|
|
94
|
+
message: "Cannot read properties of undefined (reading 'id')",
|
|
95
|
+
stack: 'TypeError: ...'
|
|
96
|
+
});
|
|
97
|
+
expect(collector.events).toHaveLength(1);
|
|
98
|
+
expect(collector.events[0]).toMatchObject({
|
|
99
|
+
kind: 'pageerror',
|
|
100
|
+
name: 'TypeError'
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
test('serializes captured events as jsonl', () => {
|
|
104
|
+
page.emit('pageerror', {
|
|
105
|
+
name: 'TypeError',
|
|
106
|
+
message: "Cannot read properties of undefined (reading 'id')",
|
|
107
|
+
stack: 'TypeError: ...'
|
|
108
|
+
});
|
|
109
|
+
const body = collector.toJsonlBody();
|
|
110
|
+
expect(body).toContain('"kind":"pageerror"');
|
|
111
|
+
expect(body).toContain("Cannot read properties of undefined (reading 'id')");
|
|
112
|
+
expect(body === null || body === void 0 ? void 0 : body.endsWith('\n')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
test('returns null from toJsonlBody when no events were captured', () => {
|
|
115
|
+
expect(collector.toJsonlBody()).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
test('keeps a document-navigation abort (real environment signal)', () => {
|
|
118
|
+
context.emit('requestfailed', failedRequest('https://app/agent/events/list/commentsubtabevent', 'net::ERR_ABORTED', 'document'));
|
|
119
|
+
expect(collector.events).toHaveLength(1);
|
|
120
|
+
expect(collector.events[0]).toMatchObject({
|
|
121
|
+
kind: 'requestfailed',
|
|
122
|
+
resourceType: 'document'
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _logCollectorConstants = require("../../../../../core/playwright/constants/logCollectorConstants");
|
|
4
|
+
var _runtimeLogReader = require("../../../../../core/playwright/helpers/runtimeLogReader");
|
|
5
|
+
function jsonlBody(events) {
|
|
6
|
+
return Buffer.from(`${events.map(e => JSON.stringify(e)).join('\n')}\n`);
|
|
7
|
+
}
|
|
8
|
+
describe('readRuntimeLogEventsFromResult', () => {
|
|
9
|
+
test('reads inline runtime-log-events body', () => {
|
|
10
|
+
const inlineEvents = [{
|
|
11
|
+
kind: 'pageerror',
|
|
12
|
+
message: 'inline boom'
|
|
13
|
+
}];
|
|
14
|
+
const result = {
|
|
15
|
+
attachments: [{
|
|
16
|
+
name: _logCollectorConstants.RUNTIME_LOG_EVENTS_ATTACHMENT,
|
|
17
|
+
contentType: _logCollectorConstants.RUNTIME_LOG_EVENTS_CONTENT_TYPE,
|
|
18
|
+
body: jsonlBody(inlineEvents)
|
|
19
|
+
}]
|
|
20
|
+
};
|
|
21
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsFromResult)(result)).toEqual(inlineEvents);
|
|
22
|
+
});
|
|
23
|
+
test('skips malformed jsonl lines', () => {
|
|
24
|
+
const raw = [JSON.stringify({
|
|
25
|
+
kind: 'console',
|
|
26
|
+
text: 'ok'
|
|
27
|
+
}), 'not-json', JSON.stringify({
|
|
28
|
+
kind: 'pageerror',
|
|
29
|
+
message: 'err'
|
|
30
|
+
})].join('\n');
|
|
31
|
+
const result = {
|
|
32
|
+
attachments: [{
|
|
33
|
+
name: _logCollectorConstants.RUNTIME_LOG_EVENTS_ATTACHMENT,
|
|
34
|
+
contentType: _logCollectorConstants.RUNTIME_LOG_EVENTS_CONTENT_TYPE,
|
|
35
|
+
body: Buffer.from(`${raw}\n`)
|
|
36
|
+
}]
|
|
37
|
+
};
|
|
38
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsFromResult)(result)).toEqual([{
|
|
39
|
+
kind: 'console',
|
|
40
|
+
text: 'ok'
|
|
41
|
+
}, {
|
|
42
|
+
kind: 'pageerror',
|
|
43
|
+
message: 'err'
|
|
44
|
+
}]);
|
|
45
|
+
});
|
|
46
|
+
test('returns an empty array when runtime-log-events is missing', () => {
|
|
47
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsFromResult)({
|
|
48
|
+
attachments: []
|
|
49
|
+
})).toEqual([]);
|
|
50
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsFromResult)(null)).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zohodesk/testinglibrary",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.83-n20-experimental",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@zohodesk/testinglibrary",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.83-n20-experimental",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -274,4 +274,4 @@ header {
|
|
|
274
274
|
font-size: 1rem;
|
|
275
275
|
padding: 0 0.5rem;
|
|
276
276
|
}
|
|
277
|
-
</style></head><body><main class="jesthtml-content"><header><h1 id="title">Unit Report</h1></header><section id="metadata-container"><div id="timestamp">Started: 2026-07-
|
|
277
|
+
</style></head><body><main class="jesthtml-content"><header><h1 id="title">Unit Report</h1></header><section id="metadata-container"><div id="timestamp">Started: 2026-07-05 00:43:44</div><div id="summary"><div id="suite-summary"><div class="summary-total">Suites (3)</div><div class="summary-passed ">3 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div><div id="test-summary"><div class="summary-total">Tests (26)</div><div class="summary-passed ">26 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div></div></section><details id="suite-1" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Users/muthu-19817/git/testing-framework/src/test/core/playwright/helpers/__tests__/runtimeLogReader.test.js</div><div class="suite-time">0.459s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">readRuntimeLogEventsFromResult</div><div class="test-title">reads inline runtime-log-events body</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">readRuntimeLogEventsFromResult</div><div class="test-title">skips malformed jsonl lines</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">readRuntimeLogEventsFromResult</div><div class="test-title">returns an empty array when runtime-log-events is missing</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div></div></details><details id="suite-2" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Users/muthu-19817/git/testing-framework/src/test/core/playwright/helpers/__tests__/failureTagClassifier.test.js</div><div class="suite-time">0.11s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">returns flaky and skips log analysis for flaky outcome</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">tags environment when a document abort is present</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">tags environment for a network/cert failure</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">tags code when an app undefined-property error is present</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">does not tag a non-document (xhr) abort as environment</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">tags tool when stderr reports a DATA_GENERATION_ERROR</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">tags tool for an "Error Type:" line in base64 stdout</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">does not tag tool for benign stdout/stderr</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">does not throw for malformed stdio chunk entries</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">classifyFailureTags</div><div class="test-title">leaves untagged when no environment or code pattern is present</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">mergeFailureTags</div><div class="test-title">places suffixed failure tags first, then existing tags</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">mergeFailureTags</div><div class="test-title">emits the flaky category as FLAKY without a suffix</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">mergeFailureTags</div><div class="test-title">does not duplicate an already-present failure tag</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">regex safety</div><div class="test-title">resets stateful regex for tool pattern checks</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div></div></details><details id="suite-3" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Users/muthu-19817/git/testing-framework/src/test/core/playwright/helpers/__tests__/logCollector.test.js</div><div class="suite-time">0.082s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">drops ORB-blocked request failures (message pattern)</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">drops known noisy third-party URLs (url pattern)</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">drops noisy 4xx responses by URL (e.g. getHamburgerMenu)</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">drops CORS console errors (message pattern)</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">drops baseline pageerrors (Access is denied / enable* not a function)</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">keeps a real application pageerror</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">serializes captured events as jsonl</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">returns null from toJsonlBody when no events were captured</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">LogCollector baseline-noise stripping</div><div class="test-title">keeps a document-navigation abort (real environment signal)</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div></div></details></main></body></html>
|