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
@@ -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'
@@ -26,17 +27,25 @@ import {
26
27
  isModifierKey,
27
28
  requireWithFallback,
28
29
  normalizeSpacesInString,
30
+ normalizePath,
31
+ resolveUrl,
32
+ getMimeType,
33
+ base64EncodeFile,
29
34
  } from '../utils.js'
30
35
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
31
36
  import ElementNotFound from './errors/ElementNotFound.js'
37
+ import MultipleElementsFound from './errors/MultipleElementsFound.js'
32
38
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
33
39
  import Popup from './extras/Popup.js'
34
40
  import Console from './extras/Console.js'
35
41
  import { highlightElement } from './scripts/highlightElement.js'
36
42
  import { blurElement } from './scripts/blurElement.js'
43
+ import { dropFile } from './scripts/dropFile.js'
37
44
  import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
38
45
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
39
46
  import WebElement from '../element/WebElement.js'
47
+ import { selectElement } from './extras/elementSelection.js'
48
+ import { fillRichEditor } from './extras/richTextEditor.js'
40
49
 
41
50
  let puppeteer
42
51
 
@@ -266,6 +275,7 @@ class Puppeteer extends Helper {
266
275
  show: false,
267
276
  defaultPopupAction: 'accept',
268
277
  highlightElement: false,
278
+ strict: false,
269
279
  }
270
280
 
271
281
  return Object.assign(defaults, config)
@@ -743,7 +753,7 @@ class Puppeteer extends Helper {
743
753
  }
744
754
 
745
755
  if (this.options.trace) {
746
- 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`
747
757
  const dir = path.dirname(fileName)
748
758
  if (!fileExists(dir)) fs.mkdirSync(dir)
749
759
  await this.page.tracing.start({ screenshots: true, path: fileName })
@@ -814,9 +824,26 @@ class Puppeteer extends Helper {
814
824
  * {{ react }}
815
825
  */
816
826
  async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
817
- const el = await this._locateElement(locator)
818
- if (!el) {
819
- throw new ElementNotFound(locator, 'Element to move cursor to')
827
+ let context = null
828
+ if (typeof offsetX !== 'number') {
829
+ context = offsetX
830
+ offsetX = 0
831
+ }
832
+
833
+ let el
834
+ if (context) {
835
+ const contextEls = await findElements.call(this, this.page, context)
836
+ assertElementExists(contextEls, context, 'Context element')
837
+ const els = await findElements.call(this, contextEls[0], locator)
838
+ if (!els || els.length === 0) {
839
+ throw new ElementNotFound(locator, 'Element to move cursor to')
840
+ }
841
+ el = els[0]
842
+ } else {
843
+ el = await this._locateElement(locator)
844
+ if (!el) {
845
+ throw new ElementNotFound(locator, 'Element to move cursor to')
846
+ }
820
847
  }
821
848
 
822
849
  // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
@@ -984,6 +1011,14 @@ class Puppeteer extends Helper {
984
1011
  */
985
1012
  async _locateElement(locator) {
986
1013
  const context = await this.context
1014
+ const elementIndex = store.currentStep?.opts?.elementIndex
1015
+ if (this.options.strict || elementIndex) {
1016
+ const elements = await findElements.call(this, context, locator)
1017
+ if (elements.length === 0) {
1018
+ throw new ElementNotFound(locator, 'Element', 'was not found')
1019
+ }
1020
+ return selectElement(elements, locator, this)
1021
+ }
987
1022
  return findElement.call(this, context, locator)
988
1023
  }
989
1024
 
@@ -1001,7 +1036,7 @@ class Puppeteer extends Helper {
1001
1036
  if (!els || els.length === 0) {
1002
1037
  throw new ElementNotFound(locator, 'Checkbox or radio')
1003
1038
  }
1004
- return els[0]
1039
+ return selectElement(els, locator, this)
1005
1040
  }
1006
1041
 
1007
1042
  /**
@@ -1158,8 +1193,16 @@ class Puppeteer extends Helper {
1158
1193
  * {{> seeElement }}
1159
1194
  * {{ react }}
1160
1195
  */
1161
- async seeElement(locator) {
1162
- let els = await this._locate(locator)
1196
+ async seeElement(locator, context = null) {
1197
+ let els
1198
+ if (context) {
1199
+ const contextPage = await this.context
1200
+ const contextEls = await findElements.call(this, contextPage, context)
1201
+ assertElementExists(contextEls, context, 'Context element')
1202
+ els = await findElements.call(this, contextEls[0], locator)
1203
+ } else {
1204
+ els = await this._locate(locator)
1205
+ }
1163
1206
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1164
1207
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1165
1208
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1174,8 +1217,16 @@ class Puppeteer extends Helper {
1174
1217
  * {{> dontSeeElement }}
1175
1218
  * {{ react }}
1176
1219
  */
1177
- async dontSeeElement(locator) {
1178
- let els = await this._locate(locator)
1220
+ async dontSeeElement(locator, context = null) {
1221
+ let els
1222
+ if (context) {
1223
+ const contextPage = await this.context
1224
+ const contextEls = await findElements.call(this, contextPage, context)
1225
+ assertElementExists(contextEls, context, 'Context element')
1226
+ els = await findElements.call(this, contextEls[0], locator)
1227
+ } else {
1228
+ els = await this._locate(locator)
1229
+ }
1179
1230
  els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v)
1180
1231
  // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed
1181
1232
  els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el))
@@ -1276,7 +1327,7 @@ class Puppeteer extends Helper {
1276
1327
  * @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving
1277
1328
  */
1278
1329
  async handleDownloads(downloadPath = 'downloads') {
1279
- downloadPath = path.join(global.output_dir, downloadPath)
1330
+ downloadPath = path.join(store.outputDir, downloadPath)
1280
1331
  if (!fs.existsSync(downloadPath)) {
1281
1332
  fs.mkdirSync(downloadPath, '0777')
1282
1333
  }
@@ -1338,7 +1389,7 @@ class Puppeteer extends Helper {
1338
1389
  },
1339
1390
  })
1340
1391
 
1341
- const outputFile = path.join(`${global.output_dir}/${fileName}`)
1392
+ const outputFile = path.join(`${store.outputDir}/${fileName}`)
1342
1393
 
1343
1394
  try {
1344
1395
  await new Promise((resolve, reject) => {
@@ -1498,6 +1549,7 @@ class Puppeteer extends Helper {
1498
1549
  * {{> pressKeyWithKeyNormalization }}
1499
1550
  */
1500
1551
  async pressKey(key) {
1552
+ await checkFocusBeforePressKey(this, key)
1501
1553
  const modifiers = []
1502
1554
  if (Array.isArray(key)) {
1503
1555
  for (let k of key) {
@@ -1526,6 +1578,8 @@ class Puppeteer extends Helper {
1526
1578
  * {{> type }}
1527
1579
  */
1528
1580
  async type(keys, delay = null) {
1581
+ await checkFocusBeforeType(this)
1582
+
1529
1583
  if (!Array.isArray(keys)) {
1530
1584
  keys = keys.toString()
1531
1585
  keys = keys.split('')
@@ -1541,10 +1595,19 @@ class Puppeteer extends Helper {
1541
1595
  * {{> fillField }}
1542
1596
  * {{ react }}
1543
1597
  */
1544
- async fillField(field, value) {
1545
- const els = await findVisibleFields.call(this, field)
1598
+ async fillField(field, value, context = null) {
1599
+ let els = await findVisibleFields.call(this, field, context)
1600
+ if (!els.length) {
1601
+ els = await findFields.call(this, field, context)
1602
+ }
1546
1603
  assertElementExists(els, field, 'Field')
1547
- const el = els[0]
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
+
1548
1611
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
1549
1612
  const editable = await el.getProperty('contenteditable').then(el => el.jsonValue())
1550
1613
  if (tag === 'INPUT' || tag === 'TEXTAREA') {
@@ -1562,8 +1625,8 @@ class Puppeteer extends Helper {
1562
1625
  /**
1563
1626
  * {{> clearField }}
1564
1627
  */
1565
- async clearField(field) {
1566
- return this.fillField(field, '')
1628
+ async clearField(field, context = null) {
1629
+ return this.fillField(field, '', context)
1567
1630
  }
1568
1631
 
1569
1632
  /**
@@ -1571,29 +1634,30 @@ class Puppeteer extends Helper {
1571
1634
  *
1572
1635
  * {{ react }}
1573
1636
  */
1574
- async appendField(field, value) {
1575
- const els = await findVisibleFields.call(this, field)
1637
+ async appendField(field, value, context = null) {
1638
+ const els = await findVisibleFields.call(this, field, context)
1576
1639
  assertElementExists(els, field, 'Field')
1577
- highlightActiveElement.call(this, els[0], await this._getContext())
1578
- await els[0].press('End')
1579
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
1640
+ const el = selectElement(els, field, this)
1641
+ highlightActiveElement.call(this, el, await this._getContext())
1642
+ await el.press('End')
1643
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
1580
1644
  return this._waitForAction()
1581
1645
  }
1582
1646
 
1583
1647
  /**
1584
1648
  * {{> seeInField }}
1585
1649
  */
1586
- async seeInField(field, value) {
1650
+ async seeInField(field, value, context = null) {
1587
1651
  const _value = typeof value === 'boolean' ? value : value.toString()
1588
- return proceedSeeInField.call(this, 'assert', field, _value)
1652
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
1589
1653
  }
1590
1654
 
1591
1655
  /**
1592
1656
  * {{> dontSeeInField }}
1593
1657
  */
1594
- async dontSeeInField(field, value) {
1658
+ async dontSeeInField(field, value, context = null) {
1595
1659
  const _value = typeof value === 'boolean' ? value : value.toString()
1596
- return proceedSeeInField.call(this, 'negate', field, _value)
1660
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
1597
1661
  }
1598
1662
 
1599
1663
  /**
@@ -1601,46 +1665,71 @@ class Puppeteer extends Helper {
1601
1665
  *
1602
1666
  * {{> attachFile }}
1603
1667
  */
1604
- async attachFile(locator, pathToFile) {
1605
- const file = path.join(global.codecept_dir, pathToFile)
1668
+ async attachFile(locator, pathToFile, context = null) {
1669
+ const file = path.join(store.codeceptDir, pathToFile)
1606
1670
 
1607
1671
  if (!fileExists(file)) {
1608
1672
  throw new Error(`File at ${file} can not be found on local system`)
1609
1673
  }
1610
- const els = await findFields.call(this, locator)
1611
- assertElementExists(els, locator, 'Field')
1612
- await els[0].uploadFile(file)
1674
+ const els = await findFields.call(this, locator, context)
1675
+ if (els.length) {
1676
+ const el = selectElement(els, locator, this)
1677
+ const tag = await el.evaluate(el => el.tagName)
1678
+ const type = await el.evaluate(el => el.type)
1679
+ if (tag === 'INPUT' && type === 'file') {
1680
+ await el.uploadFile(file)
1681
+ return this._waitForAction()
1682
+ }
1683
+ }
1684
+
1685
+ const targetEls = els.length ? els : await this._locate(locator)
1686
+ assertElementExists(targetEls, locator, 'Element')
1687
+ const el = selectElement(targetEls, locator, this)
1688
+ const fileData = {
1689
+ base64Content: base64EncodeFile(file),
1690
+ fileName: path.basename(file),
1691
+ mimeType: getMimeType(path.basename(file)),
1692
+ }
1693
+ await el.evaluate(dropFile, fileData)
1613
1694
  return this._waitForAction()
1614
1695
  }
1615
1696
 
1616
1697
  /**
1617
1698
  * {{> selectOption }}
1618
1699
  */
1619
- async selectOption(select, option) {
1620
- const context = await this._getContext()
1700
+ async selectOption(select, option, context = null) {
1701
+ const pageContext = await this._getContext()
1621
1702
  const matchedLocator = new Locator(select)
1622
1703
 
1704
+ let contextEl
1705
+ if (context) {
1706
+ const contextEls = await findElements.call(this, pageContext, context)
1707
+ assertElementExists(contextEls, context, 'Context element')
1708
+ contextEl = contextEls[0]
1709
+ }
1710
+
1623
1711
  // Strict locator
1624
1712
  if (!matchedLocator.isFuzzy()) {
1625
1713
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
1626
- const els = await this._locate(select)
1714
+ const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select)
1627
1715
  assertElementExists(els, select, 'Selectable element')
1628
- return proceedSelect.call(this, context, els[0], option)
1716
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1629
1717
  }
1630
1718
 
1631
1719
  // Fuzzy: try combobox
1632
1720
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
1633
- let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
1634
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1721
+ const comboboxSearchCtx = contextEl || pageContext
1722
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
1723
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1635
1724
 
1636
1725
  // Fuzzy: try listbox
1637
- els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
1638
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
1726
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
1727
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
1639
1728
 
1640
1729
  // Fuzzy: try native select
1641
- const visibleEls = await findVisibleFields.call(this, select)
1730
+ const visibleEls = await findVisibleFields.call(this, select, context)
1642
1731
  assertElementExists(visibleEls, select, 'Selectable field')
1643
- return proceedSelect.call(this, context, visibleEls[0], option)
1732
+ return proceedSelect.call(this, pageContext, selectElement(visibleEls, select, this), option)
1644
1733
  }
1645
1734
 
1646
1735
  /**
@@ -1691,7 +1780,7 @@ class Puppeteer extends Helper {
1691
1780
  const currentUrl = await this._getPageUrl()
1692
1781
  const baseUrl = this.options.url || 'http://localhost'
1693
1782
  const actualPath = new URL(currentUrl, baseUrl).pathname
1694
- return equals('url path').assert(path, actualPath)
1783
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
1695
1784
  }
1696
1785
 
1697
1786
  /**
@@ -1701,7 +1790,7 @@ class Puppeteer extends Helper {
1701
1790
  const currentUrl = await this._getPageUrl()
1702
1791
  const baseUrl = this.options.url || 'http://localhost'
1703
1792
  const actualPath = new URL(currentUrl, baseUrl).pathname
1704
- return equals('url path').negate(path, actualPath)
1793
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
1705
1794
  }
1706
1795
 
1707
1796
  /**
@@ -2441,6 +2530,7 @@ class Puppeteer extends Helper {
2441
2530
  */
2442
2531
  async waitInUrl(urlPart, sec = null) {
2443
2532
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2533
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2444
2534
 
2445
2535
  return this.page
2446
2536
  .waitForFunction(
@@ -2449,12 +2539,12 @@ class Puppeteer extends Helper {
2449
2539
  return currUrl.indexOf(urlPart) > -1
2450
2540
  },
2451
2541
  { timeout: waitTimeout },
2452
- urlPart,
2542
+ expectedUrl,
2453
2543
  )
2454
2544
  .catch(async e => {
2455
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
2545
+ const currUrl = await this._getPageUrl()
2456
2546
  if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2457
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
2547
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
2458
2548
  } else {
2459
2549
  throw e
2460
2550
  }
@@ -2466,18 +2556,13 @@ class Puppeteer extends Helper {
2466
2556
  */
2467
2557
  async waitUrlEquals(urlPart, sec = null) {
2468
2558
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2469
-
2470
- const baseUrl = this.options.url
2471
- let expectedUrl = urlPart
2472
- if (urlPart.indexOf('http') < 0) {
2473
- expectedUrl = baseUrl + urlPart
2474
- }
2559
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
2475
2560
 
2476
2561
  return this.page
2477
2562
  .waitForFunction(
2478
2563
  url => {
2479
2564
  const currUrl = decodeURIComponent(window.location.href)
2480
- return currUrl.indexOf(url) > -1
2565
+ return currUrl === url
2481
2566
  },
2482
2567
  { timeout: waitTimeout },
2483
2568
  expectedUrl,
@@ -2485,11 +2570,36 @@ class Puppeteer extends Helper {
2485
2570
  .catch(async e => {
2486
2571
  const currUrl = await this._getPageUrl()
2487
2572
  if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2488
- if (!currUrl.includes(expectedUrl)) {
2489
- throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2490
- } else {
2491
- throw new Error(`expected url not loaded, error message: ${e.message}`)
2492
- }
2573
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
2574
+ } else {
2575
+ throw e
2576
+ }
2577
+ })
2578
+ }
2579
+
2580
+ /**
2581
+ * {{> waitCurrentPathEquals }}
2582
+ */
2583
+ async waitCurrentPathEquals(path, sec = null) {
2584
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2585
+ const normalizedPath = normalizePath(path)
2586
+
2587
+ return this.page
2588
+ .waitForFunction(
2589
+ expectedPath => {
2590
+ const actualPath = window.location.pathname
2591
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
2592
+ return normalizePath(actualPath) === expectedPath
2593
+ },
2594
+ { timeout: waitTimeout },
2595
+ normalizedPath,
2596
+ )
2597
+ .catch(async e => {
2598
+ const currUrl = await this._getPageUrl()
2599
+ const baseUrl = this.options.url || 'http://localhost'
2600
+ const actualPath = new URL(currUrl, baseUrl).pathname
2601
+ if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
2602
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
2493
2603
  } else {
2494
2604
  throw e
2495
2605
  }
@@ -3026,10 +3136,11 @@ async function proceedClick(locator, context = null, options = {}) {
3026
3136
  } else {
3027
3137
  assertElementExists(els, locator, 'Clickable element')
3028
3138
  }
3139
+ const el = selectElement(els, locator, this)
3029
3140
 
3030
- highlightActiveElement.call(this, els[0], await this._getContext())
3141
+ highlightActiveElement.call(this, el, await this._getContext())
3031
3142
 
3032
- await els[0].click(options)
3143
+ await el.click(options)
3033
3144
  const promises = []
3034
3145
  if (options.waitForNavigation) {
3035
3146
  promises.push(this.waitForNavigation())
@@ -3160,43 +3271,57 @@ async function proceedIsChecked(assertType, option) {
3160
3271
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
3161
3272
  }
3162
3273
 
3163
- async function findVisibleFields(locator) {
3164
- const els = await findFields.call(this, locator)
3274
+ async function findVisibleFields(locator, context = null) {
3275
+ const els = await findFields.call(this, locator, context)
3165
3276
  const visible = await Promise.all(els.map(el => el.boundingBox()))
3166
3277
  return els.filter((el, index) => visible[index])
3167
3278
  }
3168
3279
 
3169
- async function findFields(locator) {
3280
+ async function findFields(locator, context = null) {
3281
+ let contextEl
3282
+ if (context) {
3283
+ const contextPage = await this.context
3284
+ const contextEls = await findElements.call(this, contextPage, context)
3285
+ assertElementExists(contextEls, context, 'Context element')
3286
+ contextEl = contextEls[0]
3287
+ }
3288
+
3289
+ const locateFn = contextEl
3290
+ ? loc => findElements.call(this, contextEl, loc)
3291
+ : loc => this._locate(loc)
3292
+
3170
3293
  const matchedLocator = new Locator(locator)
3171
3294
  if (!matchedLocator.isFuzzy()) {
3172
- return this._locate(matchedLocator)
3295
+ return locateFn(matchedLocator)
3173
3296
  }
3174
3297
  const literal = xpathLocator.literal(matchedLocator.value)
3175
3298
 
3176
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
3299
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
3177
3300
  if (els.length) {
3178
3301
  return els
3179
3302
  }
3180
3303
 
3181
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
3304
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
3182
3305
  if (els.length) {
3183
3306
  return els
3184
3307
  }
3185
- els = await this._locate({ xpath: Locator.field.byName(literal) })
3308
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
3186
3309
  if (els.length) {
3187
3310
  return els
3188
3311
  }
3189
3312
 
3190
3313
  // Try ARIA selector for accessible name
3191
- try {
3192
- const page = await this.context
3193
- els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3194
- if (els.length) return els
3195
- } catch (err) {
3196
- // ARIA selector not supported or failed
3314
+ if (!contextEl) {
3315
+ try {
3316
+ const page = await this.context
3317
+ els = await page.$$(`::-p-aria(${matchedLocator.value})`)
3318
+ if (els.length) return els
3319
+ } catch (err) {
3320
+ // ARIA selector not supported or failed
3321
+ }
3197
3322
  }
3198
3323
 
3199
- return this._locate({ css: matchedLocator.value })
3324
+ return locateFn({ css: matchedLocator.value })
3200
3325
  }
3201
3326
 
3202
3327
  async function proceedDragAndDrop(sourceLocator, destinationLocator) {
@@ -3225,8 +3350,8 @@ async function proceedDragAndDrop(sourceLocator, destinationLocator) {
3225
3350
  await this._waitForAction()
3226
3351
  }
3227
3352
 
3228
- async function proceedSeeInField(assertType, field, value) {
3229
- const els = await findVisibleFields.call(this, field)
3353
+ async function proceedSeeInField(assertType, field, value, context) {
3354
+ const els = await findVisibleFields.call(this, field, context)
3230
3355
  assertElementExists(els, field, 'Field')
3231
3356
  const el = els[0]
3232
3357
  const tag = await el.getProperty('tagName').then(el => el.jsonValue())
@@ -3339,6 +3464,13 @@ function assertElementExists(res, locator, prefix, suffix) {
3339
3464
  }
3340
3465
  }
3341
3466
 
3467
+ function assertOnlyOneElement(elements, locator, helper) {
3468
+ if (elements.length > 1) {
3469
+ const webElements = elements.map(el => new WebElement(el, helper))
3470
+ throw new MultipleElementsFound(locator, webElements)
3471
+ }
3472
+ }
3473
+
3342
3474
  function $XPath(element, selector) {
3343
3475
  const found = document.evaluate(selector, element || document.body, null, 5, null)
3344
3476
  const res = []
@@ -3446,7 +3578,7 @@ function getNormalizedKey(key) {
3446
3578
  }
3447
3579
 
3448
3580
  function highlightActiveElement(element, context) {
3449
- if (this.options.highlightElement && global.debugMode) {
3581
+ if (this.options.highlightElement && store.debugMode) {
3450
3582
  highlightElement(element, context)
3451
3583
  }
3452
3584
  }