@wdio/nightwatch-devtools 1.0.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/README.md +127 -0
- package/dist/constants.d.ts +57 -0
- package/dist/constants.js +97 -0
- package/dist/constants.js.map +1 -0
- package/dist/helpers/browserProxy.d.ts +46 -0
- package/dist/helpers/browserProxy.js +314 -0
- package/dist/helpers/browserProxy.js.map +1 -0
- package/dist/helpers/capturePerformance.d.ts +5 -0
- package/dist/helpers/capturePerformance.js +53 -0
- package/dist/helpers/capturePerformance.js.map +1 -0
- package/dist/helpers/cucumberHooks.cjs +54 -0
- package/dist/helpers/cucumberHooks.cjs.map +1 -0
- package/dist/helpers/cucumberHooks.d.cts +21 -0
- package/dist/helpers/suiteManager.d.ts +43 -0
- package/dist/helpers/suiteManager.js +143 -0
- package/dist/helpers/suiteManager.js.map +1 -0
- package/dist/helpers/testManager.d.ts +45 -0
- package/dist/helpers/testManager.js +143 -0
- package/dist/helpers/testManager.js.map +1 -0
- package/dist/helpers/utils.d.ts +83 -0
- package/dist/helpers/utils.js +442 -0
- package/dist/helpers/utils.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +796 -0
- package/dist/index.js.map +1 -0
- package/dist/reporter.d.ts +44 -0
- package/dist/reporter.js +185 -0
- package/dist/reporter.js.map +1 -0
- package/dist/session.d.ts +78 -0
- package/dist/session.js +643 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nightwatch DevTools Plugin
|
|
3
|
+
*
|
|
4
|
+
* Integrates Nightwatch with WebdriverIO DevTools following the WDIO service pattern.
|
|
5
|
+
* Captures commands, network requests, and console logs during test execution in real-time.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { start, stop } from '@wdio/devtools-backend';
|
|
12
|
+
import logger from '@wdio/logger';
|
|
13
|
+
import { remote } from 'webdriverio';
|
|
14
|
+
import { SessionCapturer } from './session.js';
|
|
15
|
+
import { TestReporter } from './reporter.js';
|
|
16
|
+
import { TestManager } from './helpers/testManager.js';
|
|
17
|
+
import { SuiteManager } from './helpers/suiteManager.js';
|
|
18
|
+
import { BrowserProxy } from './helpers/browserProxy.js';
|
|
19
|
+
import { TraceType } from './types.js';
|
|
20
|
+
import { determineTestState, findTestFileFromStack, deterministicUid, extractTestMetadata, parseCucumberScenario, findStepDefinitionLine, findFreePort, resolveNightwatchConfig } from './helpers/utils.js';
|
|
21
|
+
import { DEFAULTS, TIMING, TEST_STATE, PLUGIN_GLOBAL_KEY } from './constants.js';
|
|
22
|
+
const log = logger('@wdio/nightwatch-devtools');
|
|
23
|
+
class NightwatchDevToolsPlugin {
|
|
24
|
+
options;
|
|
25
|
+
sessionCapturer;
|
|
26
|
+
testReporter;
|
|
27
|
+
testManager;
|
|
28
|
+
suiteManager;
|
|
29
|
+
browserProxy;
|
|
30
|
+
isScriptInjected = false;
|
|
31
|
+
#currentTest = null;
|
|
32
|
+
#currentScenarioSuite = null;
|
|
33
|
+
#currentStep = null;
|
|
34
|
+
#lastSessionId = null;
|
|
35
|
+
#devtoolsBrowser;
|
|
36
|
+
#userDataDir;
|
|
37
|
+
#isCucumberRunner = false;
|
|
38
|
+
#passCount = 0;
|
|
39
|
+
#failCount = 0;
|
|
40
|
+
#skipCount = 0;
|
|
41
|
+
#configPath;
|
|
42
|
+
#srcFolders = [];
|
|
43
|
+
#getRerunLabel() {
|
|
44
|
+
return process.env.DEVTOOLS_RERUN_ENTRY_TYPE === 'test'
|
|
45
|
+
? process.env.DEVTOOLS_RERUN_LABEL?.trim()
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.options = {
|
|
50
|
+
port: options.port ?? 3000,
|
|
51
|
+
hostname: options.hostname ?? 'localhost'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async before() {
|
|
55
|
+
// When relaunched by the DevTools UI rerun button the backend is already
|
|
56
|
+
// running — skip startup and just connect the WebSocket worker.
|
|
57
|
+
const isReuse = process.env.DEVTOOLS_APP_REUSE === '1' &&
|
|
58
|
+
process.env.DEVTOOLS_APP_HOST &&
|
|
59
|
+
process.env.DEVTOOLS_APP_PORT;
|
|
60
|
+
if (isReuse) {
|
|
61
|
+
this.options.hostname = process.env.DEVTOOLS_APP_HOST;
|
|
62
|
+
this.options.port = Number(process.env.DEVTOOLS_APP_PORT);
|
|
63
|
+
log.info(`♻ Reusing DevTools backend at ${this.options.hostname}:${this.options.port}`);
|
|
64
|
+
// Clear execution data from the previous run when rerunning
|
|
65
|
+
// This ensures test names cache and suites are fresh for the new run
|
|
66
|
+
if (this.testReporter) {
|
|
67
|
+
this.testReporter.clearExecutionData();
|
|
68
|
+
this.suiteManager.clearExecutionData();
|
|
69
|
+
// Reset counters for fresh run
|
|
70
|
+
this.#passCount = 0;
|
|
71
|
+
this.#failCount = 0;
|
|
72
|
+
this.#skipCount = 0;
|
|
73
|
+
log.info('Cleared execution data for rerun');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.#configPath = resolveNightwatchConfig();
|
|
77
|
+
if (this.#configPath) {
|
|
78
|
+
log.info(`✓ Config: ${this.#configPath}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
log.warn('Could not find nightwatch config — test rerun will be unavailable');
|
|
82
|
+
}
|
|
83
|
+
if (isReuse) {
|
|
84
|
+
// Register the plugin instance so Cucumber hooks can call back into it.
|
|
85
|
+
;
|
|
86
|
+
globalThis[PLUGIN_GLOBAL_KEY] = this;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
this.options.port = await findFreePort(this.options.port, this.options.hostname);
|
|
91
|
+
log.info('🚀 Starting DevTools backend...');
|
|
92
|
+
await start(this.options);
|
|
93
|
+
const url = `http://${this.options.hostname}:${this.options.port}`;
|
|
94
|
+
log.info(`✓ Backend started on port ${this.options.port}`);
|
|
95
|
+
log.info(` DevTools UI: ${url}`);
|
|
96
|
+
try {
|
|
97
|
+
// Create unique user data directory for this instance to prevent conflicts
|
|
98
|
+
this.#userDataDir = path.join(os.tmpdir(), `nightwatch-devtools-${this.options.port}-${Date.now()}`);
|
|
99
|
+
if (!fs.existsSync(this.#userDataDir)) {
|
|
100
|
+
fs.mkdirSync(this.#userDataDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
this.#devtoolsBrowser = await remote({
|
|
103
|
+
logLevel: 'info',
|
|
104
|
+
automationProtocol: 'devtools',
|
|
105
|
+
capabilities: {
|
|
106
|
+
browserName: 'chrome',
|
|
107
|
+
'goog:chromeOptions': {
|
|
108
|
+
args: [
|
|
109
|
+
'--window-size=1600,1200',
|
|
110
|
+
`--user-data-dir=${this.#userDataDir}`,
|
|
111
|
+
'--no-first-run',
|
|
112
|
+
'--no-default-browser-check'
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
await this.#devtoolsBrowser.url(url);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
log.error(`Failed to open DevTools UI: ${err.message}`);
|
|
121
|
+
log.info(`Please manually open: ${url}`);
|
|
122
|
+
}
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.UI_CONNECTION_WAIT));
|
|
124
|
+
globalThis[PLUGIN_GLOBAL_KEY] = this;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
log.error(`Failed to start backend: ${err.message}`);
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async #ensureSessionInitialized(browser) {
|
|
132
|
+
const currentSessionId = browser.sessionId;
|
|
133
|
+
const isSessionChange = currentSessionId &&
|
|
134
|
+
this.#lastSessionId &&
|
|
135
|
+
currentSessionId !== this.#lastSessionId;
|
|
136
|
+
if (isSessionChange) {
|
|
137
|
+
log.info('Browser session changed — reconnecting WebSocket only');
|
|
138
|
+
this.isScriptInjected = false;
|
|
139
|
+
this.sessionCapturer?.cleanup();
|
|
140
|
+
this.sessionCapturer = null;
|
|
141
|
+
}
|
|
142
|
+
this.#lastSessionId = currentSessionId ?? null;
|
|
143
|
+
if (this.sessionCapturer) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.INITIAL_CONNECTION_WAIT));
|
|
147
|
+
this.sessionCapturer = new SessionCapturer({ port: this.options.port, hostname: this.options.hostname }, browser);
|
|
148
|
+
const connected = await this.sessionCapturer.waitForConnection(3000);
|
|
149
|
+
if (!connected) {
|
|
150
|
+
log.error('❌ Worker WebSocket failed to connect!');
|
|
151
|
+
}
|
|
152
|
+
if (!this.testReporter) {
|
|
153
|
+
// First-time setup: create reporter chain once for the entire run.
|
|
154
|
+
// These must NOT be recreated on session change — doing so generates a
|
|
155
|
+
// new feature suite with a fresh start timestamp, which DataManager sees
|
|
156
|
+
// as a new run and wipes all accumulated commands.
|
|
157
|
+
this.testReporter = new TestReporter((suitesData) => {
|
|
158
|
+
if (this.sessionCapturer) {
|
|
159
|
+
this.sessionCapturer.sendUpstream('suites', suitesData);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
this.testManager = new TestManager(this.testReporter);
|
|
163
|
+
this.suiteManager = new SuiteManager(this.testReporter);
|
|
164
|
+
this.browserProxy = new BrowserProxy(this.sessionCapturer, this.testManager, () => this.#currentTest ?? this.#currentScenarioSuite);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Session change: update the reporter's upstream callback to use the new
|
|
168
|
+
// WebSocket, update the proxy's capturer reference (avoids re-wrapping
|
|
169
|
+
// already-wrapped browser methods which would double-capture commands),
|
|
170
|
+
// then replay current suite state to the newly-connected UI.
|
|
171
|
+
this.testReporter.updateUpstream((suitesData) => {
|
|
172
|
+
if (this.sessionCapturer) {
|
|
173
|
+
this.sessionCapturer.sendUpstream('suites', suitesData);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
this.browserProxy.updateSessionCapturer(this.sessionCapturer);
|
|
177
|
+
this.testReporter.updateSuites();
|
|
178
|
+
}
|
|
179
|
+
const capabilities = browser.capabilities || {};
|
|
180
|
+
const desiredCapabilities = browser.desiredCapabilities || {};
|
|
181
|
+
const sessionId = browser.sessionId;
|
|
182
|
+
const opts = browser.options || {};
|
|
183
|
+
// Capture src_folders once so beforeEach can resolve test file paths
|
|
184
|
+
if (this.#srcFolders.length === 0) {
|
|
185
|
+
const sf = opts.src_folders;
|
|
186
|
+
this.#srcFolders = Array.isArray(sf) ? sf : sf ? [sf] : [];
|
|
187
|
+
}
|
|
188
|
+
this.sessionCapturer.sendUpstream('metadata', {
|
|
189
|
+
type: TraceType.Testrunner,
|
|
190
|
+
capabilities,
|
|
191
|
+
desiredCapabilities,
|
|
192
|
+
sessionId,
|
|
193
|
+
testEnv: opts.testEnv,
|
|
194
|
+
host: opts.webdriver?.host,
|
|
195
|
+
options: this.#buildMetadataOptions(),
|
|
196
|
+
url: ''
|
|
197
|
+
});
|
|
198
|
+
const browserName = capabilities.browserName || desiredCapabilities.browserName || 'unknown';
|
|
199
|
+
const browserVersion = capabilities.browserVersion || capabilities.version || '';
|
|
200
|
+
log.info(`✓ Browser: ${browserName}${browserVersion ? ' ' + browserVersion : ''} (session: ${sessionId})`);
|
|
201
|
+
const loggingPrefs = capabilities['goog:loggingPrefs'] ||
|
|
202
|
+
desiredCapabilities['goog:loggingPrefs'] ||
|
|
203
|
+
{};
|
|
204
|
+
if (!loggingPrefs.performance) {
|
|
205
|
+
log.warn("⚠ Network tab will be empty — add 'goog:loggingPrefs': { performance: 'ALL' } to your capabilities");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async cucumberBefore(browser, pickle) {
|
|
209
|
+
this.#isCucumberRunner = true;
|
|
210
|
+
await this.#initCucumberScenario(browser, pickle);
|
|
211
|
+
}
|
|
212
|
+
async cucumberAfter(browser, result, pickle) {
|
|
213
|
+
await this.#finalizeCucumberScenario(browser, result, pickle);
|
|
214
|
+
}
|
|
215
|
+
/** Called from Cucumber Before hook (order:1000) — one call per scenario. */
|
|
216
|
+
async #initCucumberScenario(browser, pickle) {
|
|
217
|
+
await this.#ensureSessionInitialized(browser);
|
|
218
|
+
const featureUri = pickle.uri ?? 'unknown.feature';
|
|
219
|
+
const scenarioName = pickle.name ?? 'Unknown Scenario';
|
|
220
|
+
// Derive the feature name from the "Feature: <name>" header in the file,
|
|
221
|
+
// falling back to the filename (e.g. "login") only if the file can't be read.
|
|
222
|
+
let featureName = path.basename(featureUri, '.feature');
|
|
223
|
+
let featureContent = '';
|
|
224
|
+
const featureAbsPath = path.resolve(process.cwd(), featureUri);
|
|
225
|
+
const stepDefFiles = [];
|
|
226
|
+
if (featureUri !== 'unknown.feature' && fs.existsSync(featureAbsPath)) {
|
|
227
|
+
featureContent = fs.readFileSync(featureAbsPath, 'utf-8');
|
|
228
|
+
const match = featureContent.match(/^\s*Feature:\s*(.+)/m);
|
|
229
|
+
if (match) {
|
|
230
|
+
featureName = match[1].trim();
|
|
231
|
+
}
|
|
232
|
+
this.sessionCapturer.captureSource(featureAbsPath).catch(() => { });
|
|
233
|
+
// Capture step definitions from sibling directories
|
|
234
|
+
const featureDir = path.dirname(featureAbsPath);
|
|
235
|
+
const stepDirCandidates = ['step_definitions', 'steps', 'support'];
|
|
236
|
+
for (const candidate of stepDirCandidates) {
|
|
237
|
+
const stepDir = path.join(featureDir, candidate);
|
|
238
|
+
if (fs.existsSync(stepDir) && fs.statSync(stepDir).isDirectory()) {
|
|
239
|
+
for (const entry of fs.readdirSync(stepDir)) {
|
|
240
|
+
if (/\.(js|ts|mjs|cjs)$/.test(entry)) {
|
|
241
|
+
const stepFilePath = path.join(stepDir, entry);
|
|
242
|
+
this.sessionCapturer.captureSource(stepFilePath).catch(() => { });
|
|
243
|
+
try {
|
|
244
|
+
stepDefFiles.push({
|
|
245
|
+
filePath: stepFilePath,
|
|
246
|
+
content: fs.readFileSync(stepFilePath, 'utf-8')
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// skip unreadable files
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Get or create the feature-level suite (no individual test names — scenarios go into suites[])
|
|
258
|
+
const featureSuite = this.suiteManager.getOrCreateSuite(featureUri, featureName, featureUri, []);
|
|
259
|
+
this.suiteManager.markSuiteAsRunning(featureSuite);
|
|
260
|
+
// Parse step keywords from the feature file
|
|
261
|
+
const steps = pickle.steps ?? [];
|
|
262
|
+
// Parse line numbers and keywords for TestLens navigation and step labels
|
|
263
|
+
const { featureLine, scenarioLine, stepLines, stepKeywords } = parseCucumberScenario(featureContent, scenarioName, steps.map((s) => s.text));
|
|
264
|
+
if (featureAbsPath && featureLine > 0) {
|
|
265
|
+
featureSuite.callSource = `${featureAbsPath}:${featureLine}`;
|
|
266
|
+
}
|
|
267
|
+
// Create a scenario sub-suite (child of feature suite).
|
|
268
|
+
// Use deterministicUid (no counter) so the SAME scenario always maps to the
|
|
269
|
+
// SAME uid across retries, enabling correct retry detection below.
|
|
270
|
+
const scenarioUid = deterministicUid(featureUri, `scenario:${scenarioName}`);
|
|
271
|
+
const scenarioSuite = {
|
|
272
|
+
uid: scenarioUid,
|
|
273
|
+
cid: DEFAULTS.CID,
|
|
274
|
+
title: scenarioName,
|
|
275
|
+
fullTitle: `${featureName} ${scenarioName}`,
|
|
276
|
+
parent: featureSuite.uid,
|
|
277
|
+
type: 'suite',
|
|
278
|
+
file: featureUri,
|
|
279
|
+
start: new Date(),
|
|
280
|
+
state: 'running',
|
|
281
|
+
end: null,
|
|
282
|
+
tests: [],
|
|
283
|
+
suites: [],
|
|
284
|
+
hooks: [],
|
|
285
|
+
_duration: 0,
|
|
286
|
+
callSource: featureAbsPath && scenarioLine > 0
|
|
287
|
+
? `${featureAbsPath}:${scenarioLine}`
|
|
288
|
+
: undefined
|
|
289
|
+
};
|
|
290
|
+
// Create a TestStats entry for each step
|
|
291
|
+
steps.forEach((step, i) => {
|
|
292
|
+
const keyword = stepKeywords[i] || '';
|
|
293
|
+
const stepLabel = keyword ? `${keyword} ${step.text}` : step.text;
|
|
294
|
+
const stepUid = deterministicUid(featureUri, `step:${scenarioName}:${step.text}`);
|
|
295
|
+
scenarioSuite.tests.push({
|
|
296
|
+
uid: stepUid,
|
|
297
|
+
cid: DEFAULTS.CID,
|
|
298
|
+
title: stepLabel,
|
|
299
|
+
fullTitle: `${scenarioName} ${stepLabel}`,
|
|
300
|
+
parent: scenarioUid,
|
|
301
|
+
state: 'pending',
|
|
302
|
+
start: new Date(),
|
|
303
|
+
end: null,
|
|
304
|
+
type: 'test',
|
|
305
|
+
file: featureUri,
|
|
306
|
+
retries: 0,
|
|
307
|
+
_duration: 0,
|
|
308
|
+
hooks: [],
|
|
309
|
+
callSource: (() => {
|
|
310
|
+
const loc = findStepDefinitionLine(stepDefFiles, step.text);
|
|
311
|
+
if (loc) {
|
|
312
|
+
return `${loc.filePath}:${loc.line}`;
|
|
313
|
+
}
|
|
314
|
+
if (featureAbsPath && stepLines[i] > 0) {
|
|
315
|
+
return `${featureAbsPath}:${stepLines[i]}`;
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
})()
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
// Add scenario sub-suite to the feature suite.
|
|
322
|
+
// If a suite with this uid already exists it means this is a RETRY of the same
|
|
323
|
+
// scenario — clear execution data so only the latest attempt's commands are shown.
|
|
324
|
+
const existingIdx = featureSuite.suites.findIndex((s) => s.uid === scenarioUid);
|
|
325
|
+
const isRetry = existingIdx !== -1;
|
|
326
|
+
if (isRetry) {
|
|
327
|
+
featureSuite.suites[existingIdx] = scenarioSuite;
|
|
328
|
+
// Pass the specific scenario uid so only this scenario's execution data
|
|
329
|
+
// is reset — a uid-less clearExecutionData would mark ALL suites as
|
|
330
|
+
// running, destroying the previous terminal states of sibling scenarios.
|
|
331
|
+
this.sessionCapturer.sendUpstream('clearExecutionData', {
|
|
332
|
+
uid: scenarioUid,
|
|
333
|
+
entryType: 'suite'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
featureSuite.suites.push(scenarioSuite);
|
|
338
|
+
}
|
|
339
|
+
this.#currentScenarioSuite = scenarioSuite;
|
|
340
|
+
this.#currentStep = null;
|
|
341
|
+
this.#currentTest = null;
|
|
342
|
+
this.testReporter.updateSuites();
|
|
343
|
+
if (!this.isScriptInjected) {
|
|
344
|
+
this.browserProxy.wrapUrlMethod(browser);
|
|
345
|
+
this.isScriptInjected = true;
|
|
346
|
+
}
|
|
347
|
+
this.browserProxy.wrapBrowserCommands(browser);
|
|
348
|
+
this.browserProxy.resetCommandTracking();
|
|
349
|
+
log.info(`🥒 Scenario: ${scenarioName}`);
|
|
350
|
+
}
|
|
351
|
+
/** Called from Cucumber After hook (order:1000) — one call per scenario. */
|
|
352
|
+
async #finalizeCucumberScenario(browser, result, pickle) {
|
|
353
|
+
try {
|
|
354
|
+
const status = String(result?.status ?? 'UNKNOWN').toUpperCase();
|
|
355
|
+
const scenarioState = status === 'PASSED'
|
|
356
|
+
? TEST_STATE.PASSED
|
|
357
|
+
: status === 'SKIPPED'
|
|
358
|
+
? TEST_STATE.SKIPPED
|
|
359
|
+
: TEST_STATE.FAILED;
|
|
360
|
+
if (this.#currentScenarioSuite) {
|
|
361
|
+
const duration = Date.now() -
|
|
362
|
+
(this.#currentScenarioSuite.start?.getTime() ?? Date.now());
|
|
363
|
+
this.#currentScenarioSuite.state = scenarioState;
|
|
364
|
+
this.#currentScenarioSuite.end = new Date();
|
|
365
|
+
this.#currentScenarioSuite._duration = duration;
|
|
366
|
+
// Ensure any still-running or pending steps are marked appropriately
|
|
367
|
+
for (const step of this.#currentScenarioSuite.tests) {
|
|
368
|
+
if (typeof step !== 'string' &&
|
|
369
|
+
(step.state === 'running' || step.state === 'pending')) {
|
|
370
|
+
step.state =
|
|
371
|
+
scenarioState === TEST_STATE.PASSED
|
|
372
|
+
? TEST_STATE.PASSED
|
|
373
|
+
: TEST_STATE.FAILED;
|
|
374
|
+
step.end = new Date();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const featureUri = pickle?.uri ?? 'unknown.feature';
|
|
378
|
+
this.testManager.markTestAsProcessed(featureUri, pickle?.name ?? '');
|
|
379
|
+
const featureSuite = this.suiteManager.getSuite(featureUri);
|
|
380
|
+
if (featureSuite) {
|
|
381
|
+
// Finalize is not called until all scenarios are done — just update state
|
|
382
|
+
this.suiteManager.finalizeSuiteState(featureSuite);
|
|
383
|
+
}
|
|
384
|
+
this.#incrementCount(scenarioState);
|
|
385
|
+
const icon = this.#testIcon(scenarioState);
|
|
386
|
+
const durationSec = (duration / 1000).toFixed(2);
|
|
387
|
+
log.info(` ${icon} ${pickle?.name ?? 'Unknown'} (${durationSec}s)`);
|
|
388
|
+
this.testReporter.updateSuites();
|
|
389
|
+
this.#currentScenarioSuite = null;
|
|
390
|
+
this.#currentStep = null;
|
|
391
|
+
this.#currentTest = null;
|
|
392
|
+
}
|
|
393
|
+
await this.sessionCapturer.captureTrace(browser);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
log.error(`Failed to finalize Cucumber scenario: ${err.message}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Called from Cucumber BeforeStep hook — marks the step as running. */
|
|
400
|
+
async cucumberBeforeStep(browser, pickleStep, _pickle) {
|
|
401
|
+
if (!this.#currentScenarioSuite) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Reset per-step dedup tracking so commands in step N are never
|
|
405
|
+
// mistaken for retries of identically-signatured commands from step N-1.
|
|
406
|
+
this.browserProxy?.resetCommandTracking();
|
|
407
|
+
const stepText = pickleStep?.text ?? '';
|
|
408
|
+
const step = this.#currentScenarioSuite.tests.find((t) => typeof t !== 'string' &&
|
|
409
|
+
(t.title.endsWith(stepText) || t.title === stepText));
|
|
410
|
+
if (step) {
|
|
411
|
+
step.state = TEST_STATE.RUNNING;
|
|
412
|
+
step.start = new Date();
|
|
413
|
+
step.end = null;
|
|
414
|
+
this.#currentStep = step;
|
|
415
|
+
this.testReporter.updateSuites();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/** Called from Cucumber AfterStep hook — records the step result. */
|
|
419
|
+
async cucumberAfterStep(_browser, result, pickleStep, _pickle) {
|
|
420
|
+
const step = this.#currentStep;
|
|
421
|
+
if (!step) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const status = String(result?.status ?? 'UNKNOWN').toUpperCase();
|
|
425
|
+
const stepState = status === 'PASSED'
|
|
426
|
+
? TEST_STATE.PASSED
|
|
427
|
+
: status === 'SKIPPED'
|
|
428
|
+
? TEST_STATE.SKIPPED
|
|
429
|
+
: TEST_STATE.FAILED;
|
|
430
|
+
step.state = stepState;
|
|
431
|
+
step.end = new Date();
|
|
432
|
+
step._duration = Date.now() - (step.start?.getTime() ?? Date.now());
|
|
433
|
+
this.#currentStep = null;
|
|
434
|
+
this.testReporter.updateSuites();
|
|
435
|
+
void pickleStep; // used by BeforeStep to find the step
|
|
436
|
+
}
|
|
437
|
+
async beforeEach(browser) {
|
|
438
|
+
if (this.#isCucumberRunner) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
await this.#ensureSessionInitialized(browser);
|
|
442
|
+
const currentTest = browser.currentTest;
|
|
443
|
+
if (!currentTest) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const testFile = (currentTest.module || '').split('/').pop() ||
|
|
447
|
+
currentTest.module ||
|
|
448
|
+
DEFAULTS.FILE_NAME;
|
|
449
|
+
let fullPath = findTestFileFromStack() || null;
|
|
450
|
+
const cachedPath = this.browserProxy.getCurrentTestFullPath();
|
|
451
|
+
if (!fullPath && cachedPath && cachedPath.includes(testFile)) {
|
|
452
|
+
fullPath = cachedPath;
|
|
453
|
+
}
|
|
454
|
+
if (!fullPath && testFile) {
|
|
455
|
+
const workspaceRoot = process.cwd();
|
|
456
|
+
// currentTest.module is the path relative to a src_folder, e.g. "basic/ecosia"
|
|
457
|
+
// So we must try: path.join(cwd, srcFolder, module + '.js') for each src_folder
|
|
458
|
+
const modulePath = (currentTest.module || '').replace(/\\/g, '/');
|
|
459
|
+
const srcFolderPaths = this.#srcFolders.flatMap((sf) => modulePath
|
|
460
|
+
? [
|
|
461
|
+
path.join(workspaceRoot, sf, modulePath + '.js'),
|
|
462
|
+
path.join(workspaceRoot, sf, modulePath + '.ts'),
|
|
463
|
+
path.join(workspaceRoot, sf, modulePath + '.cjs'),
|
|
464
|
+
path.join(workspaceRoot, sf, modulePath)
|
|
465
|
+
]
|
|
466
|
+
: []);
|
|
467
|
+
const possiblePaths = [
|
|
468
|
+
// Highest priority: expand module path via each configured src_folder
|
|
469
|
+
...srcFolderPaths,
|
|
470
|
+
// Fallback: treat module path as relative to cwd (works when src_folders isn't nested)
|
|
471
|
+
...(modulePath
|
|
472
|
+
? [
|
|
473
|
+
path.join(workspaceRoot, modulePath + '.js'),
|
|
474
|
+
path.join(workspaceRoot, modulePath + '.ts'),
|
|
475
|
+
path.join(workspaceRoot, modulePath + '.cjs'),
|
|
476
|
+
path.join(workspaceRoot, modulePath)
|
|
477
|
+
]
|
|
478
|
+
: []),
|
|
479
|
+
path.join(workspaceRoot, 'example/tests', testFile + '.js'),
|
|
480
|
+
path.join(workspaceRoot, 'example/tests', testFile),
|
|
481
|
+
path.join(workspaceRoot, 'tests', testFile + '.js'),
|
|
482
|
+
path.join(workspaceRoot, 'test', testFile + '.js'),
|
|
483
|
+
path.join(workspaceRoot, testFile + '.js')
|
|
484
|
+
];
|
|
485
|
+
for (const possiblePath of possiblePaths) {
|
|
486
|
+
if (fs.existsSync(possiblePath)) {
|
|
487
|
+
fullPath = possiblePath;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Extract suite title and test metadata
|
|
493
|
+
let suiteTitle = testFile;
|
|
494
|
+
if (!fullPath) {
|
|
495
|
+
log.warn(`[beforeEach] Could not resolve file path for "${testFile}" — source view will be unavailable`);
|
|
496
|
+
}
|
|
497
|
+
let testNames = [];
|
|
498
|
+
let suiteLine = null;
|
|
499
|
+
let testLines = [];
|
|
500
|
+
if (fullPath) {
|
|
501
|
+
const { suiteTitle: parsedTitle, testNames: parsedNames, suiteLine: parsedSuiteLine, testLines: parsedTestLines } = extractTestMetadata(fullPath);
|
|
502
|
+
if (parsedTitle) {
|
|
503
|
+
suiteTitle = parsedTitle;
|
|
504
|
+
}
|
|
505
|
+
testNames = parsedNames;
|
|
506
|
+
suiteLine = parsedSuiteLine;
|
|
507
|
+
testLines = parsedTestLines;
|
|
508
|
+
}
|
|
509
|
+
const rerunLabel = this.#getRerunLabel();
|
|
510
|
+
if (rerunLabel) {
|
|
511
|
+
const targetIndex = testNames.findIndex((name) => name === rerunLabel);
|
|
512
|
+
if (targetIndex !== -1) {
|
|
513
|
+
testNames = [testNames[targetIndex]];
|
|
514
|
+
testLines = testLines[targetIndex] ? [testLines[targetIndex]] : [];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Get or create suite for this test file
|
|
518
|
+
const currentSuite = this.suiteManager.getOrCreateSuite(testFile, suiteTitle, fullPath, testNames, suiteLine, testLines);
|
|
519
|
+
// Capture source file for display
|
|
520
|
+
if (fullPath && fullPath.includes('/')) {
|
|
521
|
+
this.sessionCapturer.captureSource(fullPath).catch(() => { });
|
|
522
|
+
}
|
|
523
|
+
const runningTest = currentSuite.tests.find((t) => typeof t !== 'string' && t.state === TEST_STATE.RUNNING);
|
|
524
|
+
if (runningTest) {
|
|
525
|
+
const testcases = currentTest?.results?.testcases || {};
|
|
526
|
+
if (testcases[runningTest.title]) {
|
|
527
|
+
const testcase = testcases[runningTest.title];
|
|
528
|
+
const testState = determineTestState(testcase);
|
|
529
|
+
runningTest.state = testState;
|
|
530
|
+
runningTest.end = new Date();
|
|
531
|
+
runningTest._duration = parseFloat(testcase.time || '0') * 1000;
|
|
532
|
+
this.testManager.updateTestState(runningTest, testState);
|
|
533
|
+
this.testManager.markTestAsProcessed(testFile, runningTest.title);
|
|
534
|
+
this.#incrementCount(testState);
|
|
535
|
+
const prevIcon = this.#testIcon(testState);
|
|
536
|
+
log.info(` ${prevIcon} ${runningTest.title} (${(runningTest._duration / 1000).toFixed(2)}s)`);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const endTime = new Date();
|
|
540
|
+
const duration = endTime.getTime() - (runningTest.start?.getTime() || 0);
|
|
541
|
+
this.testManager.updateTestState(runningTest, TEST_STATE.PASSED, endTime, duration);
|
|
542
|
+
this.testManager.markTestAsProcessed(testFile, runningTest.title);
|
|
543
|
+
this.#passCount++;
|
|
544
|
+
log.info(` ✅ ${runningTest.title} (${(duration / 1000).toFixed(2)}s)`);
|
|
545
|
+
}
|
|
546
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.UI_RENDER_DELAY));
|
|
547
|
+
}
|
|
548
|
+
const processedTests = this.testManager.getProcessedTests(testFile);
|
|
549
|
+
const runtimeTestName = typeof currentTest?.name === 'string'
|
|
550
|
+
? currentTest.name.trim()
|
|
551
|
+
: undefined;
|
|
552
|
+
const matchedRuntimeTestName = runtimeTestName
|
|
553
|
+
? testNames.find((name) => runtimeTestName === name || runtimeTestName.endsWith(` ${name}`))
|
|
554
|
+
: undefined;
|
|
555
|
+
const currentTestName = matchedRuntimeTestName ||
|
|
556
|
+
testNames.find((name) => !processedTests.has(name));
|
|
557
|
+
if (currentTestName) {
|
|
558
|
+
if (processedTests.size === 0) {
|
|
559
|
+
this.suiteManager.markSuiteAsRunning(currentSuite);
|
|
560
|
+
}
|
|
561
|
+
const test = this.testManager.findTestInSuite(currentSuite, currentTestName);
|
|
562
|
+
if (test) {
|
|
563
|
+
test.state = TEST_STATE.RUNNING;
|
|
564
|
+
test.start = new Date();
|
|
565
|
+
test.end = null;
|
|
566
|
+
this.testReporter.onTestStart(test);
|
|
567
|
+
this.#currentTest = test;
|
|
568
|
+
log.info(` ▶ ${currentTestName}`);
|
|
569
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.TEST_START_DELAY));
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
log.warn(`Test "${currentTestName}" not found in suite "${currentSuite.title}"`);
|
|
573
|
+
this.#currentTest = null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (!this.isScriptInjected) {
|
|
577
|
+
this.browserProxy.wrapUrlMethod(browser);
|
|
578
|
+
this.isScriptInjected = true;
|
|
579
|
+
}
|
|
580
|
+
this.browserProxy.resetCommandTracking();
|
|
581
|
+
this.browserProxy.wrapBrowserCommands(browser);
|
|
582
|
+
}
|
|
583
|
+
async afterEach(browser) {
|
|
584
|
+
// Cucumber runner manages its own lifecycle via cucumberHooks.cjs
|
|
585
|
+
if (this.#isCucumberRunner) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (browser && this.sessionCapturer) {
|
|
589
|
+
try {
|
|
590
|
+
const currentTest = browser.currentTest;
|
|
591
|
+
const results = currentTest?.results || {};
|
|
592
|
+
const testFile = (currentTest.module || '').split('/').pop() || DEFAULTS.FILE_NAME;
|
|
593
|
+
const testcases = results.testcases || {};
|
|
594
|
+
const testcaseNames = Object.keys(testcases);
|
|
595
|
+
const currentSuite = this.suiteManager.getSuite(testFile);
|
|
596
|
+
if (currentSuite) {
|
|
597
|
+
const processedTests = this.testManager.getProcessedTests(testFile);
|
|
598
|
+
if (testcaseNames.length === 0) {
|
|
599
|
+
const runningTest = currentSuite.tests.find((t) => typeof t !== 'string' && t.state === TEST_STATE.RUNNING);
|
|
600
|
+
if (runningTest && !processedTests.has(runningTest.title)) {
|
|
601
|
+
const testState = results.errors > 0 || results.failed > 0
|
|
602
|
+
? TEST_STATE.FAILED
|
|
603
|
+
: TEST_STATE.PASSED;
|
|
604
|
+
const endTime = new Date();
|
|
605
|
+
const duration = endTime.getTime() - (runningTest.start?.getTime() || 0);
|
|
606
|
+
this.testManager.updateTestState(runningTest, testState, endTime, duration);
|
|
607
|
+
this.testManager.markTestAsProcessed(testFile, runningTest.title);
|
|
608
|
+
this.#incrementCount(testState);
|
|
609
|
+
const icon = this.#testIcon(testState);
|
|
610
|
+
log.info(` ${icon} ${runningTest.title} (${(duration / 1000).toFixed(2)}s)`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
const unprocessedTests = testcaseNames.filter((name) => !processedTests.has(name));
|
|
615
|
+
for (const currentTestName of unprocessedTests) {
|
|
616
|
+
const testcase = testcases[currentTestName];
|
|
617
|
+
const testState = determineTestState(testcase);
|
|
618
|
+
const test = this.testManager.findTestInSuite(currentSuite, currentTestName);
|
|
619
|
+
if (test) {
|
|
620
|
+
const dur = parseFloat(testcase.time || '0') * 1000;
|
|
621
|
+
this.testManager.updateTestState(test, testState, new Date(), dur);
|
|
622
|
+
this.#incrementCount(testState);
|
|
623
|
+
const icon = this.#testIcon(testState);
|
|
624
|
+
log.info(` ${icon} ${currentTestName} (${(dur / 1000).toFixed(2)}s)`);
|
|
625
|
+
}
|
|
626
|
+
this.testManager.markTestAsProcessed(testFile, currentTestName);
|
|
627
|
+
}
|
|
628
|
+
if (processedTests.size === testcaseNames.length) {
|
|
629
|
+
this.suiteManager.finalizeSuite(currentSuite);
|
|
630
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.SUITE_COMPLETE_DELAY));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
await this.sessionCapturer.captureTrace(browser);
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
log.error(`Failed to capture trace: ${err.message}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async after(browser) {
|
|
642
|
+
try {
|
|
643
|
+
const currentTest = browser?.currentTest;
|
|
644
|
+
const testcases = currentTest?.results?.testcases || {};
|
|
645
|
+
for (const [, suite] of (this.suiteManager?.getAllSuites() ?? new Map()).entries()) {
|
|
646
|
+
this.testManager.finalizeSuiteTests(suite, testcases);
|
|
647
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.SUITE_COMPLETE_DELAY));
|
|
648
|
+
this.suiteManager.finalizeSuite(suite);
|
|
649
|
+
}
|
|
650
|
+
await new Promise((resolve) => setTimeout(resolve, TIMING.SUITE_COMPLETE_DELAY));
|
|
651
|
+
const summary = [
|
|
652
|
+
this.#passCount > 0 ? `${this.#passCount} passed` : null,
|
|
653
|
+
this.#failCount > 0 ? `${this.#failCount} failed` : null,
|
|
654
|
+
this.#skipCount > 0 ? `${this.#skipCount} skipped` : null
|
|
655
|
+
]
|
|
656
|
+
.filter(Boolean)
|
|
657
|
+
.join(' ');
|
|
658
|
+
const totalFailed = this.#failCount;
|
|
659
|
+
log.info(`${totalFailed > 0 ? '❌' : '✅'} Tests complete! ${summary}`);
|
|
660
|
+
log.info(` DevTools UI: http://${this.options.hostname}:${this.options.port}`);
|
|
661
|
+
if (!this.#devtoolsBrowser) {
|
|
662
|
+
// Reuse mode: force one final suites broadcast so the UI reflects the
|
|
663
|
+
// actual outcome before the process exits.
|
|
664
|
+
this.testReporter?.updateSuites();
|
|
665
|
+
log.info('♻ Rerun complete — flushing WebSocket...');
|
|
666
|
+
await this.sessionCapturer?.closeWebSocket();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
log.info('💡 Please close the DevTools browser window to finish...');
|
|
670
|
+
if (this.#devtoolsBrowser) {
|
|
671
|
+
;
|
|
672
|
+
logger.setLevel('devtools', 'warn');
|
|
673
|
+
let exitBySignal = false;
|
|
674
|
+
const signalHandler = () => {
|
|
675
|
+
exitBySignal = true;
|
|
676
|
+
log.info('\n✓ Exiting... Browser window will remain open');
|
|
677
|
+
process.exit(0);
|
|
678
|
+
};
|
|
679
|
+
process.once('SIGINT', signalHandler);
|
|
680
|
+
process.once('SIGTERM', signalHandler);
|
|
681
|
+
while (true) {
|
|
682
|
+
try {
|
|
683
|
+
await this.#devtoolsBrowser.getTitle();
|
|
684
|
+
await new Promise((res) => setTimeout(res, TIMING.BROWSER_POLL_INTERVAL));
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
if (!exitBySignal) {
|
|
688
|
+
log.info('Browser window closed, stopping DevTools app');
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!exitBySignal) {
|
|
694
|
+
process.removeListener('SIGINT', signalHandler);
|
|
695
|
+
process.removeListener('SIGTERM', signalHandler);
|
|
696
|
+
logger.setLevel('devtools', 'info');
|
|
697
|
+
try {
|
|
698
|
+
await this.#devtoolsBrowser.deleteSession();
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// session already closed
|
|
702
|
+
}
|
|
703
|
+
await stop();
|
|
704
|
+
process.exit(0);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
log.error(`Failed to stop backend: ${err.message}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
#buildMetadataOptions() {
|
|
713
|
+
return {
|
|
714
|
+
framework: this.#isCucumberRunner ? 'nightwatch-cucumber' : 'nightwatch',
|
|
715
|
+
configFile: this.#configPath,
|
|
716
|
+
baseDir: process.cwd(),
|
|
717
|
+
runCapabilities: {
|
|
718
|
+
canRunSuites: true,
|
|
719
|
+
canRunTests: !this.#isCucumberRunner,
|
|
720
|
+
canRunAll: false
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
#incrementCount(state) {
|
|
725
|
+
if (state === TEST_STATE.PASSED) {
|
|
726
|
+
this.#passCount++;
|
|
727
|
+
}
|
|
728
|
+
else if (state === TEST_STATE.SKIPPED) {
|
|
729
|
+
this.#skipCount++;
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
this.#failCount++;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
#testIcon(state) {
|
|
736
|
+
return state === TEST_STATE.PASSED
|
|
737
|
+
? '✅'
|
|
738
|
+
: state === TEST_STATE.SKIPPED
|
|
739
|
+
? '⏭'
|
|
740
|
+
: '❌';
|
|
741
|
+
}
|
|
742
|
+
registerEventHandlers(eventHub) {
|
|
743
|
+
this.#isCucumberRunner = eventHub.runner === 'cucumber';
|
|
744
|
+
if (this.#isCucumberRunner) {
|
|
745
|
+
log.info('✓ Cucumber runner detected via NightwatchEventHub');
|
|
746
|
+
}
|
|
747
|
+
log.info('✓ NightwatchEventHub registered — enriched metadata enabled');
|
|
748
|
+
const handleSessionMetadata = (data) => {
|
|
749
|
+
try {
|
|
750
|
+
const { sessionCapabilities, sessionId, testEnv, host, modulePath } = data?.metadata ?? {};
|
|
751
|
+
if (this.sessionCapturer && (sessionCapabilities || sessionId)) {
|
|
752
|
+
this.sessionCapturer.sendUpstream('metadata', {
|
|
753
|
+
type: TraceType.Testrunner,
|
|
754
|
+
capabilities: sessionCapabilities ?? {},
|
|
755
|
+
sessionId,
|
|
756
|
+
testEnv,
|
|
757
|
+
host,
|
|
758
|
+
modulePath,
|
|
759
|
+
options: this.#buildMetadataOptions()
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
log.error(`Error in event handler: ${err.message}`);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
eventHub.on('TestSuiteStarted', handleSessionMetadata);
|
|
768
|
+
eventHub.on('TestRunStarted', handleSessionMetadata);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
export const cucumberHooksPath = fileURLToPath(new URL('./helpers/cucumberHooks.cjs', import.meta.url));
|
|
772
|
+
export default function createNightwatchDevTools(options) {
|
|
773
|
+
const plugin = new NightwatchDevToolsPlugin(options);
|
|
774
|
+
return {
|
|
775
|
+
// Set long timeout to allow user to review DevTools UI
|
|
776
|
+
// The after() hook waits for the browser window to be closed
|
|
777
|
+
asyncHookTimeout: 3600000,
|
|
778
|
+
before: async function () {
|
|
779
|
+
await plugin.before();
|
|
780
|
+
},
|
|
781
|
+
beforeEach: async function (browser) {
|
|
782
|
+
await plugin.beforeEach(browser);
|
|
783
|
+
},
|
|
784
|
+
afterEach: async function (browser) {
|
|
785
|
+
await plugin.afterEach(browser);
|
|
786
|
+
},
|
|
787
|
+
after: async function () {
|
|
788
|
+
await plugin.after();
|
|
789
|
+
},
|
|
790
|
+
registerEventHandlers: function (eventHub) {
|
|
791
|
+
plugin.registerEventHandlers(eventHub);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
export { NightwatchDevToolsPlugin };
|
|
796
|
+
//# sourceMappingURL=index.js.map
|