codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20

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 (294) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1187 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +159 -0
  7. package/docs/ai.md +537 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/assertions.md +415 -0
  11. package/docs/auth.md +318 -0
  12. package/docs/basics.md +424 -0
  13. package/docs/bdd.md +539 -0
  14. package/docs/best.md +240 -0
  15. package/docs/bootstrap.md +132 -0
  16. package/docs/commands.md +352 -0
  17. package/docs/community-helpers.md +63 -0
  18. package/docs/configuration.md +230 -0
  19. package/docs/continuous-integration.md +497 -0
  20. package/docs/custom-helpers.md +297 -0
  21. package/docs/data.md +448 -0
  22. package/docs/debugging.md +332 -0
  23. package/docs/detox.md +235 -0
  24. package/docs/docker.md +136 -0
  25. package/docs/effects.md +179 -0
  26. package/docs/element-based-testing.md +295 -0
  27. package/docs/element-selection.md +125 -0
  28. package/docs/els.md +328 -0
  29. package/docs/examples.md +161 -0
  30. package/docs/heal.md +213 -0
  31. package/docs/helpers/ApiDataFactory.md +267 -0
  32. package/docs/helpers/Appium.md +1405 -0
  33. package/docs/helpers/Detox.md +665 -0
  34. package/docs/helpers/ExpectHelper.md +275 -0
  35. package/docs/helpers/FileSystem.md +152 -0
  36. package/docs/helpers/GraphQL.md +152 -0
  37. package/docs/helpers/GraphQLDataFactory.md +226 -0
  38. package/docs/helpers/JSONResponse.md +255 -0
  39. package/docs/helpers/Mochawesome.md +8 -0
  40. package/docs/helpers/MockRequest.md +377 -0
  41. package/docs/helpers/MockServer.md +212 -0
  42. package/docs/helpers/Playwright.md +2969 -0
  43. package/docs/helpers/Polly.md +44 -0
  44. package/docs/helpers/Protractor.md +1769 -0
  45. package/docs/helpers/Puppeteer-firefox.md +86 -0
  46. package/docs/helpers/Puppeteer.md +2690 -0
  47. package/docs/helpers/REST.md +289 -0
  48. package/docs/helpers/SoftExpectHelper.md +352 -0
  49. package/docs/helpers/WebDriver.md +2682 -0
  50. package/docs/hooks.md +339 -0
  51. package/docs/index.md +111 -0
  52. package/docs/installation.md +83 -0
  53. package/docs/internal-api.md +265 -0
  54. package/docs/internal-test-server.md +89 -0
  55. package/docs/locators.md +355 -0
  56. package/docs/mcp.md +485 -0
  57. package/docs/migration-4.md +556 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +585 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins.md +866 -0
  63. package/docs/puppeteer.md +314 -0
  64. package/docs/quickstart.md +120 -0
  65. package/docs/react.md +70 -0
  66. package/docs/reports.md +483 -0
  67. package/docs/retry.md +274 -0
  68. package/docs/secrets.md +150 -0
  69. package/docs/sessions.md +80 -0
  70. package/docs/shadow.md +68 -0
  71. package/docs/test-structure.md +275 -0
  72. package/docs/timeouts.md +183 -0
  73. package/docs/translation.md +247 -0
  74. package/docs/tutorial.md +271 -0
  75. package/docs/typescript.md +374 -0
  76. package/docs/web-element.md +251 -0
  77. package/docs/webdriver.md +708 -0
  78. package/docs/within.md +55 -0
  79. package/lib/ai.js +3 -2
  80. package/lib/aria.js +260 -0
  81. package/lib/assertions.js +18 -0
  82. package/lib/codecept.js +26 -23
  83. package/lib/command/check.js +2 -1
  84. package/lib/command/dryRun.js +24 -5
  85. package/lib/command/generate.js +2 -0
  86. package/lib/command/gherkin/snippets.js +5 -4
  87. package/lib/command/init.js +248 -269
  88. package/lib/command/list.js +150 -10
  89. package/lib/command/query.js +218 -0
  90. package/lib/command/run-multiple.js +2 -0
  91. package/lib/command/run-workers.js +2 -0
  92. package/lib/command/run.js +1 -1
  93. package/lib/command/workers/runTests.js +10 -10
  94. package/lib/config.js +77 -4
  95. package/lib/container.js +114 -17
  96. package/lib/effects.js +17 -0
  97. package/lib/element/WebElement.js +246 -2
  98. package/lib/els.js +12 -6
  99. package/lib/globals.js +32 -19
  100. package/lib/heal.js +4 -3
  101. package/lib/helper/ApiDataFactory.js +2 -1
  102. package/lib/helper/Appium.js +8 -8
  103. package/lib/helper/FileSystem.js +3 -2
  104. package/lib/helper/GraphQLDataFactory.js +2 -1
  105. package/lib/helper/Playwright.js +228 -162
  106. package/lib/helper/Puppeteer.js +208 -76
  107. package/lib/helper/WebDriver.js +173 -68
  108. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  109. package/lib/helper/errors/NonFocusedType.js +8 -0
  110. package/lib/helper/extras/Download.js +45 -0
  111. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  112. package/lib/helper/extras/elementSelection.js +58 -0
  113. package/lib/helper/extras/focusCheck.js +43 -0
  114. package/lib/helper/extras/richTextEditor.js +178 -0
  115. package/lib/helper/scripts/dropFile.js +11 -0
  116. package/lib/history.js +3 -2
  117. package/lib/html.js +103 -16
  118. package/lib/index.js +9 -1
  119. package/lib/listener/config.js +6 -4
  120. package/lib/listener/emptyRun.js +2 -1
  121. package/lib/listener/globalRetry.js +32 -6
  122. package/lib/listener/helpers.js +4 -1
  123. package/lib/listener/mocha.js +2 -1
  124. package/lib/listener/pageobjects.js +43 -0
  125. package/lib/listener/result.js +3 -2
  126. package/lib/locator.js +126 -3
  127. package/lib/mocha/cli.js +14 -2
  128. package/lib/mocha/factory.js +7 -2
  129. package/lib/mocha/inject.js +1 -1
  130. package/lib/mocha/scenarioConfig.js +2 -1
  131. package/lib/mocha/ui.js +5 -6
  132. package/lib/parser.js +2 -2
  133. package/lib/pause.js +38 -4
  134. package/lib/plugin/aiTrace.js +453 -0
  135. package/lib/plugin/analyze.js +1 -1
  136. package/lib/plugin/auth.js +3 -3
  137. package/lib/plugin/browser.js +77 -0
  138. package/lib/plugin/expose.js +159 -0
  139. package/lib/plugin/heal.js +44 -1
  140. package/lib/plugin/pageInfo.js +53 -49
  141. package/lib/plugin/pause.js +131 -0
  142. package/lib/plugin/pauseOnFail.js +10 -34
  143. package/lib/plugin/retryFailedStep.js +28 -19
  144. package/lib/plugin/screencast.js +287 -0
  145. package/lib/plugin/screenshot.js +563 -0
  146. package/lib/plugin/screenshotOnFail.js +8 -171
  147. package/lib/rerun.js +2 -1
  148. package/lib/result.js +2 -1
  149. package/lib/step/base.js +3 -2
  150. package/lib/step/config.js +15 -2
  151. package/lib/step/record.js +2 -2
  152. package/lib/store.js +72 -3
  153. package/lib/translation.js +2 -1
  154. package/lib/utils/mask_data.js +2 -1
  155. package/lib/utils/pluginParser.js +151 -0
  156. package/lib/utils/trace.js +297 -0
  157. package/lib/utils.js +77 -3
  158. package/lib/workers.js +52 -22
  159. package/package.json +19 -13
  160. package/typings/index.d.ts +19 -5
  161. package/docs/webapi/amOnPage.mustache +0 -11
  162. package/docs/webapi/appendField.mustache +0 -11
  163. package/docs/webapi/attachFile.mustache +0 -12
  164. package/docs/webapi/blur.mustache +0 -18
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -9
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -29
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  177. package/docs/webapi/dontSeeElement.mustache +0 -8
  178. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  179. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  180. package/docs/webapi/dontSeeInField.mustache +0 -11
  181. package/docs/webapi/dontSeeInSource.mustache +0 -8
  182. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  183. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  184. package/docs/webapi/doubleClick.mustache +0 -13
  185. package/docs/webapi/downloadFile.mustache +0 -12
  186. package/docs/webapi/dragAndDrop.mustache +0 -9
  187. package/docs/webapi/dragSlider.mustache +0 -11
  188. package/docs/webapi/executeAsyncScript.mustache +0 -24
  189. package/docs/webapi/executeScript.mustache +0 -26
  190. package/docs/webapi/fillField.mustache +0 -16
  191. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  192. package/docs/webapi/focus.mustache +0 -13
  193. package/docs/webapi/forceClick.mustache +0 -28
  194. package/docs/webapi/forceRightClick.mustache +0 -18
  195. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  196. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  197. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  198. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  199. package/docs/webapi/grabCookie.mustache +0 -11
  200. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  201. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  202. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  203. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  204. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  205. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  206. package/docs/webapi/grabGeoLocation.mustache +0 -8
  207. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  208. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  209. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  210. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  211. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  212. package/docs/webapi/grabPopupText.mustache +0 -5
  213. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  214. package/docs/webapi/grabSource.mustache +0 -8
  215. package/docs/webapi/grabTextFrom.mustache +0 -10
  216. package/docs/webapi/grabTextFromAll.mustache +0 -9
  217. package/docs/webapi/grabTitle.mustache +0 -8
  218. package/docs/webapi/grabValueFrom.mustache +0 -9
  219. package/docs/webapi/grabValueFromAll.mustache +0 -8
  220. package/docs/webapi/grabWebElement.mustache +0 -9
  221. package/docs/webapi/grabWebElements.mustache +0 -9
  222. package/docs/webapi/moveCursorTo.mustache +0 -12
  223. package/docs/webapi/openNewTab.mustache +0 -7
  224. package/docs/webapi/pressKey.mustache +0 -12
  225. package/docs/webapi/pressKeyDown.mustache +0 -12
  226. package/docs/webapi/pressKeyUp.mustache +0 -12
  227. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  228. package/docs/webapi/refreshPage.mustache +0 -6
  229. package/docs/webapi/resizeWindow.mustache +0 -6
  230. package/docs/webapi/rightClick.mustache +0 -14
  231. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  232. package/docs/webapi/saveScreenshot.mustache +0 -12
  233. package/docs/webapi/say.mustache +0 -10
  234. package/docs/webapi/scrollIntoView.mustache +0 -11
  235. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  236. package/docs/webapi/scrollPageToTop.mustache +0 -6
  237. package/docs/webapi/scrollTo.mustache +0 -12
  238. package/docs/webapi/see.mustache +0 -11
  239. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  240. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  241. package/docs/webapi/seeCookie.mustache +0 -8
  242. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  243. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  244. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  245. package/docs/webapi/seeElement.mustache +0 -8
  246. package/docs/webapi/seeElementInDOM.mustache +0 -8
  247. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  248. package/docs/webapi/seeInField.mustache +0 -12
  249. package/docs/webapi/seeInPopup.mustache +0 -8
  250. package/docs/webapi/seeInSource.mustache +0 -7
  251. package/docs/webapi/seeInTitle.mustache +0 -8
  252. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  253. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  254. package/docs/webapi/seeTextEquals.mustache +0 -9
  255. package/docs/webapi/seeTitleEquals.mustache +0 -8
  256. package/docs/webapi/seeTraffic.mustache +0 -36
  257. package/docs/webapi/selectOption.mustache +0 -21
  258. package/docs/webapi/setCookie.mustache +0 -16
  259. package/docs/webapi/setGeoLocation.mustache +0 -12
  260. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  261. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  262. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  263. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  264. package/docs/webapi/switchTo.mustache +0 -9
  265. package/docs/webapi/switchToNextTab.mustache +0 -10
  266. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  267. package/docs/webapi/type.mustache +0 -21
  268. package/docs/webapi/uncheckOption.mustache +0 -13
  269. package/docs/webapi/wait.mustache +0 -8
  270. package/docs/webapi/waitForClickable.mustache +0 -11
  271. package/docs/webapi/waitForCookie.mustache +0 -9
  272. package/docs/webapi/waitForDetached.mustache +0 -10
  273. package/docs/webapi/waitForDisabled.mustache +0 -6
  274. package/docs/webapi/waitForElement.mustache +0 -11
  275. package/docs/webapi/waitForEnabled.mustache +0 -6
  276. package/docs/webapi/waitForFunction.mustache +0 -17
  277. package/docs/webapi/waitForInvisible.mustache +0 -10
  278. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  279. package/docs/webapi/waitForText.mustache +0 -13
  280. package/docs/webapi/waitForValue.mustache +0 -10
  281. package/docs/webapi/waitForVisible.mustache +0 -10
  282. package/docs/webapi/waitInUrl.mustache +0 -9
  283. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  284. package/docs/webapi/waitToHide.mustache +0 -10
  285. package/docs/webapi/waitUrlEquals.mustache +0 -10
  286. package/lib/helper/AI.js +0 -214
  287. package/lib/listener/enhancedGlobalRetry.js +0 -110
  288. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  289. package/lib/plugin/htmlReporter.js +0 -3648
  290. package/lib/plugin/stepByStepReport.js +0 -427
  291. package/lib/plugin/subtitles.js +0 -89
  292. package/lib/retryCoordinator.js +0 -207
  293. package/typings/promiseBasedTypes.d.ts +0 -9469
  294. package/typings/types.d.ts +0 -11402
@@ -1,5 +1,6 @@
1
1
  let webdriverio
2
2
 
3
+ import fs from 'fs'
3
4
  import assert from 'assert'
4
5
  import path from 'path'
5
6
  import crypto from 'crypto'
@@ -9,21 +10,39 @@ import promiseRetry from 'promise-retry'
9
10
  import { includes as stringIncludes } from '../assert/include.js'
10
11
  import { urlEquals, equals } from '../assert/equal.js'
11
12
  import store from '../store.js'
13
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
12
14
  import output from '../output.js'
13
15
  const { debug } = output
14
16
  import { empty } from '../assert/empty.js'
15
17
  import { truth } from '../assert/truth.js'
16
- import { xpathLocator, fileExists, decodeUrl, chunkArray, convertCssPropertiesToCamelCase, screenshotOutputFolder, getNormalizedKeyAttributeValue, modifierKeys } from '../utils.js'
18
+ import {
19
+ xpathLocator,
20
+ fileExists,
21
+ decodeUrl,
22
+ chunkArray,
23
+ convertCssPropertiesToCamelCase,
24
+ screenshotOutputFolder,
25
+ getNormalizedKeyAttributeValue,
26
+ modifierKeys,
27
+ normalizePath,
28
+ resolveUrl,
29
+ getMimeType,
30
+ base64EncodeFile,
31
+ } from '../utils.js'
17
32
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
18
33
  import ElementNotFound from './errors/ElementNotFound.js'
34
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
19
35
  import ConnectionRefused from './errors/ConnectionRefused.js'
20
36
  import Locator from '../locator.js'
21
37
  import { highlightElement } from './scripts/highlightElement.js'
22
38
  import { focusElement } from './scripts/focusElement.js'
23
39
  import { blurElement } from './scripts/blurElement.js'
24
40
  import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
41
+ import { dropFile } from './scripts/dropFile.js'
25
42
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
26
43
  import WebElement from '../element/WebElement.js'
44
+ import { selectElement } from './extras/elementSelection.js'
45
+ import { fillRichEditor } from './extras/richTextEditor.js'
27
46
 
28
47
  const SHADOW = 'shadow'
29
48
  const webRoot = 'body'
@@ -489,6 +508,7 @@ class WebDriver extends Helper {
489
508
  keepBrowserState: false,
490
509
  deprecationWarnings: false,
491
510
  highlightElement: false,
511
+ strict: false,
492
512
  }
493
513
 
494
514
  // override defaults with config
@@ -1076,7 +1096,7 @@ class WebDriver extends Helper {
1076
1096
  } else {
1077
1097
  assertElementExists(res, locator, 'Clickable element')
1078
1098
  }
1079
- const elem = usingFirstElement(res)
1099
+ const elem = selectElement(res, locator, this)
1080
1100
  highlightActiveElement.call(this, elem)
1081
1101
  return this.browser[clickMethod](getElementId(elem))
1082
1102
  }
@@ -1095,7 +1115,7 @@ class WebDriver extends Helper {
1095
1115
  } else {
1096
1116
  assertElementExists(res, locator, 'Clickable element')
1097
1117
  }
1098
- const elem = usingFirstElement(res)
1118
+ const elem = selectElement(res, locator, this)
1099
1119
  highlightActiveElement.call(this, elem)
1100
1120
 
1101
1121
  return this.executeScript(el => {
@@ -1123,7 +1143,7 @@ class WebDriver extends Helper {
1123
1143
  assertElementExists(res, locator, 'Clickable element')
1124
1144
  }
1125
1145
 
1126
- const elem = usingFirstElement(res)
1146
+ const elem = selectElement(res, locator, this)
1127
1147
  highlightActiveElement.call(this, elem)
1128
1148
  return elem.doubleClick()
1129
1149
  }
@@ -1143,7 +1163,7 @@ class WebDriver extends Helper {
1143
1163
  assertElementExists(res, locator, 'Clickable element')
1144
1164
  }
1145
1165
 
1146
- const el = usingFirstElement(res)
1166
+ const el = selectElement(res, locator, this)
1147
1167
 
1148
1168
  await el.moveTo()
1149
1169
 
@@ -1255,11 +1275,16 @@ class WebDriver extends Helper {
1255
1275
  * {{ custom }}
1256
1276
  *
1257
1277
  */
1258
- async fillField(field, value) {
1259
- const res = await findFields.call(this, field)
1278
+ async fillField(field, value, context = null) {
1279
+ const res = await findFields.call(this, field, context)
1260
1280
  assertElementExists(res, field, 'Field')
1261
- const elem = usingFirstElement(res)
1281
+ const elem = selectElement(res, field, this)
1262
1282
  highlightActiveElement.call(this, elem)
1283
+
1284
+ if (await fillRichEditor(this, elem, value)) {
1285
+ return
1286
+ }
1287
+
1263
1288
  try {
1264
1289
  await elem.clearValue()
1265
1290
  } catch (err) {
@@ -1278,10 +1303,10 @@ class WebDriver extends Helper {
1278
1303
  * {{> appendField }}
1279
1304
  * {{ react }}
1280
1305
  */
1281
- async appendField(field, value) {
1282
- const res = await findFields.call(this, field)
1306
+ async appendField(field, value, context = null) {
1307
+ const res = await findFields.call(this, field, context)
1283
1308
  assertElementExists(res, field, 'Field')
1284
- const elem = usingFirstElement(res)
1309
+ const elem = selectElement(res, field, this)
1285
1310
  highlightActiveElement.call(this, elem)
1286
1311
  return elem.addValue(value.toString())
1287
1312
  }
@@ -1290,10 +1315,10 @@ class WebDriver extends Helper {
1290
1315
  * {{> clearField }}
1291
1316
  *
1292
1317
  */
1293
- async clearField(field) {
1294
- const res = await findFields.call(this, field)
1318
+ async clearField(field, context = null) {
1319
+ const res = await findFields.call(this, field, context)
1295
1320
  assertElementExists(res, field, 'Field')
1296
- const elem = usingFirstElement(res)
1321
+ const elem = selectElement(res, field, this)
1297
1322
  highlightActiveElement.call(this, elem)
1298
1323
  return elem.clearValue(getElementId(elem))
1299
1324
  }
@@ -1301,30 +1326,31 @@ class WebDriver extends Helper {
1301
1326
  /**
1302
1327
  * {{> selectOption }}
1303
1328
  */
1304
- async selectOption(select, option) {
1329
+ async selectOption(select, option, context = null) {
1330
+ const locateFn = prepareLocateFn.call(this, context)
1305
1331
  const matchedLocator = new Locator(select)
1306
1332
 
1307
1333
  // Strict locator
1308
1334
  if (!matchedLocator.isFuzzy()) {
1309
1335
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1310
- const els = await this._locate(select)
1336
+ const els = await locateFn(select)
1311
1337
  assertElementExists(els, select, 'Selectable element')
1312
- return proceedSelectOption.call(this, usingFirstElement(els), option)
1338
+ return proceedSelectOption.call(this, selectElement(els, select, this), option)
1313
1339
  }
1314
1340
 
1315
1341
  // Fuzzy: try combobox
1316
1342
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1317
1343
  let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
1318
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1344
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1319
1345
 
1320
1346
  // Fuzzy: try listbox
1321
1347
  els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
1322
- if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option)
1348
+ if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
1323
1349
 
1324
1350
  // Fuzzy: try native select
1325
- const res = await findFields.call(this, select)
1351
+ const res = await findFields.call(this, select, context)
1326
1352
  assertElementExists(res, select, 'Selectable field')
1327
- return proceedSelectOption.call(this, usingFirstElement(res), option)
1353
+ return proceedSelectOption.call(this, selectElement(res, select, this), option)
1328
1354
  }
1329
1355
 
1330
1356
  /**
@@ -1332,28 +1358,41 @@ class WebDriver extends Helper {
1332
1358
  *
1333
1359
  * {{> attachFile }}
1334
1360
  */
1335
- async attachFile(locator, pathToFile) {
1336
- let file = path.join(global.codecept_dir, pathToFile)
1361
+ async attachFile(locator, pathToFile, context = null) {
1362
+ let file = path.join(store.codeceptDir, pathToFile)
1337
1363
  if (!fileExists(file)) {
1338
1364
  throw new Error(`File at ${file} can not be found on local system`)
1339
1365
  }
1340
1366
 
1341
- const res = await findFields.call(this, locator)
1367
+ const res = await findFields.call(this, locator, context)
1342
1368
  this.debug(`Uploading ${file}`)
1343
- assertElementExists(res, locator, 'File field')
1344
- const el = usingFirstElement(res)
1345
1369
 
1346
- // Remote Upload (when running Selenium Server)
1347
- if (this.options.remoteFileUpload) {
1348
- try {
1349
- this.debugSection('File', 'Uploading file to remote server')
1350
- file = await this.browser.uploadFile(file)
1351
- } catch (err) {
1352
- throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
1370
+ if (res.length) {
1371
+ const el = selectElement(res, locator, this)
1372
+ const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
1373
+ const type = await this.browser.execute(function (elem) { return elem.type }, el)
1374
+ if (tag === 'INPUT' && type === 'file') {
1375
+ if (this.options.remoteFileUpload) {
1376
+ try {
1377
+ this.debugSection('File', 'Uploading file to remote server')
1378
+ file = await this.browser.uploadFile(file)
1379
+ } catch (err) {
1380
+ throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
1381
+ }
1382
+ }
1383
+ return el.addValue(file)
1353
1384
  }
1354
1385
  }
1355
1386
 
1356
- return el.addValue(file)
1387
+ const targetRes = res.length ? res : await this._locate(locator)
1388
+ assertElementExists(targetRes, locator, 'Element')
1389
+ const targetEl = selectElement(targetRes, locator, this)
1390
+ const fileData = {
1391
+ base64Content: base64EncodeFile(file),
1392
+ fileName: path.basename(file),
1393
+ mimeType: getMimeType(path.basename(file)),
1394
+ }
1395
+ return this.browser.execute(dropFile, targetEl, fileData)
1357
1396
  }
1358
1397
 
1359
1398
  /**
@@ -1367,7 +1406,7 @@ class WebDriver extends Helper {
1367
1406
  const res = await findCheckable.call(this, field, locateFn)
1368
1407
 
1369
1408
  assertElementExists(res, field, 'Checkable')
1370
- const elem = usingFirstElement(res)
1409
+ const elem = selectElement(res, field, this)
1371
1410
  const elementId = getElementId(elem)
1372
1411
  highlightActiveElement.call(this, elem)
1373
1412
 
@@ -1388,7 +1427,7 @@ class WebDriver extends Helper {
1388
1427
  const res = await findCheckable.call(this, field, locateFn)
1389
1428
 
1390
1429
  assertElementExists(res, field, 'Checkable')
1391
- const elem = usingFirstElement(res)
1430
+ const elem = selectElement(res, field, this)
1392
1431
  const elementId = getElementId(elem)
1393
1432
  highlightActiveElement.call(this, elem)
1394
1433
 
@@ -1586,18 +1625,18 @@ class WebDriver extends Helper {
1586
1625
  * {{> seeInField }}
1587
1626
  *
1588
1627
  */
1589
- async seeInField(field, value) {
1628
+ async seeInField(field, value, context = null) {
1590
1629
  const _value = typeof value === 'boolean' ? value : value.toString()
1591
- return proceedSeeField.call(this, 'assert', field, _value)
1630
+ return proceedSeeField.call(this, 'assert', field, _value, context)
1592
1631
  }
1593
1632
 
1594
1633
  /**
1595
1634
  * {{> dontSeeInField }}
1596
1635
  *
1597
1636
  */
1598
- async dontSeeInField(field, value) {
1637
+ async dontSeeInField(field, value, context = null) {
1599
1638
  const _value = typeof value === 'boolean' ? value : value.toString()
1600
- return proceedSeeField.call(this, 'negate', field, _value)
1639
+ return proceedSeeField.call(this, 'negate', field, _value, context)
1601
1640
  }
1602
1641
 
1603
1642
  /**
@@ -1621,8 +1660,9 @@ class WebDriver extends Helper {
1621
1660
  * {{ react }}
1622
1661
  *
1623
1662
  */
1624
- async seeElement(locator) {
1625
- const res = await this._locate(locator, true)
1663
+ async seeElement(locator, context = null) {
1664
+ const locateFn = prepareLocateFn.call(this, context)
1665
+ const res = context ? await locateFn(locator) : await this._locate(locator, true)
1626
1666
  assertElementExists(res, locator)
1627
1667
  const selected = await forEachAsync(res, async el => el.isDisplayed())
1628
1668
  try {
@@ -1636,8 +1676,9 @@ class WebDriver extends Helper {
1636
1676
  * {{> dontSeeElement }}
1637
1677
  * {{ react }}
1638
1678
  */
1639
- async dontSeeElement(locator) {
1640
- const res = await this._locate(locator, false)
1679
+ async dontSeeElement(locator, context = null) {
1680
+ const locateFn = prepareLocateFn.call(this, context)
1681
+ const res = context ? await locateFn(locator) : await this._locate(locator, false)
1641
1682
  if (!res || res.length === 0) {
1642
1683
  return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
1643
1684
  }
@@ -1851,7 +1892,7 @@ class WebDriver extends Helper {
1851
1892
  const currentUrl = await this.browser.getUrl()
1852
1893
  const baseUrl = this.options.url || 'http://localhost'
1853
1894
  const actualPath = new URL(currentUrl, baseUrl).pathname
1854
- return equals('url path').assert(path, actualPath)
1895
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1855
1896
  }
1856
1897
 
1857
1898
  /**
@@ -1861,7 +1902,7 @@ class WebDriver extends Helper {
1861
1902
  const currentUrl = await this.browser.getUrl()
1862
1903
  const baseUrl = this.options.url || 'http://localhost'
1863
1904
  const actualPath = new URL(currentUrl, baseUrl).pathname
1864
- return equals('url path').negate(path, actualPath)
1905
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1865
1906
  }
1866
1907
 
1867
1908
  /**
@@ -1936,8 +1977,22 @@ class WebDriver extends Helper {
1936
1977
  * {{> moveCursorTo }}
1937
1978
  */
1938
1979
  async moveCursorTo(locator, xOffset, yOffset) {
1939
- const res = await this._locate(withStrictLocator(locator), true)
1940
- assertElementExists(res, locator)
1980
+ let context = null
1981
+ if (typeof xOffset !== 'number' && xOffset !== undefined) {
1982
+ context = xOffset
1983
+ xOffset = undefined
1984
+ }
1985
+
1986
+ let res
1987
+ if (context) {
1988
+ const contextRes = await this._locate(withStrictLocator(context), true)
1989
+ assertElementExists(contextRes, context, 'Context element')
1990
+ res = await contextRes[0].$$(withStrictLocator(locator))
1991
+ assertElementExists(res, locator)
1992
+ } else {
1993
+ res = await this._locate(withStrictLocator(locator), true)
1994
+ assertElementExists(res, locator)
1995
+ }
1941
1996
  const elem = usingFirstElement(res)
1942
1997
  try {
1943
1998
  await elem.moveTo({ xOffset, yOffset })
@@ -2189,6 +2244,7 @@ class WebDriver extends Helper {
2189
2244
  * {{> pressKeyWithKeyNormalization }}
2190
2245
  */
2191
2246
  async pressKey(key) {
2247
+ await checkFocusBeforePressKey(this, key)
2192
2248
  const modifiers = []
2193
2249
  if (Array.isArray(key)) {
2194
2250
  for (let k of key) {
@@ -2235,6 +2291,8 @@ class WebDriver extends Helper {
2235
2291
  * {{> type }}
2236
2292
  */
2237
2293
  async type(keys, delay = null) {
2294
+ await checkFocusBeforeType(this)
2295
+
2238
2296
  if (!Array.isArray(keys)) {
2239
2297
  keys = keys.toString()
2240
2298
  keys = keys.split('')
@@ -2487,6 +2545,7 @@ class WebDriver extends Helper {
2487
2545
  async waitInUrl(urlPart, sec = null) {
2488
2546
  const client = this.browser
2489
2547
  const aSec = sec || this.options.waitForTimeoutInSeconds
2548
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2490
2549
  let currUrl = ''
2491
2550
 
2492
2551
  return client
@@ -2494,7 +2553,7 @@ class WebDriver extends Helper {
2494
2553
  function () {
2495
2554
  return this.getUrl().then(res => {
2496
2555
  currUrl = decodeUrl(res)
2497
- return currUrl.indexOf(urlPart) > -1
2556
+ return currUrl.indexOf(expectedUrl) > -1
2498
2557
  })
2499
2558
  },
2500
2559
  { timeout: aSec * 1000 },
@@ -2502,7 +2561,7 @@ class WebDriver extends Helper {
2502
2561
  .catch(e => {
2503
2562
  e = wrapError(e)
2504
2563
  if (e.message.indexOf('timeout')) {
2505
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2564
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2506
2565
  }
2507
2566
  throw e
2508
2567
  })
@@ -2513,22 +2572,47 @@ class WebDriver extends Helper {
2513
2572
  */
2514
2573
  async waitUrlEquals(urlPart, sec = null) {
2515
2574
  const aSec = sec || this.options.waitForTimeoutInSeconds
2516
- const baseUrl = this.options.url
2517
- if (urlPart.indexOf('http') < 0) {
2518
- urlPart = baseUrl + urlPart
2519
- }
2575
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2520
2576
  let currUrl = ''
2521
2577
  return this.browser
2522
2578
  .waitUntil(function () {
2523
2579
  return this.getUrl().then(res => {
2524
2580
  currUrl = decodeUrl(res)
2525
- return currUrl === urlPart
2581
+ return currUrl === expectedUrl
2526
2582
  })
2527
2583
  }, aSec * 1000)
2528
2584
  .catch(e => {
2529
2585
  e = wrapError(e)
2530
2586
  if (e.message.indexOf('timeout')) {
2531
- throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`)
2587
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2588
+ }
2589
+ throw e
2590
+ })
2591
+ }
2592
+
2593
+ /**
2594
+ * {{> waitCurrentPathEquals }}
2595
+ */
2596
+ async waitCurrentPathEquals(path, sec = null) {
2597
+ const aSec = sec || this.options.waitForTimeoutInSeconds
2598
+ const normalizedPath = normalizePath(path)
2599
+ const baseUrl = this.options.url || 'http://localhost'
2600
+ let actualPath = ''
2601
+
2602
+ return this.browser
2603
+ .waitUntil(
2604
+ async () => {
2605
+ const currUrl = await this.browser.getUrl()
2606
+ const url = new URL(currUrl, baseUrl)
2607
+ actualPath = url.pathname
2608
+ return normalizePath(actualPath) === normalizedPath
2609
+ },
2610
+ { timeout: aSec * 1000 },
2611
+ )
2612
+ .catch(e => {
2613
+ e = wrapError(e)
2614
+ if (e.message.indexOf('timeout')) {
2615
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2532
2616
  }
2533
2617
  throw e
2534
2618
  })
@@ -3010,32 +3094,33 @@ async function findClickable(locator, locateFn) {
3010
3094
  return await locateFn(locator.value) // by css or xpath
3011
3095
  }
3012
3096
 
3013
- async function findFields(locator) {
3097
+ async function findFields(locator, context = null) {
3098
+ const locateFn = prepareLocateFn.call(this, context)
3014
3099
  locator = new Locator(locator)
3015
3100
 
3016
3101
  if (this._isCustomLocator(locator)) {
3017
- return this._locate(locator)
3102
+ return locateFn(locator)
3018
3103
  }
3019
3104
 
3020
- if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true)
3021
- if (locator.isRole()) return this._locate(locator, true)
3022
- if (!locator.isFuzzy()) return this._locate(locator, true)
3105
+ if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator)
3106
+ if (locator.isRole()) return locateFn(locator)
3107
+ if (!locator.isFuzzy()) return locateFn(locator)
3023
3108
 
3024
3109
  const literal = xpathLocator.literal(locator.value)
3025
- let els = await this._locate(Locator.field.labelEquals(literal))
3110
+ let els = await locateFn(Locator.field.labelEquals(literal))
3026
3111
  if (els.length) return els
3027
3112
 
3028
- els = await this._locate(Locator.field.labelContains(literal))
3113
+ els = await locateFn(Locator.field.labelContains(literal))
3029
3114
  if (els.length) return els
3030
3115
 
3031
- els = await this._locate(Locator.field.byName(literal))
3116
+ els = await locateFn(Locator.field.byName(literal))
3032
3117
  if (els.length) return els
3033
3118
 
3034
- return await this._locate(locator.value) // by css or xpath
3119
+ return await locateFn(locator.value) // by css or xpath
3035
3120
  }
3036
3121
 
3037
- async function proceedSeeField(assertType, field, value) {
3038
- const res = await findFields.call(this, field)
3122
+ async function proceedSeeField(assertType, field, value, context) {
3123
+ const res = await findFields.call(this, field, context)
3039
3124
  assertElementExists(res, field, 'Field')
3040
3125
  const elem = usingFirstElement(res)
3041
3126
  const elemId = getElementId(elem)
@@ -3220,10 +3305,30 @@ function assertElementExists(res, locator, prefix, suffix) {
3220
3305
  }
3221
3306
 
3222
3307
  function usingFirstElement(els) {
3308
+ const rawIndex = store.currentStep?.opts?.elementIndex
3309
+ if (rawIndex != null && els.length > 1) {
3310
+ let elementIndex = rawIndex
3311
+ if (elementIndex === 'first') elementIndex = 1
3312
+ if (elementIndex === 'last') elementIndex = -1
3313
+ if (Number.isInteger(elementIndex) && elementIndex !== 0) {
3314
+ const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
3315
+ if (idx >= 0 && idx < els.length) {
3316
+ debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
3317
+ return els[idx]
3318
+ }
3319
+ }
3320
+ }
3223
3321
  if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
3224
3322
  return els[0]
3225
3323
  }
3226
3324
 
3325
+ function assertOnlyOneElement(elements, locator, helper) {
3326
+ if (elements.length > 1) {
3327
+ const webElements = Array.from(elements).map(el => new WebElement(el, helper))
3328
+ throw new MultipleElementsFound(locator, webElements)
3329
+ }
3330
+ }
3331
+
3227
3332
  function getElementId(el) {
3228
3333
  // W3C WebDriver web element identifier
3229
3334
  // https://w3c.github.io/webdriver/#dfn-web-element-identifier
@@ -3385,7 +3490,7 @@ function isModifierKey(key) {
3385
3490
  }
3386
3491
 
3387
3492
  function highlightActiveElement(element) {
3388
- if (this.options.highlightElement && global.debugMode) {
3493
+ if (this.options.highlightElement && store.debugMode) {
3389
3494
  highlightElement(element, this.browser)
3390
3495
  }
3391
3496
  }
@@ -1,40 +1,45 @@
1
1
  import Locator from '../../locator.js'
2
2
 
3
- /**
4
- * Error thrown when strict mode is enabled and multiple elements are found
5
- * for a single-element locator operation (click, fillField, etc.)
6
- */
7
3
  class MultipleElementsFound extends Error {
8
- /**
9
- * @param {Locator|string|object} locator - The locator used
10
- * @param {Array<HTMLElement>} elements - Array of Playwright element handles found
11
- */
12
- constructor(locator, elements) {
13
- super(`Multiple elements (${elements.length}) found for "${locator}". Call fetchDetails() for full information.`)
4
+ constructor(locator, webElements) {
5
+ const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
6
+ ? new Locator(locator).toString()
7
+ : String(locator)
8
+ super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`)
14
9
  this.name = 'MultipleElementsFound'
15
10
  this.locator = locator
16
- this.elements = elements
17
- this.count = elements.length
11
+ this.webElements = webElements
12
+ this.count = webElements.length
18
13
  this._detailsFetched = false
19
14
  }
20
15
 
21
- /**
22
- * Fetch detailed information about the found elements asynchronously
23
- * This updates the error message with XPath and element previews
24
- */
25
16
  async fetchDetails() {
26
17
  if (this._detailsFetched) return
27
18
 
28
19
  try {
29
- if (typeof this.locator === 'object' && !(this.locator instanceof Locator)) {
30
- this.locator = JSON.stringify(this.locator)
20
+ const items = []
21
+ const maxToShow = Math.min(this.count, 10)
22
+
23
+ for (let i = 0; i < maxToShow; i++) {
24
+ const webEl = this.webElements[i]
25
+ try {
26
+ const xpath = await webEl.toAbsoluteXPath()
27
+ const html = await webEl.toSimplifiedHTML()
28
+ items.push(` ${i + 1}. > ${xpath}\n ${html}`)
29
+ } catch (err) {
30
+ items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
31
+ }
31
32
  }
32
33
 
33
- const locatorObj = new Locator(this.locator)
34
- const elementList = await this._generateElementList(this.elements, this.count)
34
+ if (this.count > 10) {
35
+ items.push(` ... and ${this.count - 10} more`)
36
+ }
35
37
 
36
- this.message = `Multiple elements (${this.count}) found for "${locatorObj.toString()}" in strict mode.\n` +
37
- elementList +
38
+ const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator))
39
+ ? new Locator(this.locator).toString()
40
+ : String(this.locator)
41
+ this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` +
42
+ items.join('\n') +
38
43
  `\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
39
44
  } catch (err) {
40
45
  this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
@@ -42,94 +47,6 @@ class MultipleElementsFound extends Error {
42
47
 
43
48
  this._detailsFetched = true
44
49
  }
45
-
46
- /**
47
- * Generate a formatted list of found elements with their XPath and preview
48
- * @param {Array<HTMLElement>} elements
49
- * @param {number} count
50
- * @returns {Promise<string>}
51
- */
52
- async _generateElementList(elements, count) {
53
- const items = []
54
- const maxToShow = Math.min(count, 10)
55
-
56
- for (let i = 0; i < maxToShow; i++) {
57
- const el = elements[i]
58
- try {
59
- const info = await this._getElementInfo(el)
60
- items.push(` ${i + 1}. ${info.xpath} (${info.preview})`)
61
- } catch (err) {
62
- // Element might be detached or inaccessible
63
- items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
64
- }
65
- }
66
-
67
- if (count > 10) {
68
- items.push(` ... and ${count - 10} more`)
69
- }
70
-
71
- return items.join('\n')
72
- }
73
-
74
- /**
75
- * Get XPath and preview for an element by running JavaScript in browser context
76
- * @param {HTMLElement} element
77
- * @returns {Promise<{xpath: string, preview: string}>}
78
- */
79
- async _getElementInfo(element) {
80
- return element.evaluate((el) => {
81
- // Generate a unique XPath for this element
82
- const getUniqueXPath = (element) => {
83
- if (element.id) {
84
- return `//*[@id="${element.id}"]`
85
- }
86
-
87
- const parts = []
88
- let current = element
89
-
90
- while (current && current.nodeType === Node.ELEMENT_NODE) {
91
- let index = 0
92
- let sibling = current.previousSibling
93
-
94
- while (sibling) {
95
- if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
96
- index++
97
- }
98
- sibling = sibling.previousSibling
99
- }
100
-
101
- const tagName = current.tagName.toLowerCase()
102
- const pathIndex = index > 0 ? `[${index + 1}]` : ''
103
- parts.unshift(`${tagName}${pathIndex}`)
104
-
105
- current = current.parentElement
106
-
107
- // Stop at body to keep XPath reasonable
108
- if (current && current.tagName === 'BODY') {
109
- parts.unshift('body')
110
- break
111
- }
112
- }
113
-
114
- return '/' + parts.join('/')
115
- }
116
-
117
- // Get a preview of the element (tag, classes, id)
118
- const getPreview = (element) => {
119
- const tag = element.tagName.toLowerCase()
120
- const id = element.id ? `#${element.id}` : ''
121
- const classes = element.className
122
- ? '.' + element.className.split(' ').filter(c => c).join('.')
123
- : ''
124
- return `${tag}${id}${classes || ''}`
125
- }
126
-
127
- return {
128
- xpath: getUniqueXPath(el),
129
- preview: getPreview(el),
130
- }
131
- })
132
- }
133
50
  }
134
51
 
135
52
  export default MultipleElementsFound
@@ -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