codeceptjs 4.0.0-rc.10 → 4.0.0-rc.15
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/bin/mcp-server.js +32 -5
- package/lib/ai.js +3 -2
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +6 -6
- package/lib/command/check.js +2 -0
- package/lib/command/dryRun.js +2 -3
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +1 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +1 -0
- package/lib/command/run.js +1 -1
- package/lib/command/workers/runTests.js +10 -10
- package/lib/container.js +63 -13
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +128 -0
- package/lib/globals.js +22 -10
- package/lib/heal.js +4 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +22 -17
- package/lib/helper/Puppeteer.js +20 -6
- package/lib/helper/WebDriver.js +12 -2
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +115 -0
- package/lib/history.js +3 -2
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +112 -0
- package/lib/mocha/cli.js +4 -2
- package/lib/mocha/factory.js +2 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/plugin/aiTrace.js +4 -3
- package/lib/plugin/analyze.js +1 -1
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/pageInfo.js +2 -1
- package/lib/plugin/pauseOn.js +167 -0
- package/lib/plugin/screenshotOnFail.js +3 -4
- package/lib/plugin/stepByStepReport.js +5 -4
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/record.js +1 -1
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils.js +4 -3
- package/lib/workers.js +2 -0
- package/package.json +5 -3
|
@@ -256,6 +256,134 @@ class WebElement {
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Run a function in the browser with this element as the first argument.
|
|
261
|
+
* @param {Function} fn Browser-side function. Receives the element, then extra args.
|
|
262
|
+
* @param {...any} args Additional arguments passed to the function
|
|
263
|
+
* @returns {Promise<any>} Value returned by fn
|
|
264
|
+
*/
|
|
265
|
+
async evaluate(fn, ...args) {
|
|
266
|
+
switch (this.helperType) {
|
|
267
|
+
case 'playwright':
|
|
268
|
+
case 'puppeteer':
|
|
269
|
+
return this.element.evaluate(fn, ...args)
|
|
270
|
+
case 'webdriver':
|
|
271
|
+
return this.helper.executeScript(fn, this.element, ...args)
|
|
272
|
+
default:
|
|
273
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Focus the element.
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
async focus() {
|
|
282
|
+
switch (this.helperType) {
|
|
283
|
+
case 'playwright':
|
|
284
|
+
return this.element.focus()
|
|
285
|
+
case 'puppeteer':
|
|
286
|
+
if (this.element.focus) return this.element.focus()
|
|
287
|
+
return this.element.evaluate(el => el.focus())
|
|
288
|
+
case 'webdriver':
|
|
289
|
+
return this.helper.executeScript(el => el.focus(), this.element)
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Type characters via the page/browser keyboard into the focused element.
|
|
297
|
+
* Unlike `type()`, this does not call `.fill()`/`.setValue()`, so it works
|
|
298
|
+
* with contenteditable nodes, iframe bodies, and editor-owned hidden textareas.
|
|
299
|
+
* @param {string} text Text to send
|
|
300
|
+
* @param {Object} [options] Options (e.g. `{ delay }`)
|
|
301
|
+
* @returns {Promise<void>}
|
|
302
|
+
*/
|
|
303
|
+
async typeText(text, options = {}) {
|
|
304
|
+
const s = String(text)
|
|
305
|
+
switch (this.helperType) {
|
|
306
|
+
case 'playwright':
|
|
307
|
+
case 'puppeteer':
|
|
308
|
+
return this.helper.page.keyboard.type(s, options)
|
|
309
|
+
case 'webdriver': {
|
|
310
|
+
const ENTER = '\uE007'
|
|
311
|
+
const parts = s.split('\n')
|
|
312
|
+
for (let i = 0; i < parts.length; i++) {
|
|
313
|
+
if (parts[i]) await this.helper.browser.keys(parts[i])
|
|
314
|
+
if (i < parts.length - 1) await this.helper.browser.keys(ENTER)
|
|
315
|
+
}
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Select all content in the focused field and delete it via keyboard input.
|
|
325
|
+
* Sends Ctrl+A and Meta+A (so it works across platforms) followed by Backspace.
|
|
326
|
+
* @returns {Promise<void>}
|
|
327
|
+
*/
|
|
328
|
+
async selectAllAndDelete() {
|
|
329
|
+
switch (this.helperType) {
|
|
330
|
+
case 'playwright':
|
|
331
|
+
await this.helper.page.keyboard.press('Control+a').catch(() => {})
|
|
332
|
+
await this.helper.page.keyboard.press('Meta+a').catch(() => {})
|
|
333
|
+
await this.helper.page.keyboard.press('Backspace')
|
|
334
|
+
return
|
|
335
|
+
case 'puppeteer':
|
|
336
|
+
for (const mod of ['Control', 'Meta']) {
|
|
337
|
+
try {
|
|
338
|
+
await this.helper.page.keyboard.down(mod)
|
|
339
|
+
await this.helper.page.keyboard.press('KeyA')
|
|
340
|
+
await this.helper.page.keyboard.up(mod)
|
|
341
|
+
} catch (e) {}
|
|
342
|
+
}
|
|
343
|
+
await this.helper.page.keyboard.press('Backspace')
|
|
344
|
+
return
|
|
345
|
+
case 'webdriver': {
|
|
346
|
+
const b = this.helper.browser
|
|
347
|
+
await b.keys(['Control', 'a']).catch(() => {})
|
|
348
|
+
await b.keys(['Meta', 'a']).catch(() => {})
|
|
349
|
+
await b.keys(['Backspace'])
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
default:
|
|
353
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Treat this element as an iframe; invoke `fn` with a WebElement wrapping
|
|
359
|
+
* the iframe body. For WebDriver this switches the browser into the frame
|
|
360
|
+
* for the duration of the callback and switches back on exit.
|
|
361
|
+
* @param {(body: WebElement) => Promise<any>} fn
|
|
362
|
+
* @returns {Promise<any>} Return value of fn
|
|
363
|
+
*/
|
|
364
|
+
async inIframe(fn) {
|
|
365
|
+
switch (this.helperType) {
|
|
366
|
+
case 'playwright':
|
|
367
|
+
case 'puppeteer': {
|
|
368
|
+
const frame = await this.element.contentFrame()
|
|
369
|
+
const body = await frame.$('body')
|
|
370
|
+
return fn(new WebElement(body, this.helper))
|
|
371
|
+
}
|
|
372
|
+
case 'webdriver': {
|
|
373
|
+
const browser = this.helper.browser
|
|
374
|
+
await browser.switchFrame(this.element)
|
|
375
|
+
try {
|
|
376
|
+
const body = await browser.$('body')
|
|
377
|
+
return await fn(new WebElement(body, this.helper))
|
|
378
|
+
} finally {
|
|
379
|
+
await browser.switchFrame(null)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
259
387
|
/**
|
|
260
388
|
* Find first child element matching the locator
|
|
261
389
|
* @param {string|Object} locator Element locator
|
package/lib/globals.js
CHANGED
|
@@ -8,19 +8,35 @@ import fsPath from 'path'
|
|
|
8
8
|
import ActorFactory from './actor.js'
|
|
9
9
|
import output from './output.js'
|
|
10
10
|
import locator from './locator.js'
|
|
11
|
+
import store from './store.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Initialize CodeceptJS core globals
|
|
14
15
|
* Called from Codecept.initGlobals()
|
|
15
16
|
*/
|
|
16
17
|
export async function initCodeceptGlobals(dir, config, container) {
|
|
18
|
+
store.initialize({
|
|
19
|
+
codeceptDir: dir,
|
|
20
|
+
outputDir: fsPath.resolve(dir, config.output),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
store.noGlobals = config.noGlobals || false
|
|
24
|
+
store.maskSensitiveData = config.maskSensitiveData || false
|
|
25
|
+
|
|
26
|
+
// Keep globals for backward compat with external plugins
|
|
17
27
|
global.codecept_dir = dir
|
|
18
28
|
global.output_dir = fsPath.resolve(dir, config.output)
|
|
19
29
|
|
|
20
30
|
if (config.noGlobals) return;
|
|
21
|
-
|
|
31
|
+
|
|
32
|
+
output.print(output.styles.debug('Global functions are deprecated. Use `import { Helper, pause, within, session } from "codeceptjs"` instead. Set `noGlobals: true` in config to disable globals.'));
|
|
33
|
+
|
|
34
|
+
const HelperModule = await import('@codeceptjs/helper')
|
|
35
|
+
global.Helper = global.codecept_helper = HelperModule.default || HelperModule
|
|
36
|
+
|
|
37
|
+
// Set up actor global - will use container when available
|
|
22
38
|
global.actor = global.codecept_actor = (obj) => {
|
|
23
|
-
return ActorFactory(obj,
|
|
39
|
+
return ActorFactory(obj, container)
|
|
24
40
|
}
|
|
25
41
|
global.Actor = global.actor
|
|
26
42
|
|
|
@@ -52,8 +68,6 @@ export async function initCodeceptGlobals(dir, config, container) {
|
|
|
52
68
|
const secretModule = await import('./secret.js')
|
|
53
69
|
global.secret = secretModule.secret || (secretModule.default && secretModule.default.secret)
|
|
54
70
|
|
|
55
|
-
global.codecept_debug = output.debug
|
|
56
|
-
|
|
57
71
|
const codeceptjsModule = await import('./index.js') // load all objects
|
|
58
72
|
global.codeceptjs = codeceptjsModule.default || codeceptjsModule
|
|
59
73
|
|
|
@@ -65,9 +79,6 @@ export async function initCodeceptGlobals(dir, config, container) {
|
|
|
65
79
|
global.Then = stepDefinitions.Then
|
|
66
80
|
global.DefineParameterType = stepDefinitions.defineParameterType
|
|
67
81
|
|
|
68
|
-
// debug mode
|
|
69
|
-
global.debugMode = false
|
|
70
|
-
|
|
71
82
|
// mask sensitive data
|
|
72
83
|
global.maskSensitiveData = config.maskSensitiveData || false
|
|
73
84
|
|
|
@@ -78,6 +89,8 @@ export async function initCodeceptGlobals(dir, config, container) {
|
|
|
78
89
|
* Called from mocha/ui.js pre-require event
|
|
79
90
|
*/
|
|
80
91
|
export function initMochaGlobals(context) {
|
|
92
|
+
if (store.noGlobals) return;
|
|
93
|
+
|
|
81
94
|
// Mocha test framework globals
|
|
82
95
|
global.BeforeAll = context.BeforeAll
|
|
83
96
|
global.AfterAll = context.AfterAll
|
|
@@ -106,6 +119,8 @@ export function getGlobalNames() {
|
|
|
106
119
|
return [
|
|
107
120
|
'codecept_dir',
|
|
108
121
|
'output_dir',
|
|
122
|
+
'Helper',
|
|
123
|
+
'codecept_helper',
|
|
109
124
|
'actor',
|
|
110
125
|
'codecept_actor',
|
|
111
126
|
'Actor',
|
|
@@ -117,13 +132,11 @@ export function getGlobalNames() {
|
|
|
117
132
|
'inject',
|
|
118
133
|
'share',
|
|
119
134
|
'secret',
|
|
120
|
-
'codecept_debug',
|
|
121
135
|
'codeceptjs',
|
|
122
136
|
'Given',
|
|
123
137
|
'When',
|
|
124
138
|
'Then',
|
|
125
139
|
'DefineParameterType',
|
|
126
|
-
'debugMode',
|
|
127
140
|
'maskSensitiveData',
|
|
128
141
|
'BeforeAll',
|
|
129
142
|
'AfterAll',
|
|
@@ -136,6 +149,5 @@ export function getGlobalNames() {
|
|
|
136
149
|
'After',
|
|
137
150
|
'Scenario',
|
|
138
151
|
'xScenario',
|
|
139
|
-
'container'
|
|
140
152
|
]
|
|
141
153
|
}
|
package/lib/heal.js
CHANGED
|
@@ -4,6 +4,7 @@ import colors from 'chalk'
|
|
|
4
4
|
import recorder from './recorder.js'
|
|
5
5
|
import output from './output.js'
|
|
6
6
|
import event from './event.js'
|
|
7
|
+
import container from './container.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @class
|
|
@@ -69,7 +70,7 @@ class Heal {
|
|
|
69
70
|
if (!prepareFn) continue
|
|
70
71
|
|
|
71
72
|
if (context[property]) continue
|
|
72
|
-
context[property] = await prepareFn(
|
|
73
|
+
context[property] = await prepareFn(container.support())
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
output.level(currentOutputLevel)
|
|
@@ -116,10 +117,10 @@ class Heal {
|
|
|
116
117
|
})
|
|
117
118
|
|
|
118
119
|
if (typeof codeSnippet === 'string') {
|
|
119
|
-
const I =
|
|
120
|
+
const I = container.support('I')
|
|
120
121
|
await eval(codeSnippet)
|
|
121
122
|
} else if (typeof codeSnippet === 'function') {
|
|
122
|
-
await codeSnippet(
|
|
123
|
+
await codeSnippet(container.support())
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
this.fixes.push({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import Helper from '@codeceptjs/helper'
|
|
3
3
|
import REST from './REST.js'
|
|
4
|
+
import store from '../store.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Helper for managing remote data using REST API.
|
|
@@ -324,7 +325,7 @@ class ApiDataFactory extends Helper {
|
|
|
324
325
|
await import.meta.resolve(modulePath)
|
|
325
326
|
} catch (e) {
|
|
326
327
|
// If not found, try relative to codecept_dir
|
|
327
|
-
modulePath = path.join(
|
|
328
|
+
modulePath = path.join(store.codeceptDir, modulePath)
|
|
328
329
|
}
|
|
329
330
|
// check if the new syntax `export default new Factory()` is used and loads the builder, otherwise loads the module that used old syntax `module.exports = new Factory()`.
|
|
330
331
|
const module = await import(modulePath)
|
package/lib/helper/FileSystem.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs'
|
|
|
4
4
|
|
|
5
5
|
import Helper from '@codeceptjs/helper'
|
|
6
6
|
import { fileExists } from '../utils.js'
|
|
7
|
+
import store from '../store.js'
|
|
7
8
|
import { fileIncludes } from '../assert/include.js'
|
|
8
9
|
import { fileEquals } from '../assert/equal.js'
|
|
9
10
|
|
|
@@ -33,7 +34,7 @@ import { fileEquals } from '../assert/equal.js'
|
|
|
33
34
|
class FileSystem extends Helper {
|
|
34
35
|
constructor() {
|
|
35
36
|
super()
|
|
36
|
-
this.dir =
|
|
37
|
+
this.dir = store.codeceptDir
|
|
37
38
|
this.file = ''
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -52,7 +53,7 @@ class FileSystem extends Helper {
|
|
|
52
53
|
* @param {string} openPath
|
|
53
54
|
*/
|
|
54
55
|
amInPath(openPath) {
|
|
55
|
-
this.dir = path.join(
|
|
56
|
+
this.dir = path.join(store.codeceptDir, openPath)
|
|
56
57
|
try {
|
|
57
58
|
this.debugSection('Dir', this.dir)
|
|
58
59
|
} catch (e) {
|
|
@@ -2,6 +2,7 @@ import path from 'path'
|
|
|
2
2
|
|
|
3
3
|
import HelperModule from '@codeceptjs/helper'
|
|
4
4
|
import GraphQL from './GraphQL.js'
|
|
5
|
+
import store from '../store.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Helper for managing remote data using GraphQL queries.
|
|
@@ -251,7 +252,7 @@ class GraphQLDataFactory extends Helper {
|
|
|
251
252
|
try {
|
|
252
253
|
require.resolve(modulePath)
|
|
253
254
|
} catch (e) {
|
|
254
|
-
modulePath = path.join(
|
|
255
|
+
modulePath = path.join(store.codeceptDir, modulePath)
|
|
255
256
|
}
|
|
256
257
|
const builder = require(modulePath)
|
|
257
258
|
return builder.build(data)
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -7,6 +7,7 @@ import promiseRetry from 'promise-retry'
|
|
|
7
7
|
import Locator from '../locator.js'
|
|
8
8
|
import recorder from '../recorder.js'
|
|
9
9
|
import store from '../store.js'
|
|
10
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
10
11
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
11
12
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
12
13
|
import { empty } from '../assert/empty.js'
|
|
@@ -39,15 +40,12 @@ import { findReact, findVue, findByPlaywrightLocator } from './extras/Playwright
|
|
|
39
40
|
import { dropFile } from './scripts/dropFile.js'
|
|
40
41
|
import WebElement from '../element/WebElement.js'
|
|
41
42
|
import { selectElement } from './extras/elementSelection.js'
|
|
43
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
42
44
|
|
|
43
45
|
let playwright
|
|
44
46
|
let perfTiming
|
|
45
47
|
let defaultSelectorEnginesInitialized = false
|
|
46
48
|
|
|
47
|
-
// Use global object to track selector registration across workers
|
|
48
|
-
if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
49
|
-
global.__playwrightSelectorsRegistered = false
|
|
50
|
-
}
|
|
51
49
|
|
|
52
50
|
const popupStore = new Popup()
|
|
53
51
|
const consoleLogStore = new Console()
|
|
@@ -448,7 +446,7 @@ class Playwright extends Helper {
|
|
|
448
446
|
this.options.recordVideo = { size }
|
|
449
447
|
}
|
|
450
448
|
if (this.options.recordVideo && !this.options.recordVideo.dir) {
|
|
451
|
-
this.options.recordVideo.dir = `${
|
|
449
|
+
this.options.recordVideo.dir = `${store.outputDir}/videos/`
|
|
452
450
|
}
|
|
453
451
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
454
452
|
this.isElectron = this.options.browser === 'electron'
|
|
@@ -510,18 +508,18 @@ class Playwright extends Helper {
|
|
|
510
508
|
try {
|
|
511
509
|
// Always wrap in try-catch since selectors might be registered globally across workers
|
|
512
510
|
// Check global flag to avoid re-registration in worker processes
|
|
513
|
-
if (!
|
|
511
|
+
if (!defaultSelectorEnginesInitialized) {
|
|
514
512
|
try {
|
|
515
513
|
await playwright.selectors.register('__value', createValueEngine)
|
|
516
514
|
await playwright.selectors.register('__disabled', createDisabledEngine)
|
|
517
|
-
|
|
515
|
+
defaultSelectorEnginesInitialized = true
|
|
518
516
|
defaultSelectorEnginesInitialized = true
|
|
519
517
|
} catch (e) {
|
|
520
518
|
if (!e.message.includes('already registered')) {
|
|
521
519
|
throw e
|
|
522
520
|
}
|
|
523
521
|
// Selector already registered globally by another worker
|
|
524
|
-
|
|
522
|
+
defaultSelectorEnginesInitialized = true
|
|
525
523
|
defaultSelectorEnginesInitialized = true
|
|
526
524
|
}
|
|
527
525
|
} else {
|
|
@@ -614,7 +612,7 @@ class Playwright extends Helper {
|
|
|
614
612
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
|
|
615
613
|
if (this.options.recordHar) {
|
|
616
614
|
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
|
|
617
|
-
const fileName = `${`${
|
|
615
|
+
const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
|
|
618
616
|
const dir = path.dirname(fileName)
|
|
619
617
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
620
618
|
this.options.recordHar.path = fileName
|
|
@@ -1636,7 +1634,7 @@ class Playwright extends Helper {
|
|
|
1636
1634
|
* @returns Promise<void>
|
|
1637
1635
|
*/
|
|
1638
1636
|
async replayFromHar(harFilePath, opts) {
|
|
1639
|
-
const file = path.join(
|
|
1637
|
+
const file = path.join(store.codeceptDir, harFilePath)
|
|
1640
1638
|
|
|
1641
1639
|
if (!fileExists(file)) {
|
|
1642
1640
|
throw new Error(`File at ${file} cannot be found on local system`)
|
|
@@ -2047,7 +2045,7 @@ class Playwright extends Helper {
|
|
|
2047
2045
|
const filePath = await download.path()
|
|
2048
2046
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
2049
2047
|
|
|
2050
|
-
const downloadPath = path.join(
|
|
2048
|
+
const downloadPath = path.join(store.outputDir, fileName)
|
|
2051
2049
|
if (!fs.existsSync(path.dirname(downloadPath))) {
|
|
2052
2050
|
fs.mkdirSync(path.dirname(downloadPath), '0777')
|
|
2053
2051
|
}
|
|
@@ -2231,6 +2229,7 @@ class Playwright extends Helper {
|
|
|
2231
2229
|
* {{> pressKeyWithKeyNormalization }}
|
|
2232
2230
|
*/
|
|
2233
2231
|
async pressKey(key) {
|
|
2232
|
+
await checkFocusBeforePressKey(this, key)
|
|
2234
2233
|
const modifiers = []
|
|
2235
2234
|
if (Array.isArray(key)) {
|
|
2236
2235
|
for (let k of key) {
|
|
@@ -2259,6 +2258,8 @@ class Playwright extends Helper {
|
|
|
2259
2258
|
* {{> type }}
|
|
2260
2259
|
*/
|
|
2261
2260
|
async type(keys, delay = null) {
|
|
2261
|
+
await checkFocusBeforeType(this)
|
|
2262
|
+
|
|
2262
2263
|
// Always use page.keyboard.type for any string (including single character and national characters).
|
|
2263
2264
|
if (!Array.isArray(keys)) {
|
|
2264
2265
|
keys = keys.toString()
|
|
@@ -2283,11 +2284,15 @@ class Playwright extends Helper {
|
|
|
2283
2284
|
assertElementExists(els, field, 'Field')
|
|
2284
2285
|
const el = selectElement(els, field, this)
|
|
2285
2286
|
|
|
2287
|
+
await highlightActiveElement.call(this, el)
|
|
2288
|
+
|
|
2289
|
+
if (await fillRichEditor(this, el, value)) {
|
|
2290
|
+
return this._waitForAction()
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2286
2293
|
await el.clear()
|
|
2287
2294
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
2288
2295
|
|
|
2289
|
-
await highlightActiveElement.call(this, el)
|
|
2290
|
-
|
|
2291
2296
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2292
2297
|
|
|
2293
2298
|
return this._waitForAction()
|
|
@@ -2343,7 +2348,7 @@ class Playwright extends Helper {
|
|
|
2343
2348
|
*
|
|
2344
2349
|
*/
|
|
2345
2350
|
async attachFile(locator, pathToFile, context = null) {
|
|
2346
|
-
const file = path.join(
|
|
2351
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
2347
2352
|
|
|
2348
2353
|
if (!fileExists(file)) {
|
|
2349
2354
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
@@ -2933,7 +2938,7 @@ class Playwright extends Helper {
|
|
|
2933
2938
|
const els = await this._locate(matchedLocator)
|
|
2934
2939
|
assertElementExists(els, locator)
|
|
2935
2940
|
const snapshot = await els[0].ariaSnapshot()
|
|
2936
|
-
this.debugSection('Aria Snapshot', snapshot)
|
|
2941
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
2937
2942
|
return snapshot
|
|
2938
2943
|
}
|
|
2939
2944
|
|
|
@@ -4819,7 +4824,7 @@ async function refreshContextSession() {
|
|
|
4819
4824
|
|
|
4820
4825
|
function saveVideoForPage(page, name) {
|
|
4821
4826
|
if (!page.video()) return null
|
|
4822
|
-
const fileName = `${`${
|
|
4827
|
+
const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
|
|
4823
4828
|
page
|
|
4824
4829
|
.video()
|
|
4825
4830
|
.saveAs(fileName)
|
|
@@ -4836,7 +4841,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4836
4841
|
if (!context) return
|
|
4837
4842
|
if (!context.tracing) return
|
|
4838
4843
|
try {
|
|
4839
|
-
const fileName = `${`${
|
|
4844
|
+
const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
4840
4845
|
await context.tracing.stop({ path: fileName })
|
|
4841
4846
|
return fileName
|
|
4842
4847
|
} catch (err) {
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -8,6 +8,7 @@ import promiseRetry from 'promise-retry'
|
|
|
8
8
|
import Locator from '../locator.js'
|
|
9
9
|
import recorder from '../recorder.js'
|
|
10
10
|
import store from '../store.js'
|
|
11
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
11
12
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
12
13
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
13
14
|
import { empty } from '../assert/empty.js'
|
|
@@ -44,6 +45,7 @@ import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElem
|
|
|
44
45
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
45
46
|
import WebElement from '../element/WebElement.js'
|
|
46
47
|
import { selectElement } from './extras/elementSelection.js'
|
|
48
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
47
49
|
|
|
48
50
|
let puppeteer
|
|
49
51
|
|
|
@@ -751,7 +753,7 @@ class Puppeteer extends Helper {
|
|
|
751
753
|
}
|
|
752
754
|
|
|
753
755
|
if (this.options.trace) {
|
|
754
|
-
const fileName = `${`${
|
|
756
|
+
const fileName = `${`${store.outputDir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
|
|
755
757
|
const dir = path.dirname(fileName)
|
|
756
758
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
757
759
|
await this.page.tracing.start({ screenshots: true, path: fileName })
|
|
@@ -1325,7 +1327,7 @@ class Puppeteer extends Helper {
|
|
|
1325
1327
|
* @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving
|
|
1326
1328
|
*/
|
|
1327
1329
|
async handleDownloads(downloadPath = 'downloads') {
|
|
1328
|
-
downloadPath = path.join(
|
|
1330
|
+
downloadPath = path.join(store.outputDir, downloadPath)
|
|
1329
1331
|
if (!fs.existsSync(downloadPath)) {
|
|
1330
1332
|
fs.mkdirSync(downloadPath, '0777')
|
|
1331
1333
|
}
|
|
@@ -1387,7 +1389,7 @@ class Puppeteer extends Helper {
|
|
|
1387
1389
|
},
|
|
1388
1390
|
})
|
|
1389
1391
|
|
|
1390
|
-
const outputFile = path.join(`${
|
|
1392
|
+
const outputFile = path.join(`${store.outputDir}/${fileName}`)
|
|
1391
1393
|
|
|
1392
1394
|
try {
|
|
1393
1395
|
await new Promise((resolve, reject) => {
|
|
@@ -1547,6 +1549,7 @@ class Puppeteer extends Helper {
|
|
|
1547
1549
|
* {{> pressKeyWithKeyNormalization }}
|
|
1548
1550
|
*/
|
|
1549
1551
|
async pressKey(key) {
|
|
1552
|
+
await checkFocusBeforePressKey(this, key)
|
|
1550
1553
|
const modifiers = []
|
|
1551
1554
|
if (Array.isArray(key)) {
|
|
1552
1555
|
for (let k of key) {
|
|
@@ -1575,6 +1578,8 @@ class Puppeteer extends Helper {
|
|
|
1575
1578
|
* {{> type }}
|
|
1576
1579
|
*/
|
|
1577
1580
|
async type(keys, delay = null) {
|
|
1581
|
+
await checkFocusBeforeType(this)
|
|
1582
|
+
|
|
1578
1583
|
if (!Array.isArray(keys)) {
|
|
1579
1584
|
keys = keys.toString()
|
|
1580
1585
|
keys = keys.split('')
|
|
@@ -1591,9 +1596,18 @@ class Puppeteer extends Helper {
|
|
|
1591
1596
|
* {{ react }}
|
|
1592
1597
|
*/
|
|
1593
1598
|
async fillField(field, value, context = null) {
|
|
1594
|
-
|
|
1599
|
+
let els = await findVisibleFields.call(this, field, context)
|
|
1600
|
+
if (!els.length) {
|
|
1601
|
+
els = await findFields.call(this, field, context)
|
|
1602
|
+
}
|
|
1595
1603
|
assertElementExists(els, field, 'Field')
|
|
1596
1604
|
const el = selectElement(els, field, this)
|
|
1605
|
+
|
|
1606
|
+
if (await fillRichEditor(this, el, value)) {
|
|
1607
|
+
highlightActiveElement.call(this, el, await this._getContext())
|
|
1608
|
+
return this._waitForAction()
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1597
1611
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1598
1612
|
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
1599
1613
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
@@ -1652,7 +1666,7 @@ class Puppeteer extends Helper {
|
|
|
1652
1666
|
* {{> attachFile }}
|
|
1653
1667
|
*/
|
|
1654
1668
|
async attachFile(locator, pathToFile, context = null) {
|
|
1655
|
-
const file = path.join(
|
|
1669
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
1656
1670
|
|
|
1657
1671
|
if (!fileExists(file)) {
|
|
1658
1672
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
@@ -3564,7 +3578,7 @@ function getNormalizedKey(key) {
|
|
|
3564
3578
|
}
|
|
3565
3579
|
|
|
3566
3580
|
function highlightActiveElement(element, context) {
|
|
3567
|
-
if (this.options.highlightElement &&
|
|
3581
|
+
if (this.options.highlightElement && store.debugMode) {
|
|
3568
3582
|
highlightElement(element, context)
|
|
3569
3583
|
}
|
|
3570
3584
|
}
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -10,6 +10,7 @@ import promiseRetry from 'promise-retry'
|
|
|
10
10
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
11
11
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
12
12
|
import store from '../store.js'
|
|
13
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
13
14
|
import output from '../output.js'
|
|
14
15
|
const { debug } = output
|
|
15
16
|
import { empty } from '../assert/empty.js'
|
|
@@ -41,6 +42,7 @@ import { dropFile } from './scripts/dropFile.js'
|
|
|
41
42
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
42
43
|
import WebElement from '../element/WebElement.js'
|
|
43
44
|
import { selectElement } from './extras/elementSelection.js'
|
|
45
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
44
46
|
|
|
45
47
|
const SHADOW = 'shadow'
|
|
46
48
|
const webRoot = 'body'
|
|
@@ -1278,6 +1280,11 @@ class WebDriver extends Helper {
|
|
|
1278
1280
|
assertElementExists(res, field, 'Field')
|
|
1279
1281
|
const elem = selectElement(res, field, this)
|
|
1280
1282
|
highlightActiveElement.call(this, elem)
|
|
1283
|
+
|
|
1284
|
+
if (await fillRichEditor(this, elem, value)) {
|
|
1285
|
+
return
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1281
1288
|
try {
|
|
1282
1289
|
await elem.clearValue()
|
|
1283
1290
|
} catch (err) {
|
|
@@ -1352,7 +1359,7 @@ class WebDriver extends Helper {
|
|
|
1352
1359
|
* {{> attachFile }}
|
|
1353
1360
|
*/
|
|
1354
1361
|
async attachFile(locator, pathToFile, context = null) {
|
|
1355
|
-
let file = path.join(
|
|
1362
|
+
let file = path.join(store.codeceptDir, pathToFile)
|
|
1356
1363
|
if (!fileExists(file)) {
|
|
1357
1364
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1358
1365
|
}
|
|
@@ -2237,6 +2244,7 @@ class WebDriver extends Helper {
|
|
|
2237
2244
|
* {{> pressKeyWithKeyNormalization }}
|
|
2238
2245
|
*/
|
|
2239
2246
|
async pressKey(key) {
|
|
2247
|
+
await checkFocusBeforePressKey(this, key)
|
|
2240
2248
|
const modifiers = []
|
|
2241
2249
|
if (Array.isArray(key)) {
|
|
2242
2250
|
for (let k of key) {
|
|
@@ -2283,6 +2291,8 @@ class WebDriver extends Helper {
|
|
|
2283
2291
|
* {{> type }}
|
|
2284
2292
|
*/
|
|
2285
2293
|
async type(keys, delay = null) {
|
|
2294
|
+
await checkFocusBeforeType(this)
|
|
2295
|
+
|
|
2286
2296
|
if (!Array.isArray(keys)) {
|
|
2287
2297
|
keys = keys.toString()
|
|
2288
2298
|
keys = keys.split('')
|
|
@@ -3480,7 +3490,7 @@ function isModifierKey(key) {
|
|
|
3480
3490
|
}
|
|
3481
3491
|
|
|
3482
3492
|
function highlightActiveElement(element) {
|
|
3483
|
-
if (this.options.highlightElement &&
|
|
3493
|
+
if (this.options.highlightElement && store.debugMode) {
|
|
3484
3494
|
highlightElement(element, this.browser)
|
|
3485
3495
|
}
|
|
3486
3496
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import minimatch from 'minimatch'
|
|
4
|
+
import store from '../../store.js'
|
|
5
|
+
import assert from 'assert'
|
|
6
|
+
|
|
7
|
+
function getDownloadDir() {
|
|
8
|
+
return path.join(store.outputDir, 'downloads')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getNewFiles(downloadDir, sinceTimestamp) {
|
|
12
|
+
if (!fs.existsSync(downloadDir)) return []
|
|
13
|
+
return fs.readdirSync(downloadDir).filter(name => {
|
|
14
|
+
const stat = fs.statSync(path.join(downloadDir, name))
|
|
15
|
+
return stat.isFile() && stat.mtimeMs >= sinceTimestamp
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function seeFileDownloaded(arg) {
|
|
20
|
+
const downloadDir = getDownloadDir()
|
|
21
|
+
const files = getNewFiles(downloadDir, this._downloadStartTimestamp)
|
|
22
|
+
|
|
23
|
+
if (arg === undefined || arg === null) {
|
|
24
|
+
assert.ok(files.length > 0, `No files downloaded to ${downloadDir}`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
if (typeof arg === 'number') {
|
|
28
|
+
assert.strictEqual(files.length, arg, `Expected ${arg} downloaded file(s), found ${files.length}: [${files.join(', ')}]`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const regexMatch = arg.match(/^\/(.+)\/$/)
|
|
32
|
+
if (regexMatch) {
|
|
33
|
+
const re = new RegExp(regexMatch[1])
|
|
34
|
+
assert.ok(files.some(f => re.test(f)), `No file matches ${arg}. Downloaded: [${files.join(', ')}]`)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if (/[*?[\]]/.test(arg)) {
|
|
38
|
+
const matched = minimatch.match(files, arg)
|
|
39
|
+
assert.ok(matched.length > 0, `No file matches glob "${arg}". Downloaded: [${files.join(', ')}]`)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
assert.ok(files.includes(arg), `File "${arg}" not downloaded. Downloaded: [${files.join(', ')}]`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { seeFileDownloaded, getDownloadDir }
|