@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.
@@ -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
- const additionalContext = await browser.newContext();
29
- const additionalPage = await additionalContext.newPage();
30
- const ctxTestDetails = {
31
- page: additionalPage,
28
+ let context = await browser.newContext();
29
+ let page = await context.newPage();
30
+ let ctxTestDetails = {
31
+ page,
32
32
  $tags,
33
- context: additionalContext,
33
+ context,
34
34
  ...actorInfo
35
35
  };
36
- await (0, _loginDefaultStepsHelper.executeDefaultLoginSteps)(additionalContext, testInfo, ctxTestDetails, actorInfo);
36
+ await (0, _loginDefaultStepsHelper.executeDefaultLoginSteps)(context, testInfo, ctxTestDetails, actorInfo);
37
37
  this.actorsObj[role] = {
38
38
  role,
39
39
  browser,
40
- context: additionalContext,
41
- page: additionalPage,
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
- try {
73
- await use(ctxObject);
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 = `w${testInfo.workerIndex}-${sanitizeForFilename(testInfo.title)}-${testInfo.retry}`;
59
- filePath = collector.flushToFile(LOGS_DIR, base);
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
  };
@@ -32,6 +32,7 @@ var _default = exports.default = {
32
32
  } finally {
33
33
  await (0, _loginDefaultStepsHelper.performDefaultPageSteps)(testDetails);
34
34
  await use(page);
35
+ await context.close();
35
36
  }
36
37
  }
37
38
  };
@@ -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
- // once per test via writeFileContents.
13
+ // to the project runtime-logs folder per test.
13
14
 
14
15
  const DEFAULT_CONFIG = {
15
16
  maxEvents: 2000,
16
- maxBodyBytes: 0,
17
- captureHeaders: false,
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
- this.counts.failed++;
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: req.url(),
95
- failure: (_req$failure = req.failure()) === null || _req$failure === void 0 ? void 0 : _req$failure.errorText,
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
- if (this._shouldIgnoreMessage(text)) {
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: msg.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
+ }