codeceptjs 4.0.0-rc.8 → 4.0.0

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 (314) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +751 -172
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +743 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +198 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +7 -7
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/dryRun.js +24 -5
  105. package/lib/command/generate.js +2 -0
  106. package/lib/command/gherkin/snippets.js +5 -4
  107. package/lib/command/init.js +248 -266
  108. package/lib/command/list.js +150 -10
  109. package/lib/command/query.js +218 -0
  110. package/lib/command/run-multiple.js +3 -2
  111. package/lib/command/run-workers.js +1 -14
  112. package/lib/command/run.js +3 -17
  113. package/lib/command/utils.js +14 -0
  114. package/lib/command/workers/runTests.js +11 -15
  115. package/lib/config.js +77 -4
  116. package/lib/container.js +97 -15
  117. package/lib/effects.js +17 -0
  118. package/lib/element/WebElement.js +195 -3
  119. package/lib/els.js +12 -6
  120. package/lib/globals.js +32 -19
  121. package/lib/heal.js +7 -4
  122. package/lib/helper/ApiDataFactory.js +2 -1
  123. package/lib/helper/FileSystem.js +3 -2
  124. package/lib/helper/GraphQLDataFactory.js +2 -1
  125. package/lib/helper/Playwright.js +96 -115
  126. package/lib/helper/Puppeteer.js +43 -131
  127. package/lib/helper/WebDriver.js +42 -52
  128. package/lib/helper/errors/NonFocusedType.js +8 -0
  129. package/lib/helper/extras/Download.js +45 -0
  130. package/lib/helper/extras/PlaywrightLocator.js +10 -0
  131. package/lib/helper/extras/elementSelection.js +58 -0
  132. package/lib/helper/extras/focusCheck.js +43 -0
  133. package/lib/helper/extras/richTextEditor.js +178 -0
  134. package/lib/history.js +3 -2
  135. package/lib/html.js +90 -16
  136. package/lib/index.js +9 -1
  137. package/lib/listener/config.js +6 -4
  138. package/lib/listener/emptyRun.js +2 -1
  139. package/lib/listener/helpers.js +4 -1
  140. package/lib/listener/mocha.js +2 -1
  141. package/lib/listener/pageobjects.js +43 -0
  142. package/lib/listener/result.js +3 -2
  143. package/lib/locator.js +126 -16
  144. package/lib/mocha/cli.js +4 -2
  145. package/lib/mocha/factory.js +7 -2
  146. package/lib/mocha/inject.js +1 -1
  147. package/lib/mocha/scenarioConfig.js +2 -1
  148. package/lib/mocha/ui.js +5 -6
  149. package/lib/parser.js +2 -2
  150. package/lib/pause.js +38 -4
  151. package/lib/plugin/aiTrace.js +96 -103
  152. package/lib/plugin/analyze.js +9 -9
  153. package/lib/plugin/auth.js +3 -3
  154. package/lib/plugin/browser.js +77 -0
  155. package/lib/plugin/expose.js +159 -0
  156. package/lib/plugin/heal.js +47 -3
  157. package/lib/plugin/junitReporter.js +303 -0
  158. package/lib/plugin/pageInfo.js +54 -52
  159. package/lib/plugin/pause.js +131 -0
  160. package/lib/plugin/pauseOnFail.js +11 -33
  161. package/lib/plugin/retryFailedStep.js +15 -13
  162. package/lib/plugin/screencast.js +289 -0
  163. package/lib/plugin/screenshot.js +558 -0
  164. package/lib/plugin/screenshotOnFail.js +9 -170
  165. package/lib/plugin/stepTimeout.js +3 -2
  166. package/lib/recorder.js +1 -1
  167. package/lib/rerun.js +2 -1
  168. package/lib/result.js +2 -1
  169. package/lib/step/base.js +10 -9
  170. package/lib/step/comment.js +2 -2
  171. package/lib/step/config.js +15 -2
  172. package/lib/step/helper.js +4 -4
  173. package/lib/step/meta.js +3 -3
  174. package/lib/step/record.js +5 -5
  175. package/lib/store.js +72 -3
  176. package/lib/translation.js +2 -1
  177. package/lib/utils/mask_data.js +2 -1
  178. package/lib/utils/pluginParser.js +151 -0
  179. package/lib/utils/trace.js +297 -0
  180. package/lib/utils.js +29 -3
  181. package/lib/workers.js +14 -22
  182. package/package.json +17 -14
  183. package/typings/index.d.ts +19 -5
  184. package/docs/webapi/amOnPage.mustache +0 -11
  185. package/docs/webapi/appendField.mustache +0 -16
  186. package/docs/webapi/attachFile.mustache +0 -24
  187. package/docs/webapi/blur.mustache +0 -18
  188. package/docs/webapi/checkOption.mustache +0 -13
  189. package/docs/webapi/clearCookie.mustache +0 -9
  190. package/docs/webapi/clearField.mustache +0 -14
  191. package/docs/webapi/click.mustache +0 -29
  192. package/docs/webapi/clickLink.mustache +0 -8
  193. package/docs/webapi/closeCurrentTab.mustache +0 -7
  194. package/docs/webapi/closeOtherTabs.mustache +0 -8
  195. package/docs/webapi/dontSee.mustache +0 -11
  196. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  197. package/docs/webapi/dontSeeCookie.mustache +0 -8
  198. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  199. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  200. package/docs/webapi/dontSeeElement.mustache +0 -12
  201. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  202. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  203. package/docs/webapi/dontSeeInField.mustache +0 -16
  204. package/docs/webapi/dontSeeInSource.mustache +0 -8
  205. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  206. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  207. package/docs/webapi/doubleClick.mustache +0 -13
  208. package/docs/webapi/downloadFile.mustache +0 -12
  209. package/docs/webapi/dragAndDrop.mustache +0 -9
  210. package/docs/webapi/dragSlider.mustache +0 -11
  211. package/docs/webapi/executeAsyncScript.mustache +0 -24
  212. package/docs/webapi/executeScript.mustache +0 -26
  213. package/docs/webapi/fillField.mustache +0 -21
  214. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  215. package/docs/webapi/focus.mustache +0 -13
  216. package/docs/webapi/forceClick.mustache +0 -28
  217. package/docs/webapi/forceRightClick.mustache +0 -18
  218. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  219. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  220. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  221. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  222. package/docs/webapi/grabCookie.mustache +0 -11
  223. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  224. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  225. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  226. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  227. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  228. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  229. package/docs/webapi/grabGeoLocation.mustache +0 -8
  230. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  231. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  232. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  233. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  234. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  235. package/docs/webapi/grabPopupText.mustache +0 -5
  236. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  237. package/docs/webapi/grabSource.mustache +0 -8
  238. package/docs/webapi/grabTextFrom.mustache +0 -10
  239. package/docs/webapi/grabTextFromAll.mustache +0 -9
  240. package/docs/webapi/grabTitle.mustache +0 -8
  241. package/docs/webapi/grabValueFrom.mustache +0 -9
  242. package/docs/webapi/grabValueFromAll.mustache +0 -8
  243. package/docs/webapi/grabWebElement.mustache +0 -9
  244. package/docs/webapi/grabWebElements.mustache +0 -9
  245. package/docs/webapi/moveCursorTo.mustache +0 -16
  246. package/docs/webapi/openNewTab.mustache +0 -7
  247. package/docs/webapi/pressKey.mustache +0 -12
  248. package/docs/webapi/pressKeyDown.mustache +0 -12
  249. package/docs/webapi/pressKeyUp.mustache +0 -12
  250. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  251. package/docs/webapi/refreshPage.mustache +0 -6
  252. package/docs/webapi/resizeWindow.mustache +0 -6
  253. package/docs/webapi/rightClick.mustache +0 -14
  254. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  255. package/docs/webapi/saveScreenshot.mustache +0 -12
  256. package/docs/webapi/say.mustache +0 -10
  257. package/docs/webapi/scrollIntoView.mustache +0 -11
  258. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  259. package/docs/webapi/scrollPageToTop.mustache +0 -6
  260. package/docs/webapi/scrollTo.mustache +0 -12
  261. package/docs/webapi/see.mustache +0 -11
  262. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  263. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  264. package/docs/webapi/seeCookie.mustache +0 -8
  265. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  266. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  267. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  268. package/docs/webapi/seeElement.mustache +0 -12
  269. package/docs/webapi/seeElementInDOM.mustache +0 -8
  270. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  271. package/docs/webapi/seeInField.mustache +0 -17
  272. package/docs/webapi/seeInPopup.mustache +0 -8
  273. package/docs/webapi/seeInSource.mustache +0 -7
  274. package/docs/webapi/seeInTitle.mustache +0 -8
  275. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  276. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  277. package/docs/webapi/seeTextEquals.mustache +0 -9
  278. package/docs/webapi/seeTitleEquals.mustache +0 -8
  279. package/docs/webapi/seeTraffic.mustache +0 -36
  280. package/docs/webapi/selectOption.mustache +0 -26
  281. package/docs/webapi/setCookie.mustache +0 -16
  282. package/docs/webapi/setGeoLocation.mustache +0 -12
  283. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  284. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  285. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  286. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  287. package/docs/webapi/switchTo.mustache +0 -9
  288. package/docs/webapi/switchToNextTab.mustache +0 -10
  289. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  290. package/docs/webapi/type.mustache +0 -21
  291. package/docs/webapi/uncheckOption.mustache +0 -13
  292. package/docs/webapi/wait.mustache +0 -8
  293. package/docs/webapi/waitForClickable.mustache +0 -11
  294. package/docs/webapi/waitForCookie.mustache +0 -9
  295. package/docs/webapi/waitForDetached.mustache +0 -10
  296. package/docs/webapi/waitForDisabled.mustache +0 -6
  297. package/docs/webapi/waitForElement.mustache +0 -11
  298. package/docs/webapi/waitForEnabled.mustache +0 -6
  299. package/docs/webapi/waitForFunction.mustache +0 -17
  300. package/docs/webapi/waitForInvisible.mustache +0 -10
  301. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  302. package/docs/webapi/waitForText.mustache +0 -13
  303. package/docs/webapi/waitForValue.mustache +0 -10
  304. package/docs/webapi/waitForVisible.mustache +0 -10
  305. package/docs/webapi/waitInUrl.mustache +0 -9
  306. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  307. package/docs/webapi/waitToHide.mustache +0 -10
  308. package/docs/webapi/waitUrlEquals.mustache +0 -10
  309. package/lib/helper/AI.js +0 -214
  310. package/lib/helper/Mochawesome.js +0 -96
  311. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  312. package/lib/helper/extras/React.js +0 -65
  313. package/lib/plugin/stepByStepReport.js +0 -431
  314. package/lib/plugin/subtitles.js +0 -89
@@ -7,6 +7,7 @@ import promiseRetry from 'promise-retry'
7
7
  import Locator from '../locator.js'
8
8
  import recorder from '../recorder.js'
9
9
  import store from '../store.js'
10
+ import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
10
11
  import { includes as stringIncludes } from '../assert/include.js'
11
12
  import { urlEquals, equals } from '../assert/equal.js'
12
13
  import { empty } from '../assert/empty.js'
@@ -35,18 +36,16 @@ import MultipleElementsFound from './errors/MultipleElementsFound.js'
35
36
  import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
36
37
  import Popup from './extras/Popup.js'
37
38
  import Console from './extras/Console.js'
38
- import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
39
+ import { findByPlaywrightLocator } from './extras/PlaywrightLocator.js'
39
40
  import { dropFile } from './scripts/dropFile.js'
40
41
  import WebElement from '../element/WebElement.js'
42
+ import { selectElement } from './extras/elementSelection.js'
43
+ import { fillRichEditor } from './extras/richTextEditor.js'
41
44
 
42
45
  let playwright
43
46
  let perfTiming
44
47
  let defaultSelectorEnginesInitialized = false
45
48
 
46
- // Use global object to track selector registration across workers
47
- if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
48
- global.__playwrightSelectorsRegistered = false
49
- }
50
49
 
51
50
  const popupStore = new Popup()
52
51
  const consoleLogStore = new Console()
@@ -447,7 +446,7 @@ class Playwright extends Helper {
447
446
  this.options.recordVideo = { size }
448
447
  }
449
448
  if (this.options.recordVideo && !this.options.recordVideo.dir) {
450
- this.options.recordVideo.dir = `${global.output_dir}/videos/`
449
+ this.options.recordVideo.dir = `${store.outputDir}/videos/`
451
450
  }
452
451
  this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
453
452
  this.isElectron = this.options.browser === 'electron'
@@ -509,18 +508,18 @@ class Playwright extends Helper {
509
508
  try {
510
509
  // Always wrap in try-catch since selectors might be registered globally across workers
511
510
  // Check global flag to avoid re-registration in worker processes
512
- if (!global.__playwrightSelectorsRegistered) {
511
+ if (!defaultSelectorEnginesInitialized) {
513
512
  try {
514
513
  await playwright.selectors.register('__value', createValueEngine)
515
514
  await playwright.selectors.register('__disabled', createDisabledEngine)
516
- global.__playwrightSelectorsRegistered = true
515
+ defaultSelectorEnginesInitialized = true
517
516
  defaultSelectorEnginesInitialized = true
518
517
  } catch (e) {
519
518
  if (!e.message.includes('already registered')) {
520
519
  throw e
521
520
  }
522
521
  // Selector already registered globally by another worker
523
- global.__playwrightSelectorsRegistered = true
522
+ defaultSelectorEnginesInitialized = true
524
523
  defaultSelectorEnginesInitialized = true
525
524
  }
526
525
  } else {
@@ -613,7 +612,7 @@ class Playwright extends Helper {
613
612
  if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
614
613
  if (this.options.recordHar) {
615
614
  const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
616
- const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
615
+ const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
617
616
  const dir = path.dirname(fileName)
618
617
  if (!fileExists(dir)) fs.mkdirSync(dir)
619
618
  this.options.recordHar.path = fileName
@@ -756,6 +755,11 @@ class Playwright extends Helper {
756
755
  }
757
756
 
758
757
  async _afterSuite() {
758
+ // Reset leftover test-level cleanup state (e.g. screenshot failures)
759
+ // so only errors from this suite teardown are evaluated below.
760
+ this.hasCleanupError = false
761
+ this.testFailures = []
762
+
759
763
  // Stop browser after suite completes
760
764
  // For restart strategies: stop after each suite
761
765
  // For session mode (restart:false): stop after the last suite
@@ -1635,7 +1639,7 @@ class Playwright extends Helper {
1635
1639
  * @returns Promise<void>
1636
1640
  */
1637
1641
  async replayFromHar(harFilePath, opts) {
1638
- const file = path.join(global.codecept_dir, harFilePath)
1642
+ const file = path.join(store.codeceptDir, harFilePath)
1639
1643
 
1640
1644
  if (!fileExists(file)) {
1641
1645
  throw new Error(`File at ${file} cannot be found on local system`)
@@ -1779,8 +1783,7 @@ class Playwright extends Helper {
1779
1783
  if (elements.length === 0) {
1780
1784
  throw new ElementNotFound(locator, 'Element', 'was not found')
1781
1785
  }
1782
- if (this.options.strict) assertOnlyOneElement(elements, locator, this)
1783
- return elements[0]
1786
+ return selectElement(elements, locator, this)
1784
1787
  }
1785
1788
 
1786
1789
  /**
@@ -1795,8 +1798,7 @@ class Playwright extends Helper {
1795
1798
  const context = providedContext || (await this._getContext())
1796
1799
  const els = await findCheckable.call(this, locator, context)
1797
1800
  assertElementExists(els[0], locator, 'Checkbox or radio')
1798
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
1799
- return els[0]
1801
+ return selectElement(els, locator, this)
1800
1802
  }
1801
1803
 
1802
1804
  /**
@@ -2048,7 +2050,7 @@ class Playwright extends Helper {
2048
2050
  const filePath = await download.path()
2049
2051
  fileName = fileName || `downloads/${path.basename(filePath)}`
2050
2052
 
2051
- const downloadPath = path.join(global.output_dir, fileName)
2053
+ const downloadPath = path.join(store.outputDir, fileName)
2052
2054
  if (!fs.existsSync(path.dirname(downloadPath))) {
2053
2055
  fs.mkdirSync(path.dirname(downloadPath), '0777')
2054
2056
  }
@@ -2079,15 +2081,6 @@ class Playwright extends Helper {
2079
2081
  return proceedClick.call(this, locator, context, options)
2080
2082
  }
2081
2083
 
2082
- /**
2083
- * Clicks link and waits for navigation (deprecated)
2084
- */
2085
- async clickLink(locator, context = null) {
2086
- console.log('clickLink deprecated: Playwright automatically waits for navigation to happen.')
2087
- console.log('Replace I.clickLink with I.click')
2088
- return this.click(locator, context)
2089
- }
2090
-
2091
2084
  /**
2092
2085
  * {{> forceClick }}
2093
2086
  */
@@ -2232,6 +2225,7 @@ class Playwright extends Helper {
2232
2225
  * {{> pressKeyWithKeyNormalization }}
2233
2226
  */
2234
2227
  async pressKey(key) {
2228
+ await checkFocusBeforePressKey(this, key)
2235
2229
  const modifiers = []
2236
2230
  if (Array.isArray(key)) {
2237
2231
  for (let k of key) {
@@ -2260,6 +2254,8 @@ class Playwright extends Helper {
2260
2254
  * {{> type }}
2261
2255
  */
2262
2256
  async type(keys, delay = null) {
2257
+ await checkFocusBeforeType(this)
2258
+
2263
2259
  // Always use page.keyboard.type for any string (including single character and national characters).
2264
2260
  if (!Array.isArray(keys)) {
2265
2261
  keys = keys.toString()
@@ -2282,14 +2278,17 @@ class Playwright extends Helper {
2282
2278
  async fillField(field, value, context = null) {
2283
2279
  const els = await findFields.call(this, field, context)
2284
2280
  assertElementExists(els, field, 'Field')
2285
- if (this.options.strict) assertOnlyOneElement(els, field, this)
2286
- const el = els[0]
2281
+ const el = selectElement(els, field, this)
2282
+
2283
+ await highlightActiveElement.call(this, el)
2284
+
2285
+ if (await fillRichEditor(this, el, value)) {
2286
+ return this._waitForAction()
2287
+ }
2287
2288
 
2288
2289
  await el.clear()
2289
2290
  if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
2290
2291
 
2291
- await highlightActiveElement.call(this, el)
2292
-
2293
2292
  await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2294
2293
 
2295
2294
  return this._waitForAction()
@@ -2301,9 +2300,8 @@ class Playwright extends Helper {
2301
2300
  async clearField(locator, context = null) {
2302
2301
  const els = await findFields.call(this, locator, context)
2303
2302
  assertElementExists(els, locator, 'Field to clear')
2304
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
2305
2303
 
2306
- const el = els[0]
2304
+ const el = selectElement(els, locator, this)
2307
2305
 
2308
2306
  await highlightActiveElement.call(this, el)
2309
2307
 
@@ -2318,10 +2316,10 @@ class Playwright extends Helper {
2318
2316
  async appendField(field, value, context = null) {
2319
2317
  const els = await findFields.call(this, field, context)
2320
2318
  assertElementExists(els, field, 'Field')
2321
- if (this.options.strict) assertOnlyOneElement(els, field, this)
2322
- await highlightActiveElement.call(this, els[0])
2323
- await els[0].press('End')
2324
- await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
2319
+ const el = selectElement(els, field, this)
2320
+ await highlightActiveElement.call(this, el)
2321
+ await el.press('End')
2322
+ await el.type(value.toString(), { delay: this.options.pressKeyDelay })
2325
2323
  return this._waitForAction()
2326
2324
  }
2327
2325
 
@@ -2346,29 +2344,31 @@ class Playwright extends Helper {
2346
2344
  *
2347
2345
  */
2348
2346
  async attachFile(locator, pathToFile, context = null) {
2349
- const file = path.join(global.codecept_dir, pathToFile)
2347
+ const file = path.join(store.codeceptDir, pathToFile)
2350
2348
 
2351
2349
  if (!fileExists(file)) {
2352
2350
  throw new Error(`File at ${file} can not be found on local system`)
2353
2351
  }
2354
2352
  const els = await findFields.call(this, locator, context)
2355
2353
  if (els.length) {
2356
- const tag = await els[0].evaluate(el => el.tagName)
2357
- const type = await els[0].evaluate(el => el.type)
2354
+ const el = selectElement(els, locator, this)
2355
+ const tag = await el.evaluate(el => el.tagName)
2356
+ const type = await el.evaluate(el => el.type)
2358
2357
  if (tag === 'INPUT' && type === 'file') {
2359
- await els[0].setInputFiles(file)
2358
+ await el.setInputFiles(file)
2360
2359
  return this._waitForAction()
2361
2360
  }
2362
2361
  }
2363
2362
 
2364
2363
  const targetEls = els.length ? els : await this._locate(locator)
2365
2364
  assertElementExists(targetEls, locator, 'Element')
2365
+ const el = selectElement(targetEls, locator, this)
2366
2366
  const fileData = {
2367
2367
  base64Content: base64EncodeFile(file),
2368
2368
  fileName: path.basename(file),
2369
2369
  mimeType: getMimeType(path.basename(file)),
2370
2370
  }
2371
- await targetEls[0].evaluate(dropFile, fileData)
2371
+ await el.evaluate(dropFile, fileData)
2372
2372
  return this._waitForAction()
2373
2373
  }
2374
2374
 
@@ -2391,23 +2391,23 @@ class Playwright extends Helper {
2391
2391
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2392
2392
  const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2393
2393
  assertElementExists(els, select, 'Selectable element')
2394
- return proceedSelect.call(this, pageContext, els[0], option)
2394
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2395
2395
  }
2396
2396
 
2397
2397
  // Fuzzy: try combobox
2398
2398
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2399
2399
  const comboboxSearchCtx = contextEl || pageContext
2400
2400
  let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2401
- if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
2401
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2402
2402
 
2403
2403
  // Fuzzy: try listbox
2404
2404
  els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2405
- if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
2405
+ if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2406
2406
 
2407
2407
  // Fuzzy: try native select
2408
2408
  els = await findFields.call(this, select, context)
2409
2409
  assertElementExists(els, select, 'Selectable element')
2410
- return proceedSelect.call(this, pageContext, els[0], option)
2410
+ return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
2411
2411
  }
2412
2412
 
2413
2413
  /**
@@ -2670,8 +2670,11 @@ class Playwright extends Helper {
2670
2670
  * @returns {Promise<any>}
2671
2671
  */
2672
2672
  async executeScript(fn, arg) {
2673
- if (this.context && this.context.constructor.name === 'FrameLocator') {
2674
- // switching to iframe context
2673
+ if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
2674
+ if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
2675
+ return arg.evaluate(fn)
2676
+ }
2677
+ if (this.context && typeof this.context.url !== 'function' && typeof this.context.innerText !== 'function') {
2675
2678
  return this.context.locator(':root').evaluate(fn, arg)
2676
2679
  }
2677
2680
  return this.page.evaluate.apply(this.page, [fn, arg])
@@ -2701,15 +2704,12 @@ class Playwright extends Helper {
2701
2704
  *
2702
2705
  */
2703
2706
  async grabTextFrom(locator) {
2704
- // Handle role locators with text/exact options
2705
- if (isRoleLocatorObject(locator)) {
2706
- const elements = await handleRoleLocator(this.page, locator)
2707
- if (elements && elements.length > 0) {
2708
- const text = await elements[0].textContent()
2709
- assertElementExists(text, JSON.stringify(locator))
2710
- this.debugSection('Text', text)
2711
- return text
2712
- }
2707
+ const roleElements = await handleRoleLocator(this.page, locator)
2708
+ if (roleElements && roleElements.length > 0) {
2709
+ const text = await roleElements[0].textContent()
2710
+ assertElementExists(text, JSON.stringify(locator))
2711
+ this.debugSection('Text', text)
2712
+ return text
2713
2713
  }
2714
2714
 
2715
2715
  const locatorObj = new Locator(locator, 'css')
@@ -2937,7 +2937,7 @@ class Playwright extends Helper {
2937
2937
  const els = await this._locate(matchedLocator)
2938
2938
  assertElementExists(els, locator)
2939
2939
  const snapshot = await els[0].ariaSnapshot()
2940
- this.debugSection('Aria Snapshot', snapshot)
2940
+ this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
2941
2941
  return snapshot
2942
2942
  }
2943
2943
 
@@ -3297,8 +3297,6 @@ class Playwright extends Helper {
3297
3297
  }
3298
3298
 
3299
3299
  /**
3300
- * This method accepts [React selectors](https://codecept.io/react).
3301
- *
3302
3300
  * {{> waitForVisible }}
3303
3301
  */
3304
3302
  async waitForVisible(locator, sec) {
@@ -3411,7 +3409,7 @@ class Playwright extends Helper {
3411
3409
  }
3412
3410
 
3413
3411
  async _getContext() {
3414
- if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) {
3412
+ if (this.context) {
3415
3413
  return this.context
3416
3414
  }
3417
3415
  if (this.frame) {
@@ -4195,25 +4193,22 @@ export function buildLocatorString(locator) {
4195
4193
  return locator.simplify()
4196
4194
  }
4197
4195
 
4198
- /**
4199
- * Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
4200
- */
4201
- function isRoleLocatorObject(locator) {
4202
- return locator && typeof locator === 'object' && locator.role && !locator.type
4203
- }
4204
-
4205
4196
  /**
4206
4197
  * Handles role locator objects by converting them to Playwright's getByRole() API
4198
+ * Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
4207
4199
  * Returns elements array if role locator, null otherwise
4208
4200
  */
4209
4201
  async function handleRoleLocator(context, locator) {
4210
- if (!isRoleLocatorObject(locator)) return null
4202
+ const loc = new Locator(locator)
4203
+ if (!loc.isRole()) return null
4211
4204
 
4205
+ const roleObj = loc.locator || {}
4212
4206
  const options = {}
4213
- if (locator.text) options.name = locator.text
4214
- if (locator.exact !== undefined) options.exact = locator.exact
4207
+ if (roleObj.text) options.name = roleObj.text
4208
+ if (roleObj.name) options.name = roleObj.name
4209
+ if (roleObj.exact !== undefined) options.exact = roleObj.exact
4215
4210
 
4216
- return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4211
+ return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
4217
4212
  }
4218
4213
 
4219
4214
  async function findByRole(context, locator) {
@@ -4225,13 +4220,8 @@ async function findByRole(context, locator) {
4225
4220
  }
4226
4221
 
4227
4222
  async function findElements(matcher, locator) {
4228
- // Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
4229
- const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4230
- const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4231
4223
  const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4232
4224
 
4233
- if (isReactLocator) return findReact(matcher, locator)
4234
- if (isVueLocator) return findVue(matcher, locator)
4235
4225
  if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
4236
4226
 
4237
4227
  // Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
@@ -4246,8 +4236,6 @@ async function findElements(matcher, locator) {
4246
4236
  }
4247
4237
 
4248
4238
  async function findElement(matcher, locator) {
4249
- if (locator.react) return findReact(matcher, locator)
4250
- if (locator.vue) return findVue(matcher, locator)
4251
4239
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
4252
4240
 
4253
4241
  locator = new Locator(locator, 'css')
@@ -4282,16 +4270,22 @@ async function proceedClick(locator, context = null, options = {}) {
4282
4270
  assertElementExists(els, locator, 'Clickable element')
4283
4271
  }
4284
4272
 
4285
- await highlightActiveElement.call(this, els[0])
4286
- if (store.debugMode) this.debugSection('Clicked', await elToString(els[0], 1))
4273
+ const opts = store.currentStep?.opts
4274
+ let element
4275
+ if (opts?.elementIndex != null) {
4276
+ element = selectElement(els, locator, this)
4277
+ } else {
4278
+ const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
4279
+ if (strict) assertOnlyOneElement(els, locator, this)
4280
+ element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4281
+ }
4282
+
4283
+ await highlightActiveElement.call(this, element)
4284
+ if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
4287
4285
 
4288
- /*
4289
- using the force true options itself but instead dispatching a click
4290
- */
4291
4286
  if (options.force) {
4292
- await els[0].dispatchEvent('click')
4287
+ await element.dispatchEvent('click')
4293
4288
  } else {
4294
- const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
4295
4289
  await element.click(options)
4296
4290
  }
4297
4291
  const promises = []
@@ -4308,7 +4302,6 @@ async function findClickable(matcher, locator) {
4308
4302
 
4309
4303
  if (!matchedLocator.isFuzzy()) {
4310
4304
  const els = await findElements.call(this, matcher, matchedLocator)
4311
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4312
4305
  return els
4313
4306
  }
4314
4307
 
@@ -4317,42 +4310,27 @@ async function findClickable(matcher, locator) {
4317
4310
 
4318
4311
  try {
4319
4312
  els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
4320
- if (els.length) {
4321
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4322
- return els
4323
- }
4313
+ if (els.length) return els
4324
4314
  } catch (err) {
4325
4315
  // getByRole not supported or failed
4326
4316
  }
4327
4317
 
4328
4318
  try {
4329
4319
  els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
4330
- if (els.length) {
4331
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4332
- return els
4333
- }
4320
+ if (els.length) return els
4334
4321
  } catch (err) {
4335
4322
  // getByRole not supported or failed
4336
4323
  }
4337
4324
 
4338
4325
  els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
4339
- if (els.length) {
4340
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4341
- return els
4342
- }
4326
+ if (els.length) return els
4343
4327
 
4344
4328
  els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
4345
- if (els.length) {
4346
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4347
- return els
4348
- }
4329
+ if (els.length) return els
4349
4330
 
4350
4331
  try {
4351
4332
  els = await findElements.call(this, matcher, Locator.clickable.self(literal))
4352
- if (els.length) {
4353
- if (this.options.strict) assertOnlyOneElement(els, locator, this)
4354
- return els
4355
- }
4333
+ if (els.length) return els
4356
4334
  } catch (err) {
4357
4335
  // Do nothing
4358
4336
  }
@@ -4367,7 +4345,9 @@ async function proceedSee(assertType, text, context, strict = false) {
4367
4345
  if (!context) {
4368
4346
  const el = await this.context
4369
4347
 
4370
- allText = el.constructor.name !== 'Locator' ? [await el.locator('body').innerText()] : [await el.innerText()]
4348
+ allText = typeof el.url !== 'function' && typeof el.innerText === 'function'
4349
+ ? [await el.innerText()]
4350
+ : [await el.locator('body').innerText()]
4371
4351
 
4372
4352
  description = 'web application'
4373
4353
  } else {
@@ -4437,12 +4417,9 @@ async function findFields(locator, context = null) {
4437
4417
  ? loc => findElements.call(this, contextEl, loc)
4438
4418
  : loc => this._locate(loc)
4439
4419
 
4440
- // Handle role locators with text/exact options
4441
- if (isRoleLocatorObject(locator)) {
4442
- const matcher = contextEl || (await this.page)
4443
- const roleElements = await handleRoleLocator(matcher, locator)
4444
- if (roleElements) return roleElements
4445
- }
4420
+ const matcher = contextEl || (await this.page)
4421
+ const roleElements = await handleRoleLocator(matcher, locator)
4422
+ if (roleElements) return roleElements
4446
4423
 
4447
4424
  const matchedLocator = new Locator(locator)
4448
4425
  if (!matchedLocator.isFuzzy()) {
@@ -4653,12 +4630,16 @@ async function targetCreatedHandler(page) {
4653
4630
  .catch(() => null)
4654
4631
  .then(async () => {
4655
4632
  if (this.context && this.context._type === 'Frame') {
4656
- // we are inside iframe?
4633
+ // we are inside iframe via Frame object — refresh handle
4657
4634
  const frameEl = await this.context.frameElement()
4658
4635
  this.context = await frameEl.contentFrame()
4659
4636
  this.contextLocator = null
4660
4637
  return
4661
4638
  }
4639
+ if (this.context && this.context.constructor && this.context.constructor.name === 'FrameLocator') {
4640
+ // we are inside iframe via FrameLocator — keep it across load events
4641
+ return
4642
+ }
4662
4643
  // if context element was in iframe - keep it
4663
4644
  // if (await this.context.ownerFrame()) return;
4664
4645
  this.context = page
@@ -4839,7 +4820,7 @@ async function refreshContextSession() {
4839
4820
 
4840
4821
  function saveVideoForPage(page, name) {
4841
4822
  if (!page.video()) return null
4842
- const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
4823
+ const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
4843
4824
  page
4844
4825
  .video()
4845
4826
  .saveAs(fileName)
@@ -4856,7 +4837,7 @@ async function saveTraceForContext(context, name) {
4856
4837
  if (!context) return
4857
4838
  if (!context.tracing) return
4858
4839
  try {
4859
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4840
+ const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
4860
4841
  await context.tracing.stop({ path: fileName })
4861
4842
  return fileName
4862
4843
  } catch (err) {