codeceptjs 4.0.0-rc.11 → 4.0.0-rc.16

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.
Files changed (54) hide show
  1. package/lib/ai.js +3 -2
  2. package/lib/assertions.js +18 -0
  3. package/lib/codecept.js +6 -6
  4. package/lib/command/check.js +2 -1
  5. package/lib/command/dryRun.js +2 -3
  6. package/lib/command/generate.js +2 -0
  7. package/lib/command/gherkin/snippets.js +5 -4
  8. package/lib/command/init.js +1 -0
  9. package/lib/command/run-multiple.js +2 -0
  10. package/lib/command/run-workers.js +1 -0
  11. package/lib/command/run.js +1 -1
  12. package/lib/command/workers/runTests.js +10 -10
  13. package/lib/container.js +63 -13
  14. package/lib/effects.js +17 -0
  15. package/lib/element/WebElement.js +128 -0
  16. package/lib/globals.js +22 -10
  17. package/lib/heal.js +4 -3
  18. package/lib/helper/ApiDataFactory.js +2 -1
  19. package/lib/helper/FileSystem.js +3 -2
  20. package/lib/helper/GraphQLDataFactory.js +2 -1
  21. package/lib/helper/Playwright.js +17 -16
  22. package/lib/helper/Puppeteer.js +16 -6
  23. package/lib/helper/WebDriver.js +8 -2
  24. package/lib/helper/extras/Download.js +45 -0
  25. package/lib/helper/extras/richTextEditor.js +178 -0
  26. package/lib/history.js +3 -2
  27. package/lib/listener/config.js +6 -4
  28. package/lib/listener/emptyRun.js +2 -1
  29. package/lib/listener/helpers.js +4 -1
  30. package/lib/listener/mocha.js +2 -1
  31. package/lib/listener/pageobjects.js +43 -0
  32. package/lib/listener/result.js +3 -2
  33. package/lib/locator.js +112 -0
  34. package/lib/mocha/cli.js +4 -2
  35. package/lib/mocha/factory.js +2 -1
  36. package/lib/mocha/scenarioConfig.js +2 -1
  37. package/lib/mocha/ui.js +5 -6
  38. package/lib/plugin/aiTrace.js +4 -3
  39. package/lib/plugin/analyze.js +1 -1
  40. package/lib/plugin/auth.js +3 -3
  41. package/lib/plugin/pageInfo.js +2 -1
  42. package/lib/plugin/pauseOn.js +167 -0
  43. package/lib/plugin/screenshotOnFail.js +3 -4
  44. package/lib/plugin/stepByStepReport.js +5 -4
  45. package/lib/rerun.js +2 -1
  46. package/lib/result.js +2 -1
  47. package/lib/step/base.js +3 -2
  48. package/lib/step/record.js +1 -1
  49. package/lib/store.js +72 -3
  50. package/lib/translation.js +2 -1
  51. package/lib/utils/mask_data.js +2 -1
  52. package/lib/utils.js +4 -3
  53. package/lib/workers.js +2 -0
  54. 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
- // Set up actor global - will use container when available
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, global.container || container)
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(global.inject())
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 = global.container.support('I')
120
+ const I = container.support('I')
120
121
  await eval(codeSnippet)
121
122
  } else if (typeof codeSnippet === 'function') {
122
- await codeSnippet(global.container.support())
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(global.codecept_dir, modulePath)
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)
@@ -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 = global.codecept_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(global.codecept_dir, openPath)
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(global.codecept_dir, modulePath)
255
+ modulePath = path.join(store.codeceptDir, modulePath)
255
256
  }
256
257
  const builder = require(modulePath)
257
258
  return builder.build(data)
@@ -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 = `${global.output_dir}/videos/`
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 (!global.__playwrightSelectorsRegistered) {
511
+ if (!defaultSelectorEnginesInitialized) {
515
512
  try {
516
513
  await playwright.selectors.register('__value', createValueEngine)
517
514
  await playwright.selectors.register('__disabled', createDisabledEngine)
518
- global.__playwrightSelectorsRegistered = true
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
- global.__playwrightSelectorsRegistered = true
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 = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
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(global.codecept_dir, harFilePath)
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(global.output_dir, fileName)
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(global.codecept_dir, pathToFile)
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 = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
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 = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
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) {
@@ -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 = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json`
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(global.output_dir, downloadPath)
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(`${global.output_dir}/${fileName}`)
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
- const els = await findVisibleFields.call(this, field, context)
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(global.codecept_dir, pathToFile)
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 && global.debugMode) {
3581
+ if (this.options.highlightElement && store.debugMode) {
3572
3582
  highlightElement(element, context)
3573
3583
  }
3574
3584
  }
@@ -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(global.codecept_dir, pathToFile)
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 && global.debugMode) {
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,178 @@
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
+ UNREACHABLE: 'unreachable',
11
+ }
12
+
13
+ function detectAndMark(el, opts) {
14
+ const marker = opts.marker
15
+ const kinds = opts.kinds
16
+ const CE = '[contenteditable="true"], [contenteditable=""]'
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 isFormHidden = tag === 'INPUT' && el.type === 'hidden'
31
+ if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
32
+ const style = window.getComputedStyle(el)
33
+ if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
34
+ }
35
+
36
+ const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
37
+ if (canSearchDescendants) {
38
+ const iframe = el.querySelector('iframe')
39
+ if (iframe) return mark(kinds.IFRAME, iframe)
40
+ const ce = el.querySelector(CE)
41
+ if (ce) return mark(kinds.CONTENTEDITABLE, ce)
42
+ const textareas = [...el.querySelectorAll('textarea')]
43
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
44
+ const textarea = focusable || textareas[0]
45
+ if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
46
+ }
47
+
48
+ return mark(kinds.STANDARD, el)
49
+ }
50
+
51
+ function detectInsideFrame() {
52
+ const MARKER = 'data-codeceptjs-rte-target'
53
+ const CE = '[contenteditable="true"], [contenteditable=""]'
54
+ const CONTENTEDITABLE = 'contenteditable'
55
+ const HIDDEN_TEXTAREA = 'hidden-textarea'
56
+ const body = document.body
57
+ document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
58
+
59
+ if (body.isContentEditable) return CONTENTEDITABLE
60
+
61
+ const ce = body.querySelector(CE)
62
+ if (ce) {
63
+ ce.setAttribute(MARKER, '1')
64
+ return CONTENTEDITABLE
65
+ }
66
+
67
+ const textareas = [...body.querySelectorAll('textarea')]
68
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
69
+ const textarea = focusable || textareas[0]
70
+ if (textarea) {
71
+ textarea.setAttribute(MARKER, '1')
72
+ return HIDDEN_TEXTAREA
73
+ }
74
+
75
+ return CONTENTEDITABLE
76
+ }
77
+
78
+ async function evaluateInFrame(helper, body, fn) {
79
+ if (body.helperType === 'webdriver') {
80
+ return helper.executeScript(fn)
81
+ }
82
+ return body.element.evaluate(fn)
83
+ }
84
+
85
+ function focusMarkedInFrameScript() {
86
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
87
+ el.focus()
88
+ return document.activeElement === el
89
+ }
90
+
91
+ function selectAllInFrameScript() {
92
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
93
+ el.focus()
94
+ const range = document.createRange()
95
+ range.selectNodeContents(el)
96
+ const sel = window.getSelection()
97
+ sel.removeAllRanges()
98
+ sel.addRange(range)
99
+ return document.activeElement === el
100
+ }
101
+
102
+ function selectAllInEditable(el) {
103
+ const doc = el.ownerDocument
104
+ const win = doc.defaultView
105
+ el.focus()
106
+ const range = doc.createRange()
107
+ range.selectNodeContents(el)
108
+ const sel = win.getSelection()
109
+ sel.removeAllRanges()
110
+ sel.addRange(range)
111
+ }
112
+
113
+ function unmarkAll(marker) {
114
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
115
+ }
116
+
117
+ function isActive(el) {
118
+ return el.ownerDocument.activeElement === el
119
+ }
120
+
121
+ async function assertFocused(target) {
122
+ const focused = await target.evaluate(isActive)
123
+ if (!focused) {
124
+ throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
125
+ }
126
+ }
127
+
128
+ async function findMarked(helper) {
129
+ const root = helper.page || helper.browser
130
+ const raw = await root.$('[' + MARKER + ']')
131
+ return new WebElement(raw, helper)
132
+ }
133
+
134
+ async function clearMarker(helper) {
135
+ if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
136
+ return helper.executeScript(unmarkAll, MARKER)
137
+ }
138
+
139
+ export async function fillRichEditor(helper, el, value) {
140
+ const source = el instanceof WebElement ? el : new WebElement(el, helper)
141
+ const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
142
+ if (kind === EDITOR.STANDARD) return false
143
+ if (kind === EDITOR.UNREACHABLE) {
144
+ throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
145
+ }
146
+
147
+ const target = await findMarked(helper)
148
+ const delay = helper.options.pressKeyDelay
149
+
150
+ if (kind === EDITOR.IFRAME) {
151
+ await target.inIframe(async body => {
152
+ const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
153
+ if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
154
+ const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
155
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
156
+ await body.selectAllAndDelete()
157
+ await body.typeText(value, { delay })
158
+ } else {
159
+ const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
160
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
161
+ await body.typeText(value, { delay })
162
+ }
163
+ })
164
+ } else if (kind === EDITOR.HIDDEN_TEXTAREA) {
165
+ await target.focus()
166
+ await assertFocused(target)
167
+ await target.selectAllAndDelete()
168
+ await target.typeText(value, { delay })
169
+ } else if (kind === EDITOR.CONTENTEDITABLE) {
170
+ await target.click()
171
+ await target.evaluate(selectAllInEditable)
172
+ await assertFocused(target)
173
+ await target.typeText(value, { delay })
174
+ }
175
+
176
+ await clearMarker(helper)
177
+ return true
178
+ }
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 (global.output_dir) {
13
- this.historyFile = path.join(global.output_dir, 'cli-history')
13
+ if (store.outputDir) {
14
+ this.historyFile = path.join(store.outputDir, 'cli-history')
14
15
  }
15
16
  this.commands = []
16
17
  }