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.
Files changed (57) hide show
  1. package/bin/mcp-server.js +32 -5
  2. package/lib/ai.js +3 -2
  3. package/lib/assertions.js +18 -0
  4. package/lib/codecept.js +6 -6
  5. package/lib/command/check.js +2 -0
  6. package/lib/command/dryRun.js +2 -3
  7. package/lib/command/generate.js +2 -0
  8. package/lib/command/gherkin/snippets.js +5 -4
  9. package/lib/command/init.js +1 -0
  10. package/lib/command/run-multiple.js +2 -0
  11. package/lib/command/run-workers.js +1 -0
  12. package/lib/command/run.js +1 -1
  13. package/lib/command/workers/runTests.js +10 -10
  14. package/lib/container.js +63 -13
  15. package/lib/effects.js +17 -0
  16. package/lib/element/WebElement.js +128 -0
  17. package/lib/globals.js +22 -10
  18. package/lib/heal.js +4 -3
  19. package/lib/helper/ApiDataFactory.js +2 -1
  20. package/lib/helper/FileSystem.js +3 -2
  21. package/lib/helper/GraphQLDataFactory.js +2 -1
  22. package/lib/helper/Playwright.js +22 -17
  23. package/lib/helper/Puppeteer.js +20 -6
  24. package/lib/helper/WebDriver.js +12 -2
  25. package/lib/helper/errors/NonFocusedType.js +8 -0
  26. package/lib/helper/extras/Download.js +45 -0
  27. package/lib/helper/extras/focusCheck.js +43 -0
  28. package/lib/helper/extras/richTextEditor.js +115 -0
  29. package/lib/history.js +3 -2
  30. package/lib/listener/config.js +6 -4
  31. package/lib/listener/emptyRun.js +2 -1
  32. package/lib/listener/helpers.js +4 -1
  33. package/lib/listener/mocha.js +2 -1
  34. package/lib/listener/pageobjects.js +43 -0
  35. package/lib/listener/result.js +3 -2
  36. package/lib/locator.js +112 -0
  37. package/lib/mocha/cli.js +4 -2
  38. package/lib/mocha/factory.js +2 -1
  39. package/lib/mocha/scenarioConfig.js +2 -1
  40. package/lib/mocha/ui.js +5 -6
  41. package/lib/plugin/aiTrace.js +4 -3
  42. package/lib/plugin/analyze.js +1 -1
  43. package/lib/plugin/auth.js +3 -3
  44. package/lib/plugin/pageInfo.js +2 -1
  45. package/lib/plugin/pauseOn.js +167 -0
  46. package/lib/plugin/screenshotOnFail.js +3 -4
  47. package/lib/plugin/stepByStepReport.js +5 -4
  48. package/lib/rerun.js +2 -1
  49. package/lib/result.js +2 -1
  50. package/lib/step/base.js +3 -2
  51. package/lib/step/record.js +1 -1
  52. package/lib/store.js +72 -3
  53. package/lib/translation.js +2 -1
  54. package/lib/utils/mask_data.js +2 -1
  55. package/lib/utils.js +4 -3
  56. package/lib/workers.js +2 -0
  57. 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
- // 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)
@@ -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 = `${global.output_dir}/videos/`
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 (!global.__playwrightSelectorsRegistered) {
511
+ if (!defaultSelectorEnginesInitialized) {
514
512
  try {
515
513
  await playwright.selectors.register('__value', createValueEngine)
516
514
  await playwright.selectors.register('__disabled', createDisabledEngine)
517
- global.__playwrightSelectorsRegistered = true
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
- global.__playwrightSelectorsRegistered = true
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 = `${`${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}`
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(global.codecept_dir, harFilePath)
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(global.output_dir, fileName)
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(global.codecept_dir, pathToFile)
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 = `${`${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`
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 = `${`${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`
4840
4845
  await context.tracing.stop({ path: fileName })
4841
4846
  return fileName
4842
4847
  } catch (err) {
@@ -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 = `${`${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`
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(global.output_dir, downloadPath)
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(`${global.output_dir}/${fileName}`)
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
- 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
+ }
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(global.codecept_dir, pathToFile)
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 && global.debugMode) {
3581
+ if (this.options.highlightElement && store.debugMode) {
3568
3582
  highlightElement(element, context)
3569
3583
  }
3570
3584
  }
@@ -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(global.codecept_dir, pathToFile)
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 && global.debugMode) {
3493
+ if (this.options.highlightElement && store.debugMode) {
3484
3494
  highlightElement(element, this.browser)
3485
3495
  }
3486
3496
  }
@@ -0,0 +1,8 @@
1
+ class NonFocusedType extends Error {
2
+ constructor(message) {
3
+ super(message)
4
+ this.name = 'NonFocusedType'
5
+ }
6
+ }
7
+
8
+ export default NonFocusedType
@@ -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 }