codeceptjs 4.0.1-beta.9 → 4.0.1

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 (328) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +17 -4
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1189 -0
  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 +745 -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 +195 -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 +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +14 -10
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -2
  112. package/lib/command/run-workers.js +14 -16
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +117 -9
  116. package/lib/config.js +98 -19
  117. package/lib/container.js +188 -19
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +246 -2
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +8 -8
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +367 -516
  128. package/lib/helper/Puppeteer.js +343 -197
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +6 -15
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/cli.js +19 -1
  151. package/lib/mocha/factory.js +13 -28
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/test.js +4 -2
  155. package/lib/mocha/ui.js +5 -6
  156. package/lib/output.js +2 -2
  157. package/lib/parser.js +2 -2
  158. package/lib/pause.js +38 -4
  159. package/lib/plugin/aiTrace.js +457 -0
  160. package/lib/plugin/analyze.js +9 -9
  161. package/lib/plugin/auth.js +5 -4
  162. package/lib/plugin/browser.js +77 -0
  163. package/lib/plugin/expose.js +159 -0
  164. package/lib/plugin/heal.js +47 -3
  165. package/lib/plugin/junitReporter.js +303 -0
  166. package/lib/plugin/pageInfo.js +54 -52
  167. package/lib/plugin/pause.js +131 -0
  168. package/lib/plugin/pauseOnFail.js +11 -33
  169. package/lib/plugin/retryFailedStep.js +43 -32
  170. package/lib/plugin/screencast.js +289 -0
  171. package/lib/plugin/screenshot.js +558 -0
  172. package/lib/plugin/screenshotOnFail.js +9 -170
  173. package/lib/plugin/stepTimeout.js +3 -2
  174. package/lib/recorder.js +1 -1
  175. package/lib/rerun.js +2 -1
  176. package/lib/result.js +2 -1
  177. package/lib/step/base.js +23 -9
  178. package/lib/step/comment.js +2 -2
  179. package/lib/step/config.js +15 -2
  180. package/lib/step/helper.js +4 -4
  181. package/lib/step/meta.js +3 -3
  182. package/lib/step/record.js +12 -4
  183. package/lib/store.js +72 -3
  184. package/lib/translation.js +2 -1
  185. package/lib/utils/loaderCheck.js +41 -3
  186. package/lib/utils/mask_data.js +2 -1
  187. package/lib/utils/pluginParser.js +151 -0
  188. package/lib/utils/trace.js +297 -0
  189. package/lib/utils/typescript.js +261 -49
  190. package/lib/utils.js +77 -3
  191. package/lib/workers.js +123 -17
  192. package/package.json +48 -43
  193. package/typings/index.d.ts +120 -9
  194. package/typings/promiseBasedTypes.d.ts +3243 -6057
  195. package/typings/types.d.ts +3541 -6506
  196. package/docs/webapi/amOnPage.mustache +0 -11
  197. package/docs/webapi/appendField.mustache +0 -11
  198. package/docs/webapi/attachFile.mustache +0 -12
  199. package/docs/webapi/blur.mustache +0 -18
  200. package/docs/webapi/checkOption.mustache +0 -13
  201. package/docs/webapi/clearCookie.mustache +0 -9
  202. package/docs/webapi/clearField.mustache +0 -9
  203. package/docs/webapi/click.mustache +0 -29
  204. package/docs/webapi/clickLink.mustache +0 -8
  205. package/docs/webapi/closeCurrentTab.mustache +0 -7
  206. package/docs/webapi/closeOtherTabs.mustache +0 -8
  207. package/docs/webapi/dontSee.mustache +0 -11
  208. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  209. package/docs/webapi/dontSeeCookie.mustache +0 -8
  210. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  211. package/docs/webapi/dontSeeElement.mustache +0 -8
  212. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  213. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  214. package/docs/webapi/dontSeeInField.mustache +0 -11
  215. package/docs/webapi/dontSeeInSource.mustache +0 -8
  216. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  217. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  218. package/docs/webapi/doubleClick.mustache +0 -13
  219. package/docs/webapi/downloadFile.mustache +0 -12
  220. package/docs/webapi/dragAndDrop.mustache +0 -9
  221. package/docs/webapi/dragSlider.mustache +0 -11
  222. package/docs/webapi/executeAsyncScript.mustache +0 -24
  223. package/docs/webapi/executeScript.mustache +0 -26
  224. package/docs/webapi/fillField.mustache +0 -16
  225. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  226. package/docs/webapi/focus.mustache +0 -13
  227. package/docs/webapi/forceClick.mustache +0 -28
  228. package/docs/webapi/forceRightClick.mustache +0 -18
  229. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  230. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  231. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  232. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  233. package/docs/webapi/grabCookie.mustache +0 -11
  234. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  235. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  236. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  237. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  238. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  239. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  240. package/docs/webapi/grabGeoLocation.mustache +0 -8
  241. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  242. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  243. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  244. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  245. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  246. package/docs/webapi/grabPopupText.mustache +0 -5
  247. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  248. package/docs/webapi/grabSource.mustache +0 -8
  249. package/docs/webapi/grabTextFrom.mustache +0 -10
  250. package/docs/webapi/grabTextFromAll.mustache +0 -9
  251. package/docs/webapi/grabTitle.mustache +0 -8
  252. package/docs/webapi/grabValueFrom.mustache +0 -9
  253. package/docs/webapi/grabValueFromAll.mustache +0 -8
  254. package/docs/webapi/grabWebElement.mustache +0 -9
  255. package/docs/webapi/grabWebElements.mustache +0 -9
  256. package/docs/webapi/moveCursorTo.mustache +0 -12
  257. package/docs/webapi/openNewTab.mustache +0 -7
  258. package/docs/webapi/pressKey.mustache +0 -12
  259. package/docs/webapi/pressKeyDown.mustache +0 -12
  260. package/docs/webapi/pressKeyUp.mustache +0 -12
  261. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  262. package/docs/webapi/refreshPage.mustache +0 -6
  263. package/docs/webapi/resizeWindow.mustache +0 -6
  264. package/docs/webapi/rightClick.mustache +0 -14
  265. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  266. package/docs/webapi/saveScreenshot.mustache +0 -12
  267. package/docs/webapi/say.mustache +0 -10
  268. package/docs/webapi/scrollIntoView.mustache +0 -11
  269. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  270. package/docs/webapi/scrollPageToTop.mustache +0 -6
  271. package/docs/webapi/scrollTo.mustache +0 -12
  272. package/docs/webapi/see.mustache +0 -11
  273. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  274. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  275. package/docs/webapi/seeCookie.mustache +0 -8
  276. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  277. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  278. package/docs/webapi/seeElement.mustache +0 -8
  279. package/docs/webapi/seeElementInDOM.mustache +0 -8
  280. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  281. package/docs/webapi/seeInField.mustache +0 -12
  282. package/docs/webapi/seeInPopup.mustache +0 -8
  283. package/docs/webapi/seeInSource.mustache +0 -7
  284. package/docs/webapi/seeInTitle.mustache +0 -8
  285. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  286. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  287. package/docs/webapi/seeTextEquals.mustache +0 -9
  288. package/docs/webapi/seeTitleEquals.mustache +0 -8
  289. package/docs/webapi/seeTraffic.mustache +0 -36
  290. package/docs/webapi/selectOption.mustache +0 -21
  291. package/docs/webapi/setCookie.mustache +0 -16
  292. package/docs/webapi/setGeoLocation.mustache +0 -12
  293. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  294. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  295. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  296. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  297. package/docs/webapi/switchTo.mustache +0 -9
  298. package/docs/webapi/switchToNextTab.mustache +0 -10
  299. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  300. package/docs/webapi/type.mustache +0 -21
  301. package/docs/webapi/uncheckOption.mustache +0 -13
  302. package/docs/webapi/wait.mustache +0 -8
  303. package/docs/webapi/waitForClickable.mustache +0 -11
  304. package/docs/webapi/waitForCookie.mustache +0 -9
  305. package/docs/webapi/waitForDetached.mustache +0 -10
  306. package/docs/webapi/waitForDisabled.mustache +0 -6
  307. package/docs/webapi/waitForElement.mustache +0 -11
  308. package/docs/webapi/waitForEnabled.mustache +0 -6
  309. package/docs/webapi/waitForFunction.mustache +0 -17
  310. package/docs/webapi/waitForInvisible.mustache +0 -10
  311. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  312. package/docs/webapi/waitForText.mustache +0 -13
  313. package/docs/webapi/waitForValue.mustache +0 -10
  314. package/docs/webapi/waitForVisible.mustache +0 -10
  315. package/docs/webapi/waitInUrl.mustache +0 -9
  316. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  317. package/docs/webapi/waitToHide.mustache +0 -10
  318. package/docs/webapi/waitUrlEquals.mustache +0 -10
  319. package/lib/helper/AI.js +0 -214
  320. package/lib/helper/Mochawesome.js +0 -96
  321. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  322. package/lib/helper/extras/React.js +0 -65
  323. package/lib/listener/enhancedGlobalRetry.js +0 -110
  324. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  325. package/lib/plugin/htmlReporter.js +0 -3648
  326. package/lib/plugin/stepByStepReport.js +0 -427
  327. package/lib/plugin/subtitles.js +0 -89
  328. package/lib/retryCoordinator.js +0 -207
@@ -5,10 +5,13 @@ import Locator from '../../locator.js'
5
5
  */
6
6
  class ElementNotFound {
7
7
  constructor(locator, prefixMessage = 'Element', postfixMessage = 'was not found by text|CSS|XPath') {
8
+ let locatorStr
8
9
  if (typeof locator === 'object') {
9
- locator = JSON.stringify(locator)
10
+ locatorStr = JSON.stringify(locator)
11
+ } else {
12
+ locatorStr = new Locator(locator).toString()
10
13
  }
11
- throw new Error(`${prefixMessage} "${new Locator(locator)}" ${postfixMessage}`)
14
+ throw new Error(`${prefixMessage} "${locatorStr}" ${postfixMessage}`)
12
15
  }
13
16
  }
14
17
 
@@ -0,0 +1,52 @@
1
+ import Locator from '../../locator.js'
2
+
3
+ class MultipleElementsFound extends Error {
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.`)
9
+ this.name = 'MultipleElementsFound'
10
+ this.locator = locator
11
+ this.webElements = webElements
12
+ this.count = webElements.length
13
+ this._detailsFetched = false
14
+ }
15
+
16
+ async fetchDetails() {
17
+ if (this._detailsFetched) return
18
+
19
+ try {
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
+ }
32
+ }
33
+
34
+ if (this.count > 10) {
35
+ items.push(` ... and ${this.count - 10} more`)
36
+ }
37
+
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') +
43
+ `\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
44
+ } catch (err) {
45
+ this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
46
+ }
47
+
48
+ this._detailsFetched = true
49
+ }
50
+ }
51
+
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
@@ -0,0 +1,45 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import minimatch from 'minimatch'
4
+ import store from '../../store.js'
5
+ import assert from 'assert'
6
+
7
+ function getDownloadDir() {
8
+ return path.join(store.outputDir, 'downloads')
9
+ }
10
+
11
+ function getNewFiles(downloadDir, sinceTimestamp) {
12
+ if (!fs.existsSync(downloadDir)) return []
13
+ return fs.readdirSync(downloadDir).filter(name => {
14
+ const stat = fs.statSync(path.join(downloadDir, name))
15
+ return stat.isFile() && stat.mtimeMs >= sinceTimestamp
16
+ })
17
+ }
18
+
19
+ function seeFileDownloaded(arg) {
20
+ const downloadDir = getDownloadDir()
21
+ const files = getNewFiles(downloadDir, this._downloadStartTimestamp)
22
+
23
+ if (arg === undefined || arg === null) {
24
+ assert.ok(files.length > 0, `No files downloaded to ${downloadDir}`)
25
+ return
26
+ }
27
+ if (typeof arg === 'number') {
28
+ assert.strictEqual(files.length, arg, `Expected ${arg} downloaded file(s), found ${files.length}: [${files.join(', ')}]`)
29
+ return
30
+ }
31
+ const regexMatch = arg.match(/^\/(.+)\/$/)
32
+ if (regexMatch) {
33
+ const re = new RegExp(regexMatch[1])
34
+ assert.ok(files.some(f => re.test(f)), `No file matches ${arg}. Downloaded: [${files.join(', ')}]`)
35
+ return
36
+ }
37
+ if (/[*?[\]]/.test(arg)) {
38
+ const matched = minimatch.match(files, arg)
39
+ assert.ok(matched.length > 0, `No file matches glob "${arg}". Downloaded: [${files.join(', ')}]`)
40
+ return
41
+ }
42
+ assert.ok(files.includes(arg), `File "${arg}" not downloaded. Downloaded: [${files.join(', ')}]`)
43
+ }
44
+
45
+ export { seeFileDownloaded, getDownloadDir }
@@ -1,110 +1,10 @@
1
- import Locator from '../../locator.js'
2
-
3
- function buildLocatorString(locator) {
4
- if (locator.isCustom()) {
5
- return `${locator.type}=${locator.value}`
6
- }
7
- if (locator.isXPath()) {
8
- return `xpath=${locator.value}`
9
- }
10
- return locator.simplify()
11
- }
12
-
13
- async function findElements(matcher, locator) {
14
- const matchedLocator = new Locator(locator, 'css')
15
-
16
- if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
17
- if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
18
- if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator)
19
- if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator)
20
-
21
- return matcher.locator(buildLocatorString(matchedLocator)).all()
22
- }
23
-
24
- async function findElement(matcher, locator) {
25
- const matchedLocator = new Locator(locator, 'css')
26
-
27
- if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
28
- if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
29
- if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator, { first: true })
30
- if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator, { first: true })
31
-
32
- return matcher.locator(buildLocatorString(matchedLocator)).first()
33
- }
34
-
35
- async function getVisibleElements(elements) {
36
- const visibleElements = []
37
- for (const element of elements) {
38
- if (await element.isVisible()) {
39
- visibleElements.push(element)
40
- }
41
- }
42
- if (visibleElements.length === 0) {
43
- return elements
44
- }
45
- return visibleElements
46
- }
47
-
48
- async function findReact(matcher, locator) {
49
- const details = locator.locator ?? { react: locator.value }
50
- let locatorString = `_react=${details.react}`
51
-
52
- if (details.props) {
53
- locatorString += propBuilder(details.props)
54
- }
55
-
56
- return matcher.locator(locatorString).all()
57
- }
58
-
59
- async function findVue(matcher, locator) {
60
- const details = locator.locator ?? { vue: locator.value }
61
- let locatorString = `_vue=${details.vue}`
62
-
63
- if (details.props) {
64
- locatorString += propBuilder(details.props)
65
- }
66
-
67
- return matcher.locator(locatorString).all()
68
- }
69
-
70
- async function findByPlaywrightLocator(matcher, locator, { first = false } = {}) {
71
- const details = locator.locator ?? { pw: locator.value }
72
- const locatorValue = details.pw
73
-
74
- const handle = matcher.locator(locatorValue)
75
- return first ? handle.first() : handle.all()
76
- }
77
-
78
- async function findByRole(matcher, locator, { first = false } = {}) {
79
- const details = locator.locator ?? { role: locator.value }
80
- const { role, text, name, exact, includeHidden, ...rest } = details
81
- const options = { ...rest }
82
-
83
- if (includeHidden !== undefined) options.includeHidden = includeHidden
84
-
85
- const accessibleName = name ?? text
86
- if (accessibleName !== undefined) {
87
- options.name = accessibleName
88
- if (exact === true) options.exact = true
89
- }
90
-
91
- const roleLocator = matcher.getByRole(role, options)
92
- return first ? roleLocator.first() : roleLocator.all()
93
- }
94
-
95
- function propBuilder(props) {
96
- let _props = ''
97
-
98
- for (const [key, value] of Object.entries(props)) {
99
- if (typeof value === 'object') {
100
- for (const [k, v] of Object.entries(value)) {
101
- _props += `[${key}.${k} = "${v}"]`
102
- }
103
- } else {
104
- _props += `[${key} = "${value}"]`
105
- }
1
+ async function findByPlaywrightLocator(matcher, locator) {
2
+ const pwLocator = locator.locator || locator
3
+ if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
4
+ return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
106
5
  }
107
- return _props
6
+ const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
7
+ return matcher.locator(pwValue).all()
108
8
  }
109
9
 
110
- export { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByPlaywrightLocator, findByRole }
10
+ export { findByPlaywrightLocator }
@@ -0,0 +1,58 @@
1
+ import store from '../../store.js'
2
+ import output from '../../output.js'
3
+ import WebElement from '../../element/WebElement.js'
4
+ import MultipleElementsFound from '../errors/MultipleElementsFound.js'
5
+
6
+ function resolveElementIndex(value) {
7
+ if (value === 'first') return 1
8
+ if (value === 'last') return -1
9
+ return value
10
+ }
11
+
12
+ function isStrictStep(opts, helper) {
13
+ if (opts?.exact === true || opts?.strictMode === true) return true
14
+ if (opts?.exact === false || opts?.strictMode === false) return false
15
+ return helper.options.strict
16
+ }
17
+
18
+ function selectElement(els, locator, helper) {
19
+ const opts = store.currentStep?.opts
20
+ const rawIndex = opts?.elementIndex
21
+ const elementIndex = resolveElementIndex(rawIndex)
22
+
23
+ if (elementIndex != null) {
24
+ if (els.length === 1) return els[0]
25
+
26
+ if (!Number.isInteger(elementIndex) || elementIndex === 0) {
27
+ throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
28
+ }
29
+
30
+ let idx
31
+ if (elementIndex > 0) {
32
+ idx = elementIndex - 1
33
+ if (idx >= els.length) {
34
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
35
+ }
36
+ } else {
37
+ idx = els.length + elementIndex
38
+ if (idx < 0) {
39
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
40
+ }
41
+ }
42
+
43
+ output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
44
+ return els[idx]
45
+ }
46
+
47
+ if (isStrictStep(opts, helper)) {
48
+ if (els.length > 1) {
49
+ const webElements = Array.from(els).map(el => new WebElement(el, helper))
50
+ throw new MultipleElementsFound(locator, webElements)
51
+ }
52
+ }
53
+
54
+ if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
55
+ return els[0]
56
+ }
57
+
58
+ export { selectElement }
@@ -0,0 +1,43 @@
1
+ import store from '../../store.js'
2
+ import NonFocusedType from '../errors/NonFocusedType.js'
3
+
4
+ const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i
5
+ const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
6
+
7
+ async function isNoElementFocused(helper) {
8
+ return helper.executeScript(() => {
9
+ const ae = document.activeElement
10
+ return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
11
+ })
12
+ }
13
+
14
+ export async function checkFocusBeforeType(helper) {
15
+ if (!helper.options.strict && !store.debugMode) return
16
+ if (!await isNoElementFocused(helper)) return
17
+
18
+ const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
19
+ if (helper.options.strict) throw new NonFocusedType(message)
20
+ helper.debugSection('Warning', message)
21
+ }
22
+
23
+ export async function checkFocusBeforePressKey(helper, originalKey) {
24
+ if (!helper.options.strict && !store.debugMode) return
25
+ if (!Array.isArray(originalKey)) return
26
+
27
+ let hasCtrlOrMeta = false
28
+ let actionKey = null
29
+ for (const k of originalKey) {
30
+ if (MODIFIER_PATTERN.test(k)) {
31
+ hasCtrlOrMeta = true
32
+ } else {
33
+ actionKey = k
34
+ }
35
+ }
36
+ if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return
37
+
38
+ if (!await isNoElementFocused(helper)) return
39
+
40
+ const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.`
41
+ if (helper.options.strict) throw new NonFocusedType(message)
42
+ helper.debugSection('Warning', message)
43
+ }
@@ -0,0 +1,178 @@
1
+ import WebElement from '../../element/WebElement.js'
2
+
3
+ const MARKER = 'data-codeceptjs-rte-target'
4
+
5
+ const EDITOR = {
6
+ STANDARD: 'standard',
7
+ IFRAME: 'iframe',
8
+ CONTENTEDITABLE: 'contenteditable',
9
+ HIDDEN_TEXTAREA: 'hidden-textarea',
10
+ UNREACHABLE: 'unreachable',
11
+ }
12
+
13
+ function detectAndMark(el, opts) {
14
+ const marker = opts.marker
15
+ const kinds = opts.kinds
16
+ const CE = '[contenteditable="true"], [contenteditable=""]'
17
+
18
+ function mark(kind, target) {
19
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
20
+ if (target && target.nodeType === 1) target.setAttribute(marker, '1')
21
+ return kind
22
+ }
23
+
24
+ if (!el || el.nodeType !== 1) return mark(kinds.STANDARD, el)
25
+
26
+ const tag = el.tagName
27
+ if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
28
+ if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
29
+
30
+ const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
31
+ if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
32
+ const style = window.getComputedStyle(el)
33
+ if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
34
+ }
35
+
36
+ const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
37
+ if (canSearchDescendants) {
38
+ const iframe = el.querySelector('iframe')
39
+ if (iframe) return mark(kinds.IFRAME, iframe)
40
+ const ce = el.querySelector(CE)
41
+ if (ce) return mark(kinds.CONTENTEDITABLE, ce)
42
+ const textareas = [...el.querySelectorAll('textarea')]
43
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
44
+ const textarea = focusable || textareas[0]
45
+ if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
46
+ }
47
+
48
+ return mark(kinds.STANDARD, el)
49
+ }
50
+
51
+ function detectInsideFrame() {
52
+ const MARKER = 'data-codeceptjs-rte-target'
53
+ const CE = '[contenteditable="true"], [contenteditable=""]'
54
+ const CONTENTEDITABLE = 'contenteditable'
55
+ const HIDDEN_TEXTAREA = 'hidden-textarea'
56
+ const body = document.body
57
+ document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
58
+
59
+ if (body.isContentEditable) return CONTENTEDITABLE
60
+
61
+ const ce = body.querySelector(CE)
62
+ if (ce) {
63
+ ce.setAttribute(MARKER, '1')
64
+ return CONTENTEDITABLE
65
+ }
66
+
67
+ const textareas = [...body.querySelectorAll('textarea')]
68
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
69
+ const textarea = focusable || textareas[0]
70
+ if (textarea) {
71
+ textarea.setAttribute(MARKER, '1')
72
+ return HIDDEN_TEXTAREA
73
+ }
74
+
75
+ return CONTENTEDITABLE
76
+ }
77
+
78
+ async function evaluateInFrame(helper, body, fn) {
79
+ if (body.helperType === 'webdriver') {
80
+ return helper.executeScript(fn)
81
+ }
82
+ return body.element.evaluate(fn)
83
+ }
84
+
85
+ function focusMarkedInFrameScript() {
86
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
87
+ el.focus()
88
+ return document.activeElement === el
89
+ }
90
+
91
+ function selectAllInFrameScript() {
92
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
93
+ el.focus()
94
+ const range = document.createRange()
95
+ range.selectNodeContents(el)
96
+ const sel = window.getSelection()
97
+ sel.removeAllRanges()
98
+ sel.addRange(range)
99
+ return document.activeElement === el
100
+ }
101
+
102
+ function selectAllInEditable(el) {
103
+ const doc = el.ownerDocument
104
+ const win = doc.defaultView
105
+ el.focus()
106
+ const range = doc.createRange()
107
+ range.selectNodeContents(el)
108
+ const sel = win.getSelection()
109
+ sel.removeAllRanges()
110
+ sel.addRange(range)
111
+ }
112
+
113
+ function unmarkAll(marker) {
114
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
115
+ }
116
+
117
+ function isActive(el) {
118
+ return el.ownerDocument.activeElement === el
119
+ }
120
+
121
+ async function assertFocused(target) {
122
+ const focused = await target.evaluate(isActive)
123
+ if (!focused) {
124
+ throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
125
+ }
126
+ }
127
+
128
+ async function findMarked(helper) {
129
+ const root = helper.page || helper.browser
130
+ const raw = await root.$('[' + MARKER + ']')
131
+ return new WebElement(raw, helper)
132
+ }
133
+
134
+ async function clearMarker(helper) {
135
+ if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
136
+ return helper.executeScript(unmarkAll, MARKER)
137
+ }
138
+
139
+ export async function fillRichEditor(helper, el, value) {
140
+ const source = el instanceof WebElement ? el : new WebElement(el, helper)
141
+ const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
142
+ if (kind === EDITOR.STANDARD) return false
143
+ if (kind === EDITOR.UNREACHABLE) {
144
+ throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
145
+ }
146
+
147
+ const target = await findMarked(helper)
148
+ const delay = helper.options.pressKeyDelay
149
+
150
+ if (kind === EDITOR.IFRAME) {
151
+ await target.inIframe(async body => {
152
+ const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
153
+ if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
154
+ const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
155
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
156
+ await body.selectAllAndDelete()
157
+ await body.typeText(value, { delay })
158
+ } else {
159
+ const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
160
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
161
+ await body.typeText(value, { delay })
162
+ }
163
+ })
164
+ } else if (kind === EDITOR.HIDDEN_TEXTAREA) {
165
+ await target.focus()
166
+ await assertFocused(target)
167
+ await target.selectAllAndDelete()
168
+ await target.typeText(value, { delay })
169
+ } else if (kind === EDITOR.CONTENTEDITABLE) {
170
+ await target.click()
171
+ await target.evaluate(selectAllInEditable)
172
+ await assertFocused(target)
173
+ await target.typeText(value, { delay })
174
+ }
175
+
176
+ await clearMarker(helper)
177
+ return true
178
+ }
@@ -0,0 +1,11 @@
1
+ export const dropFile = (el, { base64Content, fileName, mimeType }) => {
2
+ const binaryStr = atob(base64Content)
3
+ const bytes = new Uint8Array(binaryStr.length)
4
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
5
+ const fileObj = new File([bytes], fileName, { type: mimeType })
6
+ const dataTransfer = new DataTransfer()
7
+ dataTransfer.items.add(fileObj)
8
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
9
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
10
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
11
+ }
package/lib/history.js CHANGED
@@ -2,6 +2,7 @@ import colors from 'chalk'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
4
  import output from './output.js'
5
+ import store from './store.js'
5
6
 
6
7
  /**
7
8
  * REPL history records REPL commands and stores them in
@@ -9,8 +10,8 @@ import output from './output.js'
9
10
  */
10
11
  class ReplHistory {
11
12
  constructor() {
12
- if (global.output_dir) {
13
- this.historyFile = path.join(global.output_dir, 'cli-history')
13
+ if (store.outputDir) {
14
+ this.historyFile = path.join(store.outputDir, 'cli-history')
14
15
  }
15
16
  this.commands = []
16
17
  }