@testomatio/reporter 2.20.0 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/adapter/codecept.js +25 -2
- package/lib/adapter/e2e.js +5 -1
- package/lib/adapter/playwright.js +32 -1
- package/lib/adapter/vitest.js +4 -1
- package/lib/adapter/webdriver.js +44 -2
- package/lib/bin/cli.js +180 -6
- package/lib/fetch/report.d.ts +31 -0
- package/lib/fetch/report.js +81 -0
- package/lib/fetch/testruns.d.ts +133 -0
- package/lib/fetch/testruns.js +286 -0
- package/lib/pipe/markdown.d.ts +1 -0
- package/lib/pipe/markdown.js +7 -0
- package/lib/reporter-functions.js +39 -0
- package/lib/uploader.d.ts +16 -0
- package/lib/uploader.js +54 -0
- package/lib/utils/detectRunner.d.ts +23 -0
- package/lib/utils/detectRunner.js +149 -0
- package/package.json +2 -2
- package/src/adapter/codecept.js +22 -2
- package/src/adapter/e2e.js +4 -1
- package/src/adapter/playwright.js +31 -1
- package/src/adapter/vitest.js +4 -1
- package/src/adapter/webdriver.js +44 -2
- package/src/bin/cli.js +216 -7
- package/src/fetch/report.js +86 -0
- package/src/fetch/testruns.js +283 -0
- package/src/pipe/markdown.js +7 -0
- package/src/reporter-functions.js +41 -0
- package/src/uploader.js +51 -0
- package/src/utils/detectRunner.js +151 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isPlaceholderCommand = isPlaceholderCommand;
|
|
7
|
+
exports.detectRunner = detectRunner;
|
|
8
|
+
exports.runnerHint = runnerHint;
|
|
9
|
+
exports.buildFilterWarning = buildFilterWarning;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
13
|
+
const LAUNCHER_PREFIXES = ['npx', 'npm', 'yarn', 'pnpm', 'bun', 'node', 'bunx'];
|
|
14
|
+
const PLACEHOLDER_PHRASE_RE = /^<?-*(?:run|your|test)\s+(?:[\w-]+\s+)?(?:tests?|command|runner)>?$/i;
|
|
15
|
+
const RUNNERS = [
|
|
16
|
+
{
|
|
17
|
+
name: 'Playwright',
|
|
18
|
+
configs: ['playwright.config.js', 'playwright.config.ts', 'playwright.config.mjs', 'playwright.config.cjs'],
|
|
19
|
+
dependencies: ['@playwright/test'],
|
|
20
|
+
command: 'npx playwright test',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'CodeceptJS',
|
|
24
|
+
configs: ['codecept.conf.js', 'codecept.conf.ts'],
|
|
25
|
+
dependencies: ['codeceptjs'],
|
|
26
|
+
command: 'npx codeceptjs run',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'WebdriverIO',
|
|
30
|
+
configs: ['wdio.conf.js', 'wdio.conf.ts'],
|
|
31
|
+
dependencies: ['@wdio/cli'],
|
|
32
|
+
dependencyPrefix: '@wdio/',
|
|
33
|
+
command: 'npx wdio run ./wdio.conf.js',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Cypress',
|
|
37
|
+
configs: ['cypress.config.js', 'cypress.config.ts'],
|
|
38
|
+
dependencies: ['cypress'],
|
|
39
|
+
command: 'npx cypress run',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Jest',
|
|
43
|
+
configs: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'jest.config.json'],
|
|
44
|
+
dependencies: ['jest'],
|
|
45
|
+
command: 'npx jest',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Vitest',
|
|
49
|
+
configs: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'],
|
|
50
|
+
dependencies: ['vitest'],
|
|
51
|
+
command: 'npx vitest run',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Mocha',
|
|
55
|
+
configs: ['.mocharc.js', '.mocharc.cjs', '.mocharc.json', '.mocharc.yml', '.mocharc.yaml'],
|
|
56
|
+
dependencies: ['mocha'],
|
|
57
|
+
command: 'npx mocha',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Cucumber',
|
|
61
|
+
configs: ['cucumber.js', 'cucumber.cjs'],
|
|
62
|
+
dependencies: ['@cucumber/cucumber'],
|
|
63
|
+
command: 'npx cucumber-js',
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} command
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function isPlaceholderCommand(command) {
|
|
71
|
+
const cmd = (command || '').trim();
|
|
72
|
+
if (!cmd)
|
|
73
|
+
return false;
|
|
74
|
+
if (cmd.includes('<') || cmd.includes('>'))
|
|
75
|
+
return true;
|
|
76
|
+
const firstToken = cmd.split(/\s+/)[0].toLowerCase();
|
|
77
|
+
if (LAUNCHER_PREFIXES.includes(firstToken))
|
|
78
|
+
return false;
|
|
79
|
+
return PLACEHOLDER_PHRASE_RE.test(cmd);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} cwd
|
|
83
|
+
* @returns {Record<string, string>}
|
|
84
|
+
*/
|
|
85
|
+
function readPackageDependencies(cwd) {
|
|
86
|
+
try {
|
|
87
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, 'package.json'), 'utf8'));
|
|
88
|
+
return { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} [cwd]
|
|
96
|
+
* @returns {{ name: string, command: string } | null}
|
|
97
|
+
*/
|
|
98
|
+
function detectRunner(cwd = process.cwd()) {
|
|
99
|
+
const dependencies = readPackageDependencies(cwd);
|
|
100
|
+
for (const runner of RUNNERS) {
|
|
101
|
+
const config = runner.configs.find(file => fs_1.default.existsSync(path_1.default.join(cwd, file)));
|
|
102
|
+
const hasDependency = runner.dependencies?.some(dep => dependencies[dep])
|
|
103
|
+
|| (runner.dependencyPrefix && Object.keys(dependencies).some(dep => dep.startsWith(runner.dependencyPrefix)));
|
|
104
|
+
if (!config && !hasDependency)
|
|
105
|
+
continue;
|
|
106
|
+
let command = runner.command;
|
|
107
|
+
if (runner.name === 'WebdriverIO') {
|
|
108
|
+
command = `npx wdio run ./${config || 'wdio.conf.js'}`;
|
|
109
|
+
}
|
|
110
|
+
return { name: runner.name, command };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} [cwd]
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
function runnerHint(cwd = process.cwd()) {
|
|
119
|
+
const runner = detectRunner(cwd);
|
|
120
|
+
if (runner) {
|
|
121
|
+
return `💡 Looks like this project uses ${runner.name}. Try: ${picocolors_1.default.green(runner.command)}`;
|
|
122
|
+
}
|
|
123
|
+
return `💡 Replace the placeholder with your test runner command, e.g. ${picocolors_1.default.green('npx playwright test')}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} command
|
|
127
|
+
* @returns {string[]}
|
|
128
|
+
*/
|
|
129
|
+
function buildFilterWarning(command) {
|
|
130
|
+
return [
|
|
131
|
+
'⚠️ The runner exited with a non-zero code while a Testomat.io filter was applied.',
|
|
132
|
+
'Your command was modified to run only tests matching Testomat IDs:',
|
|
133
|
+
` ${picocolors_1.default.bold(command)}`,
|
|
134
|
+
'This usually means no local tests matched the filter:',
|
|
135
|
+
' - tests are not annotated with the matching @Txxxxxx IDs in their titles',
|
|
136
|
+
' - the filter selected tests from another repository or branch',
|
|
137
|
+
' - the test runner does not support the appended --grep / --testNamePattern option',
|
|
138
|
+
'💡 Hint: run the printed command without the appended filter option to see all your tests,',
|
|
139
|
+
'then annotate them with Testomat IDs.',
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports.isPlaceholderCommand = isPlaceholderCommand;
|
|
144
|
+
|
|
145
|
+
module.exports.detectRunner = detectRunner;
|
|
146
|
+
|
|
147
|
+
module.exports.runnerHint = runnerHint;
|
|
148
|
+
|
|
149
|
+
module.exports.buildFilterWarning = buildFilterWarning;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"jasmine": "^5.2.0",
|
|
103
103
|
"jest": "^27.4.7",
|
|
104
104
|
"jsdom": "^22.1.0",
|
|
105
|
-
"mocha": "^
|
|
105
|
+
"mocha": "^12.0.3",
|
|
106
106
|
"mock-http-server": "^1.4.5",
|
|
107
107
|
"pino": "^8.15.0",
|
|
108
108
|
"prettier": "^3.2.5",
|
package/src/adapter/codecept.js
CHANGED
|
@@ -20,7 +20,7 @@ if (!global.codeceptjs) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// @ts-ignore
|
|
23
|
-
const { event, recorder, codecept, output } = global.codeceptjs;
|
|
23
|
+
const { event, recorder, codecept, output, container } = global.codeceptjs;
|
|
24
24
|
|
|
25
25
|
const [, MAJOR_VERSION, MINOR_VERSION] = codecept
|
|
26
26
|
.version()
|
|
@@ -59,6 +59,7 @@ function CodeceptReporter(config) {
|
|
|
59
59
|
let isRunFinalized = false;
|
|
60
60
|
|
|
61
61
|
const testTimeMap = {};
|
|
62
|
+
const failedUrls = new Map();
|
|
62
63
|
const clientConfig = buildCodeceptClientConfig(config);
|
|
63
64
|
const client = new TestomatClient(clientConfig);
|
|
64
65
|
|
|
@@ -243,6 +244,22 @@ function CodeceptReporter(config) {
|
|
|
243
244
|
}, true);
|
|
244
245
|
});
|
|
245
246
|
|
|
247
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
248
|
+
// no browser in suite hooks
|
|
249
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return;
|
|
250
|
+
const helper = Object.values(container.helpers()).find(h => typeof h.grabCurrentUrl === 'function');
|
|
251
|
+
if (!helper) return;
|
|
252
|
+
|
|
253
|
+
recorder.add('url of failed test', async () => {
|
|
254
|
+
try {
|
|
255
|
+
const url = await helper.grabCurrentUrl();
|
|
256
|
+
if (url) failedUrls.set(test.uid, url);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
debug('Failed to grab url of failed test', err?.message || err);
|
|
259
|
+
}
|
|
260
|
+
}, true);
|
|
261
|
+
});
|
|
262
|
+
|
|
246
263
|
event.dispatcher.on(event.test.skipped, test => {
|
|
247
264
|
const { uid, tags, title } = test.simplify();
|
|
248
265
|
|
|
@@ -274,6 +291,9 @@ function CodeceptReporter(config) {
|
|
|
274
291
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
275
292
|
const links = services.links.get(test.fullTitle());
|
|
276
293
|
const screenshotOnFailPath = artifacts.screenshot || null;
|
|
294
|
+
const meta = { ...keyValues, ...test.meta };
|
|
295
|
+
if (test.state === STATUS.FAILED && failedUrls.has(uid)) meta.failedUrl = failedUrls.get(uid);
|
|
296
|
+
failedUrls.delete(uid);
|
|
277
297
|
|
|
278
298
|
// Build step hierarchy with screenshot from screenshotOnFail
|
|
279
299
|
const stepHierarchy = buildUnifiedStepHierarchy(
|
|
@@ -297,7 +317,7 @@ function CodeceptReporter(config) {
|
|
|
297
317
|
logs,
|
|
298
318
|
links,
|
|
299
319
|
manuallyAttachedArtifacts,
|
|
300
|
-
meta
|
|
320
|
+
meta,
|
|
301
321
|
});
|
|
302
322
|
reportTestPromises.push(reportTestPromise);
|
|
303
323
|
|
package/src/adapter/e2e.js
CHANGED
|
@@ -94,6 +94,8 @@ export function toTestData(result, groups, artifactsRoot) {
|
|
|
94
94
|
if (result.repeat) meta.repeat = result.repeat;
|
|
95
95
|
if (attempts.length > 1) meta.retryCount = attempts.length - 1;
|
|
96
96
|
if (result.status === 'flaky') meta.isFlaky = true;
|
|
97
|
+
const status = testStatus(result.status);
|
|
98
|
+
if (status === STATUS.FAILED && final?.failure?.url) meta.failedUrl = final.failure.url;
|
|
97
99
|
|
|
98
100
|
const filesById = new Map();
|
|
99
101
|
for (const artifact of final?.artifacts || []) {
|
|
@@ -128,7 +130,7 @@ export function toTestData(result, groups, artifactsRoot) {
|
|
|
128
130
|
meta,
|
|
129
131
|
};
|
|
130
132
|
|
|
131
|
-
return { status
|
|
133
|
+
return { status, data };
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
/** Attempts of a result; a serial member keeps its part of the group's attempts. */
|
|
@@ -140,6 +142,7 @@ function attemptsOf(result, groups) {
|
|
|
140
142
|
return {
|
|
141
143
|
status: member?.status || attempt.status,
|
|
142
144
|
error: member?.error || attempt.error,
|
|
145
|
+
failure: member?.failure || attempt.failure,
|
|
143
146
|
steps: member?.steps || [],
|
|
144
147
|
appLog: member?.appLog || [],
|
|
145
148
|
skip: member?.skip,
|
|
@@ -110,7 +110,11 @@ class PlaywrightReporter {
|
|
|
110
110
|
if (result.status === 'passed') status = 'failed';
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
const
|
|
113
|
+
const testStatus = checkStatus(status);
|
|
114
|
+
let failedUrl;
|
|
115
|
+
if (testStatus === Status.FAILED) failedUrl = lastNavigatedUrl(result.steps, test.parent.project().use.baseURL);
|
|
116
|
+
|
|
117
|
+
const reportTestPromise = this.client.addTestRun(testStatus, {
|
|
114
118
|
rid: `${rid}-${project.name}`,
|
|
115
119
|
error,
|
|
116
120
|
test_id: getTestomatIdFromTestTitle(`${title} ${tags.join(' ')}`),
|
|
@@ -128,6 +132,7 @@ class PlaywrightReporter {
|
|
|
128
132
|
isMobile: project.isMobile,
|
|
129
133
|
project: project.name,
|
|
130
134
|
projectDependencies: project.dependencies?.length ? project.dependencies : null,
|
|
135
|
+
failedUrl,
|
|
131
136
|
...testMeta,
|
|
132
137
|
...meta,
|
|
133
138
|
...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
|
|
@@ -244,6 +249,31 @@ function isScreenshotArtifact(attachment) {
|
|
|
244
249
|
return attachment?.contentType === 'image/png' && attachment?.name === 'screenshot';
|
|
245
250
|
}
|
|
246
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Last url the test navigated to; the reporter has no access to the page itself.
|
|
254
|
+
* @returns {string|undefined}
|
|
255
|
+
*/
|
|
256
|
+
function lastNavigatedUrl(steps, baseURL) {
|
|
257
|
+
const url = findLastNavigation(steps);
|
|
258
|
+
if (!url) return;
|
|
259
|
+
try {
|
|
260
|
+
return new URL(url, baseURL).href;
|
|
261
|
+
} catch {
|
|
262
|
+
return url;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function findLastNavigation(steps = []) {
|
|
267
|
+
let url;
|
|
268
|
+
for (const step of steps) {
|
|
269
|
+
// 'Navigate to "url"' since Playwright 1.53, 'page.goto(url)' before
|
|
270
|
+
const match = step.title.match(/^Navigate to "(.+)"$/) || step.title.match(/^page\.goto\((.+)\)$/);
|
|
271
|
+
if (step.category === 'pw:api' && match) url = match[1];
|
|
272
|
+
url = findLastNavigation(step.steps) || url;
|
|
273
|
+
}
|
|
274
|
+
return url;
|
|
275
|
+
}
|
|
276
|
+
|
|
247
277
|
function appendStep(step, shift = 0) {
|
|
248
278
|
// nesting too deep, ignore those steps
|
|
249
279
|
if (shift >= 10) return;
|
package/src/adapter/vitest.js
CHANGED
|
@@ -398,7 +398,10 @@ function normalizeVitestTest(test) {
|
|
|
398
398
|
file,
|
|
399
399
|
suiteTitle,
|
|
400
400
|
logs: '',
|
|
401
|
-
meta:
|
|
401
|
+
meta: {
|
|
402
|
+
...(test.task?.meta || {}),
|
|
403
|
+
...(typeof test.meta === 'function' ? test.meta() : test.meta || {}),
|
|
404
|
+
},
|
|
402
405
|
};
|
|
403
406
|
}
|
|
404
407
|
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -4,6 +4,9 @@ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
|
4
4
|
import { services } from '../services/index.js';
|
|
5
5
|
import { TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
|
|
6
6
|
import { stringToMD5Hash } from '../data-storage.js';
|
|
7
|
+
import createDebugMessages from 'debug';
|
|
8
|
+
|
|
9
|
+
const debug = createDebugMessages('@testomatio/reporter:adapter:webdriver');
|
|
7
10
|
|
|
8
11
|
global.TESTOMATIO_ADAPTER = 'webdriverio';
|
|
9
12
|
|
|
@@ -122,7 +125,8 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
122
125
|
async addTest(test) {
|
|
123
126
|
if (!this.client) return;
|
|
124
127
|
|
|
125
|
-
const { title, _duration: duration, state, error, output, links, artifacts,
|
|
128
|
+
const { title, _duration: duration, state, error, output, links, artifacts, logs } = test;
|
|
129
|
+
let { meta } = test;
|
|
126
130
|
|
|
127
131
|
const testId = getTestomatIdFromTestTitle(title);
|
|
128
132
|
|
|
@@ -133,6 +137,11 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
133
137
|
|
|
134
138
|
const rid = stringToMD5Hash(test.fullTitle);
|
|
135
139
|
|
|
140
|
+
if (state === 'failed') {
|
|
141
|
+
const failedUrl = (await grabCurrentUrl()) || lastNavigatedUrl(output);
|
|
142
|
+
if (failedUrl) meta = { ...meta, failedUrl };
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
await this.client.addTestRun(state, {
|
|
137
146
|
rid,
|
|
138
147
|
manuallyAttachedArtifacts: test.artifacts,
|
|
@@ -150,7 +159,7 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
150
159
|
/**
|
|
151
160
|
* @param {import('../../types/types.js').WebdriverIOScenario} scenario
|
|
152
161
|
*/
|
|
153
|
-
addBddScenario(scenario) {
|
|
162
|
+
async addBddScenario(scenario) {
|
|
154
163
|
if (!this.client) return;
|
|
155
164
|
|
|
156
165
|
const { title, _duration: duration } = scenario;
|
|
@@ -169,6 +178,12 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
169
178
|
|
|
170
179
|
const tags = scenario.tags.map(tag => tag.name);
|
|
171
180
|
|
|
181
|
+
let meta;
|
|
182
|
+
if (scenarioState === 'failed') {
|
|
183
|
+
const failedUrl = await grabCurrentUrl();
|
|
184
|
+
if (failedUrl) meta = { failedUrl };
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
return this.client.addTestRun(scenarioState, {
|
|
173
188
|
error: error ? Error(error) : null,
|
|
174
189
|
title,
|
|
@@ -176,11 +191,38 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
176
191
|
time: duration,
|
|
177
192
|
tags,
|
|
178
193
|
file: scenario.file,
|
|
194
|
+
meta,
|
|
179
195
|
// filesBuffers: screenshotsBuffers,
|
|
180
196
|
});
|
|
181
197
|
}
|
|
182
198
|
}
|
|
183
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Current url of the browser in this worker; reporters run in the worker process.
|
|
202
|
+
* @returns {Promise<string|undefined>}
|
|
203
|
+
*/
|
|
204
|
+
async function grabCurrentUrl() {
|
|
205
|
+
// @ts-ignore
|
|
206
|
+
const browser = globalThis.browser;
|
|
207
|
+
if (typeof browser?.getUrl !== 'function') return;
|
|
208
|
+
try {
|
|
209
|
+
const url = await browser.getUrl();
|
|
210
|
+
// multiremote returns a url per browser
|
|
211
|
+
if (typeof url === 'string') return url;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
debug('Failed to grab url of failed test', err?.message || err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Last url opened by the test, taken from its command log.
|
|
219
|
+
* @returns {string|undefined}
|
|
220
|
+
*/
|
|
221
|
+
function lastNavigatedUrl(output = []) {
|
|
222
|
+
const navigations = output.filter(el => el.endpoint === '/session/:sessionId/url' && el.body?.url);
|
|
223
|
+
return navigations.at(-1)?.body.url;
|
|
224
|
+
}
|
|
225
|
+
|
|
184
226
|
/**
|
|
185
227
|
*
|
|
186
228
|
* @param {*} fullTestTitle
|