codeceptjs 4.0.1-beta.20 → 4.0.1-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/codecept.js CHANGED
@@ -91,7 +91,8 @@ program
91
91
  .option(commandFlags.profile.flag, commandFlags.profile.description)
92
92
  .option(commandFlags.ai.flag, commandFlags.ai.description)
93
93
  .option(commandFlags.config.flag, commandFlags.config.description)
94
- .action(commandHandler('../lib/command/interactive.js'))
94
+ .option('--file [path]', 'JavaScript file to execute in shell context')
95
+ .action(commandHandler('../lib/command/shell.js'))
95
96
 
96
97
  program.command('list [path]').alias('l').description('List all actions for I.').action(commandHandler('../lib/command/list.js'))
97
98
 
@@ -5,9 +5,17 @@ import Container from '../container.js'
5
5
  import event from '../event.js'
6
6
  import pause from '../pause.js'
7
7
  import output from '../output.js'
8
+ import { fileURLToPath } from 'url'
9
+ import { createRequire } from 'module'
10
+ import path from 'path'
11
+
12
+ const require = createRequire(import.meta.url)
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = path.dirname(__filename)
15
+
8
16
  const webHelpers = Container.STANDARD_ACTING_HELPERS
9
17
 
10
- export default async function (path, options) {
18
+ export default async function (shellPath, options) {
11
19
  // Backward compatibility for --profile
12
20
  process.profile = options.profile
13
21
  process.env.profile = options.profile
@@ -17,7 +25,7 @@ export default async function (path, options) {
17
25
  const testsPath = getTestRoot(configFile)
18
26
 
19
27
  const codecept = new Codecept(config, options)
20
- codecept.init(testsPath)
28
+ await codecept.init(testsPath)
21
29
 
22
30
  try {
23
31
  await codecept.bootstrap()
@@ -53,7 +61,27 @@ export default async function (path, options) {
53
61
  break
54
62
  }
55
63
  }
56
- pause()
64
+
65
+ if (options.file) {
66
+ const scriptPath = path.resolve(options.file)
67
+ output.print(`Executing script: ${scriptPath}`)
68
+
69
+ // Use the same I actor that pause() uses
70
+ const I = Container.support('I')
71
+ global.I = I
72
+ globalThis.I = I
73
+
74
+ recorder.add('execute script', async () => {
75
+ try {
76
+ await import(scriptPath)
77
+ output.print('Script executed successfully')
78
+ } catch (err) {
79
+ output.error(`Error executing script: ${err.message}`)
80
+ }
81
+ })
82
+ } else {
83
+ pause()
84
+ }
57
85
  recorder.add(() => event.emit(event.test.after, {}))
58
86
  recorder.add(() => event.emit(event.suite.after, {}))
59
87
  recorder.add(() => event.emit(event.all.result, {}))
@@ -2489,35 +2489,30 @@ class Playwright extends Helper {
2489
2489
  * {{> selectOption }}
2490
2490
  */
2491
2491
  async selectOption(select, option) {
2492
- const selectLocator = Locator.from(select, 'css')
2493
- let els = null
2492
+ const context = await this.context
2493
+ const matchedLocator = new Locator(select)
2494
2494
 
2495
- if (selectLocator.isFuzzy()) {
2496
- els = await findByRole(this.page, { role: 'listbox', name: selectLocator.value })
2497
- if (!els || els.length === 0) {
2498
- els = await findByRole(this.page, { role: 'combobox', name: selectLocator.value })
2499
- }
2500
- }
2501
-
2502
- if (!els || els.length === 0) {
2503
- els = await findFields.call(this, select)
2495
+ // Strict locator
2496
+ if (!matchedLocator.isFuzzy()) {
2497
+ this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2498
+ const els = await this._locate(matchedLocator)
2499
+ assertElementExists(els, select, 'Selectable element')
2500
+ return proceedSelect.call(this, context, els[0], option)
2504
2501
  }
2505
- assertElementExists(els, select, 'Selectable field')
2506
- const el = els[0]
2507
-
2508
- await highlightActiveElement.call(this, el)
2509
- let optionToSelect = ''
2510
2502
 
2511
- try {
2512
- optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim()
2513
- } catch (e) {
2514
- optionToSelect = option
2515
- }
2503
+ // Fuzzy: try combobox
2504
+ this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2505
+ let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
2506
+ if (els?.length) return proceedSelect.call(this, context, els[0], option)
2516
2507
 
2517
- if (!Array.isArray(option)) option = [optionToSelect]
2508
+ // Fuzzy: try listbox
2509
+ els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
2510
+ if (els?.length) return proceedSelect.call(this, context, els[0], option)
2518
2511
 
2519
- await el.selectOption(option)
2520
- return this._waitForAction()
2512
+ // Fuzzy: try native select
2513
+ els = await findFields.call(this, select)
2514
+ assertElementExists(els, select, 'Selectable element')
2515
+ return proceedSelect.call(this, context, els[0], option)
2521
2516
  }
2522
2517
 
2523
2518
  /**
@@ -4649,6 +4644,45 @@ async function findFields(locator) {
4649
4644
  return this._locate({ css: locator })
4650
4645
  }
4651
4646
 
4647
+ async function proceedSelect(context, el, option) {
4648
+ const role = await el.getAttribute('role')
4649
+ const options = Array.isArray(option) ? option : [option]
4650
+
4651
+ if (role === 'combobox') {
4652
+ this.debugSection('SelectOption', 'Expanding combobox')
4653
+ await highlightActiveElement.call(this, el)
4654
+ const [ariaOwns, ariaControls] = await Promise.all([el.getAttribute('aria-owns'), el.getAttribute('aria-controls')])
4655
+ await el.click()
4656
+ await this._waitForAction()
4657
+
4658
+ const listboxId = ariaOwns || ariaControls
4659
+ let listbox = listboxId ? context.locator(`#${listboxId}`).first() : null
4660
+ if (!listbox || !(await listbox.count())) listbox = context.getByRole('listbox').first()
4661
+
4662
+ for (const opt of options) {
4663
+ const optEl = listbox.getByRole('option', { name: opt }).first()
4664
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
4665
+ await highlightActiveElement.call(this, optEl)
4666
+ await optEl.click()
4667
+ }
4668
+ return this._waitForAction()
4669
+ }
4670
+
4671
+ if (role === 'listbox') {
4672
+ for (const opt of options) {
4673
+ const optEl = el.getByRole('option', { name: opt }).first()
4674
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
4675
+ await highlightActiveElement.call(this, optEl)
4676
+ await optEl.click()
4677
+ }
4678
+ return this._waitForAction()
4679
+ }
4680
+
4681
+ await highlightActiveElement.call(this, el)
4682
+ await el.selectOption(option)
4683
+ return this._waitForAction()
4684
+ }
4685
+
4652
4686
  async function proceedSeeInField(assertType, field, value) {
4653
4687
  const els = await findFields.call(this, field)
4654
4688
  assertElementExists(els, field, 'Field')
@@ -1617,33 +1617,30 @@ class Puppeteer extends Helper {
1617
1617
  * {{> selectOption }}
1618
1618
  */
1619
1619
  async selectOption(select, option) {
1620
- const els = await findVisibleFields.call(this, select)
1621
- assertElementExists(els, select, 'Selectable field')
1622
- const el = els[0]
1623
- if ((await el.getProperty('tagName').then(t => t.jsonValue())) !== 'SELECT') {
1624
- throw new Error('Element is not <select>')
1625
- }
1626
- highlightActiveElement.call(this, els[0], await this._getContext())
1627
- if (!Array.isArray(option)) option = [option]
1628
-
1629
- for (const key in option) {
1630
- const opt = xpathLocator.literal(option[key])
1631
- let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
1632
- if (optEl.length) {
1633
- this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1634
- continue
1635
- }
1636
- optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
1637
- if (optEl.length) {
1638
- this._evaluateHandeInContext(el => (el.selected = true), optEl[0])
1639
- }
1620
+ const context = await this._getContext()
1621
+ const matchedLocator = new Locator(select)
1622
+
1623
+ // Strict locator
1624
+ if (!matchedLocator.isFuzzy()) {
1625
+ this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1626
+ const els = await this._locate(matchedLocator)
1627
+ assertElementExists(els, select, 'Selectable element')
1628
+ return proceedSelect.call(this, context, els[0], option)
1640
1629
  }
1641
- await this._evaluateHandeInContext(element => {
1642
- element.dispatchEvent(new Event('input', { bubbles: true }))
1643
- element.dispatchEvent(new Event('change', { bubbles: true }))
1644
- }, el)
1645
1630
 
1646
- return this._waitForAction()
1631
+ // Fuzzy: try combobox
1632
+ this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1633
+ let els = await findByRole.call(this, context, { role: 'combobox', name: matchedLocator.value })
1634
+ if (els?.length) return proceedSelect.call(this, context, els[0], option)
1635
+
1636
+ // Fuzzy: try listbox
1637
+ els = await findByRole.call(this, context, { role: 'listbox', name: matchedLocator.value })
1638
+ if (els?.length) return proceedSelect.call(this, context, els[0], option)
1639
+
1640
+ // Fuzzy: try native select
1641
+ els = await findVisibleFields.call(this, select)
1642
+ assertElementExists(els, select, 'Selectable element')
1643
+ return proceedSelect.call(this, context, els[0], option)
1647
1644
  }
1648
1645
 
1649
1646
  /**
@@ -3160,6 +3157,90 @@ async function findFields(locator) {
3160
3157
  return this._locate({ css: matchedLocator.value })
3161
3158
  }
3162
3159
 
3160
+ async function proceedSelect(context, el, option) {
3161
+ const role = await el.evaluate(e => e.getAttribute('role'))
3162
+ const options = Array.isArray(option) ? option : [option]
3163
+
3164
+ if (role === 'combobox') {
3165
+ this.debugSection('SelectOption', 'Expanding combobox')
3166
+ highlightActiveElement.call(this, el, context)
3167
+ const [ariaOwns, ariaControls] = await el.evaluate(e => [e.getAttribute('aria-owns'), e.getAttribute('aria-controls')])
3168
+ await el.click()
3169
+ await this._waitForAction()
3170
+
3171
+ const listboxId = ariaOwns || ariaControls
3172
+ let listbox = listboxId ? await context.$(`#${listboxId}`) : null
3173
+ if (!listbox) {
3174
+ const listboxes = await context.$$('::-p-aria([role="listbox"])')
3175
+ listbox = listboxes[0]
3176
+ }
3177
+ if (!listbox) throw new Error('Cannot find listbox for combobox')
3178
+
3179
+ for (const opt of options) {
3180
+ const optionEls = await listbox.$$('::-p-aria([role="option"])')
3181
+ let optEl = null
3182
+ for (const optionEl of optionEls) {
3183
+ const text = await optionEl.evaluate(e => e.textContent.trim())
3184
+ if (text === opt || text.includes(opt)) {
3185
+ optEl = optionEl
3186
+ break
3187
+ }
3188
+ }
3189
+ if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3190
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3191
+ highlightActiveElement.call(this, optEl, context)
3192
+ await optEl.click()
3193
+ }
3194
+ return this._waitForAction()
3195
+ }
3196
+
3197
+ if (role === 'listbox') {
3198
+ highlightActiveElement.call(this, el, context)
3199
+ for (const opt of options) {
3200
+ const optionEls = await el.$$('::-p-aria([role="option"])')
3201
+ let optEl = null
3202
+ for (const optionEl of optionEls) {
3203
+ const text = await optionEl.evaluate(e => e.textContent.trim())
3204
+ if (text === opt || text.includes(opt)) {
3205
+ optEl = optionEl
3206
+ break
3207
+ }
3208
+ }
3209
+ if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3210
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3211
+ highlightActiveElement.call(this, optEl, context)
3212
+ await optEl.click()
3213
+ }
3214
+ return this._waitForAction()
3215
+ }
3216
+
3217
+ // Native <select> element
3218
+ const tagName = await el.evaluate(e => e.tagName)
3219
+ if (tagName !== 'SELECT') {
3220
+ throw new Error('Element is not <select>')
3221
+ }
3222
+
3223
+ highlightActiveElement.call(this, el, context)
3224
+ for (const key in options) {
3225
+ const opt = xpathLocator.literal(options[key])
3226
+ let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) })
3227
+ if (optEl.length) {
3228
+ this._evaluateHandeInContext(e => (e.selected = true), optEl[0])
3229
+ continue
3230
+ }
3231
+ optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) })
3232
+ if (optEl.length) {
3233
+ this._evaluateHandeInContext(e => (e.selected = true), optEl[0])
3234
+ }
3235
+ }
3236
+ await this._evaluateHandeInContext(element => {
3237
+ element.dispatchEvent(new Event('input', { bubbles: true }))
3238
+ element.dispatchEvent(new Event('change', { bubbles: true }))
3239
+ }, el)
3240
+
3241
+ return this._waitForAction()
3242
+ }
3243
+
3163
3244
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3164
3245
  const src = await this._locateElement(sourceLocator)
3165
3246
  if (!src) {
@@ -1302,33 +1302,29 @@ class WebDriver extends Helper {
1302
1302
  * {{> selectOption }}
1303
1303
  */
1304
1304
  async selectOption(select, option) {
1305
- const res = await findFields.call(this, select)
1306
- assertElementExists(res, select, 'Selectable field')
1307
- const elem = usingFirstElement(res)
1308
- highlightActiveElement.call(this, elem)
1305
+ const matchedLocator = new Locator(select)
1309
1306
 
1310
- if (!Array.isArray(option)) {
1311
- option = [option]
1307
+ // Strict locator
1308
+ if (!matchedLocator.isFuzzy()) {
1309
+ this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1310
+ const els = await this._locate(matchedLocator)
1311
+ assertElementExists(els, select, 'Selectable element')
1312
+ return proceedSelect.call(this, els[0], option)
1312
1313
  }
1313
1314
 
1314
- // select options by visible text
1315
- let els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
1315
+ // Fuzzy: try combobox
1316
+ this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1317
+ let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1318
+ if (els?.length) return proceedSelect.call(this, els[0], option)
1316
1319
 
1317
- const clickOptionFn = async el => {
1318
- if (el[0]) el = el[0]
1319
- const elementId = getElementId(el)
1320
- if (elementId) return this.browser.elementClick(elementId)
1321
- }
1320
+ // Fuzzy: try listbox
1321
+ els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1322
+ if (els?.length) return proceedSelect.call(this, els[0], option)
1322
1323
 
1323
- if (Array.isArray(els) && els.length) {
1324
- return forEachAsync(els, clickOptionFn)
1325
- }
1326
- // select options by value
1327
- els = await forEachAsync(option, async opt => this.browser.findElementsFromElement(getElementId(elem), 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
1328
- if (els.length === 0) {
1329
- throw new ElementNotFound(select, `Option "${option}" in`, 'was not found neither by a visible text nor by a value')
1330
- }
1331
- return forEachAsync(els, clickOptionFn)
1324
+ // Fuzzy: try native select
1325
+ els = await findFields.call(this, select)
1326
+ assertElementExists(els, select, 'Selectable element')
1327
+ return proceedSelect.call(this, els[0], option)
1332
1328
  }
1333
1329
 
1334
1330
  /**
@@ -3018,6 +3014,91 @@ async function findFields(locator) {
3018
3014
  return await this._locate(locator.value) // by css or xpath
3019
3015
  }
3020
3016
 
3017
+ async function proceedSelect(el, option) {
3018
+ const elementId = getElementId(el)
3019
+ const role = await this.browser.getElementAttribute(elementId, 'role')
3020
+ const options = Array.isArray(option) ? option : [option]
3021
+
3022
+ if (role === 'combobox') {
3023
+ this.debugSection('SelectOption', 'Expanding combobox')
3024
+ highlightActiveElement.call(this, el)
3025
+ const ariaOwns = await this.browser.getElementAttribute(elementId, 'aria-owns')
3026
+ const ariaControls = await this.browser.getElementAttribute(elementId, 'aria-controls')
3027
+ await this.browser.elementClick(elementId)
3028
+ await this._waitForAction()
3029
+
3030
+ const listboxId = ariaOwns || ariaControls
3031
+ let listboxEls = listboxId ? await this.browser.$$(`#${listboxId}`) : []
3032
+ if (!listboxEls.length) {
3033
+ listboxEls = await this.browser.$$('[role="listbox"]')
3034
+ }
3035
+ if (!listboxEls.length) throw new Error('Cannot find listbox for combobox')
3036
+ const listbox = listboxEls[0]
3037
+ const listboxElId = getElementId(listbox)
3038
+
3039
+ for (const opt of options) {
3040
+ const optionEls = await this.browser.findElementsFromElement(listboxElId, 'css selector', '[role="option"]')
3041
+ let optEl = null
3042
+ for (const optionEl of optionEls) {
3043
+ const optElId = getElementId(optionEl)
3044
+ const text = await this.browser.getElementText(optElId)
3045
+ if (text === opt || (text && text.includes(opt))) {
3046
+ optEl = optionEl
3047
+ break
3048
+ }
3049
+ }
3050
+ if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3051
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3052
+ highlightActiveElement.call(this, optEl)
3053
+ await this.browser.elementClick(getElementId(optEl))
3054
+ }
3055
+ return this._waitForAction()
3056
+ }
3057
+
3058
+ if (role === 'listbox') {
3059
+ highlightActiveElement.call(this, el)
3060
+ for (const opt of options) {
3061
+ const optionEls = await this.browser.findElementsFromElement(elementId, 'css selector', '[role="option"]')
3062
+ let optEl = null
3063
+ for (const optionEl of optionEls) {
3064
+ const optElId = getElementId(optionEl)
3065
+ const text = await this.browser.getElementText(optElId)
3066
+ if (text === opt || (text && text.includes(opt))) {
3067
+ optEl = optionEl
3068
+ break
3069
+ }
3070
+ }
3071
+ if (!optEl) throw new Error(`Cannot find option "${opt}" in listbox`)
3072
+ this.debugSection('SelectOption', `Clicking: "${opt}"`)
3073
+ highlightActiveElement.call(this, optEl)
3074
+ await this.browser.elementClick(getElementId(optEl))
3075
+ }
3076
+ return this._waitForAction()
3077
+ }
3078
+
3079
+ // Native <select> element
3080
+ highlightActiveElement.call(this, el)
3081
+
3082
+ // select options by visible text
3083
+ let els = await forEachAsync(options, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byVisibleText(xpathLocator.literal(opt))))
3084
+
3085
+ const clickOptionFn = async optEl => {
3086
+ if (optEl[0]) optEl = optEl[0]
3087
+ const optElId = getElementId(optEl)
3088
+ if (optElId) return this.browser.elementClick(optElId)
3089
+ }
3090
+
3091
+ if (Array.isArray(els) && els.length) {
3092
+ return forEachAsync(els, clickOptionFn)
3093
+ }
3094
+ // select options by value
3095
+ els = await forEachAsync(options, async opt => this.browser.findElementsFromElement(elementId, 'xpath', Locator.select.byValue(xpathLocator.literal(opt))))
3096
+ if (els.length === 0) {
3097
+ throw new ElementNotFound(el, `Option "${options}" in`, 'was not found neither by a visible text nor by a value')
3098
+ }
3099
+ return forEachAsync(els, clickOptionFn)
3100
+ }
3101
+
3021
3102
  async function proceedSeeField(assertType, field, value) {
3022
3103
  const res = await findFields.call(this, field)
3023
3104
  assertElementExists(res, field, 'Field')
package/lib/locator.js CHANGED
@@ -123,7 +123,11 @@ class Locator {
123
123
  * @returns {string}
124
124
  */
125
125
  toString() {
126
- return this.output || `{${this.type}: ${this.value}}`
126
+ if (this.output) return this.output
127
+ if (this.locator && this.value === this.locator) {
128
+ return JSON.stringify(this.locator)
129
+ }
130
+ return `{${this.type}: ${this.value}}`
127
131
  }
128
132
 
129
133
  /**
@@ -496,7 +500,7 @@ Locator.clickable = {
496
500
  `.//*[@aria-label = ${literal}]`,
497
501
  `.//*[@title = ${literal}]`,
498
502
  `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
499
- `.//*[@role='button'][normalize-space(.)=${literal}]`,
503
+ `.//*[@role='link' | @role='tab' | @role='menuitem' | @role='menuitemcheckbox' | @role='menuitemradio' | @role='option' | @role='radio' | @role='checkbox' | @role='switch' | @role='treeitem' | @role='gridcell' | @role='columnheader' | @role='rowheader' | @role='scrollbar' | @role='slider' | @role='spinbutton'][normalize-space(.)='${literal}']`,
500
504
  ]),
501
505
 
502
506
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.1-beta.20",
3
+ "version": "4.0.1-beta.22",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [