codeceptjs 4.0.0-rc.11 → 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/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 +17 -16
- package/lib/helper/Puppeteer.js +16 -6
- package/lib/helper/WebDriver.js +8 -2
- package/lib/helper/extras/Download.js +45 -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
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
|
@@ -40,15 +40,12 @@ import { findReact, findVue, findByPlaywrightLocator } from './extras/Playwright
|
|
|
40
40
|
import { dropFile } from './scripts/dropFile.js'
|
|
41
41
|
import WebElement from '../element/WebElement.js'
|
|
42
42
|
import { selectElement } from './extras/elementSelection.js'
|
|
43
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
43
44
|
|
|
44
45
|
let playwright
|
|
45
46
|
let perfTiming
|
|
46
47
|
let defaultSelectorEnginesInitialized = false
|
|
47
48
|
|
|
48
|
-
// Use global object to track selector registration across workers
|
|
49
|
-
if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
50
|
-
global.__playwrightSelectorsRegistered = false
|
|
51
|
-
}
|
|
52
49
|
|
|
53
50
|
const popupStore = new Popup()
|
|
54
51
|
const consoleLogStore = new Console()
|
|
@@ -449,7 +446,7 @@ class Playwright extends Helper {
|
|
|
449
446
|
this.options.recordVideo = { size }
|
|
450
447
|
}
|
|
451
448
|
if (this.options.recordVideo && !this.options.recordVideo.dir) {
|
|
452
|
-
this.options.recordVideo.dir = `${
|
|
449
|
+
this.options.recordVideo.dir = `${store.outputDir}/videos/`
|
|
453
450
|
}
|
|
454
451
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
455
452
|
this.isElectron = this.options.browser === 'electron'
|
|
@@ -511,18 +508,18 @@ class Playwright extends Helper {
|
|
|
511
508
|
try {
|
|
512
509
|
// Always wrap in try-catch since selectors might be registered globally across workers
|
|
513
510
|
// Check global flag to avoid re-registration in worker processes
|
|
514
|
-
if (!
|
|
511
|
+
if (!defaultSelectorEnginesInitialized) {
|
|
515
512
|
try {
|
|
516
513
|
await playwright.selectors.register('__value', createValueEngine)
|
|
517
514
|
await playwright.selectors.register('__disabled', createDisabledEngine)
|
|
518
|
-
|
|
515
|
+
defaultSelectorEnginesInitialized = true
|
|
519
516
|
defaultSelectorEnginesInitialized = true
|
|
520
517
|
} catch (e) {
|
|
521
518
|
if (!e.message.includes('already registered')) {
|
|
522
519
|
throw e
|
|
523
520
|
}
|
|
524
521
|
// Selector already registered globally by another worker
|
|
525
|
-
|
|
522
|
+
defaultSelectorEnginesInitialized = true
|
|
526
523
|
defaultSelectorEnginesInitialized = true
|
|
527
524
|
}
|
|
528
525
|
} else {
|
|
@@ -615,7 +612,7 @@ class Playwright extends Helper {
|
|
|
615
612
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
|
|
616
613
|
if (this.options.recordHar) {
|
|
617
614
|
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
|
|
618
|
-
const fileName = `${`${
|
|
615
|
+
const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
|
|
619
616
|
const dir = path.dirname(fileName)
|
|
620
617
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
621
618
|
this.options.recordHar.path = fileName
|
|
@@ -1637,7 +1634,7 @@ class Playwright extends Helper {
|
|
|
1637
1634
|
* @returns Promise<void>
|
|
1638
1635
|
*/
|
|
1639
1636
|
async replayFromHar(harFilePath, opts) {
|
|
1640
|
-
const file = path.join(
|
|
1637
|
+
const file = path.join(store.codeceptDir, harFilePath)
|
|
1641
1638
|
|
|
1642
1639
|
if (!fileExists(file)) {
|
|
1643
1640
|
throw new Error(`File at ${file} cannot be found on local system`)
|
|
@@ -2048,7 +2045,7 @@ class Playwright extends Helper {
|
|
|
2048
2045
|
const filePath = await download.path()
|
|
2049
2046
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
2050
2047
|
|
|
2051
|
-
const downloadPath = path.join(
|
|
2048
|
+
const downloadPath = path.join(store.outputDir, fileName)
|
|
2052
2049
|
if (!fs.existsSync(path.dirname(downloadPath))) {
|
|
2053
2050
|
fs.mkdirSync(path.dirname(downloadPath), '0777')
|
|
2054
2051
|
}
|
|
@@ -2287,11 +2284,15 @@ class Playwright extends Helper {
|
|
|
2287
2284
|
assertElementExists(els, field, 'Field')
|
|
2288
2285
|
const el = selectElement(els, field, this)
|
|
2289
2286
|
|
|
2287
|
+
await highlightActiveElement.call(this, el)
|
|
2288
|
+
|
|
2289
|
+
if (await fillRichEditor(this, el, value)) {
|
|
2290
|
+
return this._waitForAction()
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2290
2293
|
await el.clear()
|
|
2291
2294
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
2292
2295
|
|
|
2293
|
-
await highlightActiveElement.call(this, el)
|
|
2294
|
-
|
|
2295
2296
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2296
2297
|
|
|
2297
2298
|
return this._waitForAction()
|
|
@@ -2347,7 +2348,7 @@ class Playwright extends Helper {
|
|
|
2347
2348
|
*
|
|
2348
2349
|
*/
|
|
2349
2350
|
async attachFile(locator, pathToFile, context = null) {
|
|
2350
|
-
const file = path.join(
|
|
2351
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
2351
2352
|
|
|
2352
2353
|
if (!fileExists(file)) {
|
|
2353
2354
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
@@ -4823,7 +4824,7 @@ async function refreshContextSession() {
|
|
|
4823
4824
|
|
|
4824
4825
|
function saveVideoForPage(page, name) {
|
|
4825
4826
|
if (!page.video()) return null
|
|
4826
|
-
const fileName = `${`${
|
|
4827
|
+
const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
|
|
4827
4828
|
page
|
|
4828
4829
|
.video()
|
|
4829
4830
|
.saveAs(fileName)
|
|
@@ -4840,7 +4841,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4840
4841
|
if (!context) return
|
|
4841
4842
|
if (!context.tracing) return
|
|
4842
4843
|
try {
|
|
4843
|
-
const fileName = `${`${
|
|
4844
|
+
const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
4844
4845
|
await context.tracing.stop({ path: fileName })
|
|
4845
4846
|
return fileName
|
|
4846
4847
|
} catch (err) {
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -45,6 +45,7 @@ import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElem
|
|
|
45
45
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
46
46
|
import WebElement from '../element/WebElement.js'
|
|
47
47
|
import { selectElement } from './extras/elementSelection.js'
|
|
48
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
48
49
|
|
|
49
50
|
let puppeteer
|
|
50
51
|
|
|
@@ -752,7 +753,7 @@ class Puppeteer extends Helper {
|
|
|
752
753
|
}
|
|
753
754
|
|
|
754
755
|
if (this.options.trace) {
|
|
755
|
-
const fileName = `${`${
|
|
756
|
+
const fileName = `${`${store.outputDir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
|
|
756
757
|
const dir = path.dirname(fileName)
|
|
757
758
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
758
759
|
await this.page.tracing.start({ screenshots: true, path: fileName })
|
|
@@ -1326,7 +1327,7 @@ class Puppeteer extends Helper {
|
|
|
1326
1327
|
* @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving
|
|
1327
1328
|
*/
|
|
1328
1329
|
async handleDownloads(downloadPath = 'downloads') {
|
|
1329
|
-
downloadPath = path.join(
|
|
1330
|
+
downloadPath = path.join(store.outputDir, downloadPath)
|
|
1330
1331
|
if (!fs.existsSync(downloadPath)) {
|
|
1331
1332
|
fs.mkdirSync(downloadPath, '0777')
|
|
1332
1333
|
}
|
|
@@ -1388,7 +1389,7 @@ class Puppeteer extends Helper {
|
|
|
1388
1389
|
},
|
|
1389
1390
|
})
|
|
1390
1391
|
|
|
1391
|
-
const outputFile = path.join(`${
|
|
1392
|
+
const outputFile = path.join(`${store.outputDir}/${fileName}`)
|
|
1392
1393
|
|
|
1393
1394
|
try {
|
|
1394
1395
|
await new Promise((resolve, reject) => {
|
|
@@ -1595,9 +1596,18 @@ class Puppeteer extends Helper {
|
|
|
1595
1596
|
* {{ react }}
|
|
1596
1597
|
*/
|
|
1597
1598
|
async fillField(field, value, context = null) {
|
|
1598
|
-
|
|
1599
|
+
let els = await findVisibleFields.call(this, field, context)
|
|
1600
|
+
if (!els.length) {
|
|
1601
|
+
els = await findFields.call(this, field, context)
|
|
1602
|
+
}
|
|
1599
1603
|
assertElementExists(els, field, 'Field')
|
|
1600
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
|
+
|
|
1601
1611
|
const tag = await el.getProperty('tagName').then(el => el.jsonValue())
|
|
1602
1612
|
const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
|
|
1603
1613
|
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
@@ -1656,7 +1666,7 @@ class Puppeteer extends Helper {
|
|
|
1656
1666
|
* {{> attachFile }}
|
|
1657
1667
|
*/
|
|
1658
1668
|
async attachFile(locator, pathToFile, context = null) {
|
|
1659
|
-
const file = path.join(
|
|
1669
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
1660
1670
|
|
|
1661
1671
|
if (!fileExists(file)) {
|
|
1662
1672
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
@@ -3568,7 +3578,7 @@ function getNormalizedKey(key) {
|
|
|
3568
3578
|
}
|
|
3569
3579
|
|
|
3570
3580
|
function highlightActiveElement(element, context) {
|
|
3571
|
-
if (this.options.highlightElement &&
|
|
3581
|
+
if (this.options.highlightElement && store.debugMode) {
|
|
3572
3582
|
highlightElement(element, context)
|
|
3573
3583
|
}
|
|
3574
3584
|
}
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -42,6 +42,7 @@ import { dropFile } from './scripts/dropFile.js'
|
|
|
42
42
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
43
43
|
import WebElement from '../element/WebElement.js'
|
|
44
44
|
import { selectElement } from './extras/elementSelection.js'
|
|
45
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
45
46
|
|
|
46
47
|
const SHADOW = 'shadow'
|
|
47
48
|
const webRoot = 'body'
|
|
@@ -1279,6 +1280,11 @@ class WebDriver extends Helper {
|
|
|
1279
1280
|
assertElementExists(res, field, 'Field')
|
|
1280
1281
|
const elem = selectElement(res, field, this)
|
|
1281
1282
|
highlightActiveElement.call(this, elem)
|
|
1283
|
+
|
|
1284
|
+
if (await fillRichEditor(this, elem, value)) {
|
|
1285
|
+
return
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1282
1288
|
try {
|
|
1283
1289
|
await elem.clearValue()
|
|
1284
1290
|
} catch (err) {
|
|
@@ -1353,7 +1359,7 @@ class WebDriver extends Helper {
|
|
|
1353
1359
|
* {{> attachFile }}
|
|
1354
1360
|
*/
|
|
1355
1361
|
async attachFile(locator, pathToFile, context = null) {
|
|
1356
|
-
let file = path.join(
|
|
1362
|
+
let file = path.join(store.codeceptDir, pathToFile)
|
|
1357
1363
|
if (!fileExists(file)) {
|
|
1358
1364
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1359
1365
|
}
|
|
@@ -3484,7 +3490,7 @@ function isModifierKey(key) {
|
|
|
3484
3490
|
}
|
|
3485
3491
|
|
|
3486
3492
|
function highlightActiveElement(element) {
|
|
3487
|
-
if (this.options.highlightElement &&
|
|
3493
|
+
if (this.options.highlightElement && store.debugMode) {
|
|
3488
3494
|
highlightElement(element, this.browser)
|
|
3489
3495
|
}
|
|
3490
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 }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import WebElement from '../../element/WebElement.js'
|
|
2
|
+
|
|
3
|
+
const MARKER = 'data-codeceptjs-rte-target'
|
|
4
|
+
|
|
5
|
+
const EDITOR = {
|
|
6
|
+
STANDARD: 'standard',
|
|
7
|
+
IFRAME: 'iframe',
|
|
8
|
+
CONTENTEDITABLE: 'contenteditable',
|
|
9
|
+
HIDDEN_TEXTAREA: 'hidden-textarea',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function detectAndMark(el, opts) {
|
|
13
|
+
const marker = opts.marker
|
|
14
|
+
const kinds = opts.kinds
|
|
15
|
+
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
16
|
+
const MAX_HIDDEN_ASCENT = 3
|
|
17
|
+
|
|
18
|
+
function mark(kind, target) {
|
|
19
|
+
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
20
|
+
if (target && target.nodeType === 1) target.setAttribute(marker, '1')
|
|
21
|
+
return kind
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!el || el.nodeType !== 1) return mark(kinds.STANDARD, el)
|
|
25
|
+
|
|
26
|
+
const tag = el.tagName
|
|
27
|
+
if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
|
|
28
|
+
if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
|
|
29
|
+
|
|
30
|
+
const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
|
|
31
|
+
if (canSearchDescendants) {
|
|
32
|
+
const iframe = el.querySelector('iframe')
|
|
33
|
+
if (iframe) return mark(kinds.IFRAME, iframe)
|
|
34
|
+
const ce = el.querySelector(CE)
|
|
35
|
+
if (ce) return mark(kinds.CONTENTEDITABLE, ce)
|
|
36
|
+
const textarea = el.querySelector('textarea')
|
|
37
|
+
if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const style = window.getComputedStyle(el)
|
|
41
|
+
const isHidden =
|
|
42
|
+
el.offsetParent === null ||
|
|
43
|
+
(el.offsetWidth === 0 && el.offsetHeight === 0) ||
|
|
44
|
+
style.display === 'none' ||
|
|
45
|
+
style.visibility === 'hidden'
|
|
46
|
+
if (!isHidden) return mark(kinds.STANDARD, el)
|
|
47
|
+
|
|
48
|
+
const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
|
|
49
|
+
if (isFormHidden) return mark(kinds.STANDARD, el)
|
|
50
|
+
|
|
51
|
+
let scope = el.parentElement
|
|
52
|
+
for (let depth = 0; scope && depth < MAX_HIDDEN_ASCENT; depth++, scope = scope.parentElement) {
|
|
53
|
+
const iframeNear = scope.querySelector('iframe')
|
|
54
|
+
if (iframeNear) return mark(kinds.IFRAME, iframeNear)
|
|
55
|
+
const ceNear = scope.querySelector(CE)
|
|
56
|
+
if (ceNear) return mark(kinds.CONTENTEDITABLE, ceNear)
|
|
57
|
+
const textareaNear = [...scope.querySelectorAll('textarea')].find(t => t !== el)
|
|
58
|
+
if (textareaNear) return mark(kinds.HIDDEN_TEXTAREA, textareaNear)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return mark(kinds.STANDARD, el)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function selectAllInEditable(el) {
|
|
65
|
+
const doc = el.ownerDocument
|
|
66
|
+
const win = doc.defaultView
|
|
67
|
+
el.focus()
|
|
68
|
+
const range = doc.createRange()
|
|
69
|
+
range.selectNodeContents(el)
|
|
70
|
+
const sel = win.getSelection()
|
|
71
|
+
sel.removeAllRanges()
|
|
72
|
+
sel.addRange(range)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unmarkAll(marker) {
|
|
76
|
+
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function findMarked(helper) {
|
|
80
|
+
const root = helper.page || helper.browser
|
|
81
|
+
const raw = await root.$('[' + MARKER + ']')
|
|
82
|
+
return new WebElement(raw, helper)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function clearMarker(helper) {
|
|
86
|
+
if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
|
|
87
|
+
return helper.executeScript(unmarkAll, MARKER)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function fillRichEditor(helper, el, value) {
|
|
91
|
+
const source = el instanceof WebElement ? el : new WebElement(el, helper)
|
|
92
|
+
const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
|
|
93
|
+
if (kind === EDITOR.STANDARD) return false
|
|
94
|
+
|
|
95
|
+
const target = await findMarked(helper)
|
|
96
|
+
const delay = helper.options.pressKeyDelay
|
|
97
|
+
|
|
98
|
+
if (kind === EDITOR.IFRAME) {
|
|
99
|
+
await target.inIframe(async body => {
|
|
100
|
+
await body.evaluate(selectAllInEditable)
|
|
101
|
+
await body.typeText(value, { delay })
|
|
102
|
+
})
|
|
103
|
+
} else if (kind === EDITOR.HIDDEN_TEXTAREA) {
|
|
104
|
+
await target.focus()
|
|
105
|
+
await target.selectAllAndDelete()
|
|
106
|
+
await target.typeText(value, { delay })
|
|
107
|
+
} else if (kind === EDITOR.CONTENTEDITABLE) {
|
|
108
|
+
await target.click()
|
|
109
|
+
await target.evaluate(selectAllInEditable)
|
|
110
|
+
await target.typeText(value, { delay })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await clearMarker(helper)
|
|
114
|
+
return true
|
|
115
|
+
}
|
package/lib/history.js
CHANGED
|
@@ -2,6 +2,7 @@ import colors from 'chalk'
|
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import output from './output.js'
|
|
5
|
+
import store from './store.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* REPL history records REPL commands and stores them in
|
|
@@ -9,8 +10,8 @@ import output from './output.js'
|
|
|
9
10
|
*/
|
|
10
11
|
class ReplHistory {
|
|
11
12
|
constructor() {
|
|
12
|
-
if (
|
|
13
|
-
this.historyFile = path.join(
|
|
13
|
+
if (store.outputDir) {
|
|
14
|
+
this.historyFile = path.join(store.outputDir, 'cli-history')
|
|
14
15
|
}
|
|
15
16
|
this.commands = []
|
|
16
17
|
}
|
package/lib/listener/config.js
CHANGED
|
@@ -2,16 +2,18 @@ import event from '../event.js'
|
|
|
2
2
|
import recorder from '../recorder.js'
|
|
3
3
|
import { deepMerge, deepClone, ucfirst } from '../utils.js'
|
|
4
4
|
import output from '../output.js'
|
|
5
|
+
import container from '../container.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
10
|
+
let initialized = false
|
|
11
|
+
|
|
9
12
|
export default function () {
|
|
10
|
-
|
|
11
|
-
if (global.__codeceptConfigListenerInitialized) {
|
|
13
|
+
if (initialized) {
|
|
12
14
|
return
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
initialized = true
|
|
15
17
|
|
|
16
18
|
enableDynamicConfigFor('suite')
|
|
17
19
|
enableDynamicConfigFor('test')
|
|
@@ -20,7 +22,7 @@ export default function () {
|
|
|
20
22
|
event.dispatcher.on(event[type].before, (context = {}) => {
|
|
21
23
|
// Get helpers dynamically at runtime, not at initialization time
|
|
22
24
|
// This ensures we get the actual helper instances, not placeholders
|
|
23
|
-
const helpers =
|
|
25
|
+
const helpers = container.helpers()
|
|
24
26
|
|
|
25
27
|
function updateHelperConfig(helper, config) {
|
|
26
28
|
// Guard against undefined or invalid helpers
|
package/lib/listener/emptyRun.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import figures from 'figures'
|
|
2
2
|
import event from '../event.js'
|
|
3
3
|
import output from '../output.js'
|
|
4
|
+
import container from '../container.js'
|
|
4
5
|
import { searchWithFusejs } from '../utils.js'
|
|
5
6
|
|
|
6
7
|
export default function () {
|
|
@@ -12,7 +13,7 @@ export default function () {
|
|
|
12
13
|
|
|
13
14
|
event.dispatcher.on(event.all.result, () => {
|
|
14
15
|
if (isEmptyRun) {
|
|
15
|
-
const mocha =
|
|
16
|
+
const mocha = container.mocha()
|
|
16
17
|
|
|
17
18
|
if (mocha.options.grep) {
|
|
18
19
|
output.print()
|
package/lib/listener/helpers.js
CHANGED
|
@@ -3,11 +3,12 @@ import event from '../event.js'
|
|
|
3
3
|
import recorder from '../recorder.js'
|
|
4
4
|
import store from '../store.js'
|
|
5
5
|
import output from '../output.js'
|
|
6
|
+
import container from '../container.js'
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
9
10
|
export default function () {
|
|
10
|
-
const helpers =
|
|
11
|
+
const helpers = container.helpers()
|
|
11
12
|
|
|
12
13
|
const runHelpersHook = (hook, param) => {
|
|
13
14
|
if (store.dryRun) return
|
|
@@ -29,11 +30,13 @@ export default function () {
|
|
|
29
30
|
event.dispatcher.on(event.suite.before, suite => {
|
|
30
31
|
// if (suite.parent) return; // only for root suite
|
|
31
32
|
runAsyncHelpersHook('_beforeSuite', suite, true)
|
|
33
|
+
recorder.catch()
|
|
32
34
|
})
|
|
33
35
|
|
|
34
36
|
event.dispatcher.on(event.suite.after, suite => {
|
|
35
37
|
// if (suite.parent) return; // only for root suite
|
|
36
38
|
runAsyncHelpersHook('_afterSuite', suite, true)
|
|
39
|
+
recorder.catch()
|
|
37
40
|
})
|
|
38
41
|
|
|
39
42
|
event.dispatcher.on(event.test.started, test => {
|