codeceptjs 4.0.0-rc.1 → 4.0.0-rc.7

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 (36) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +610 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  12. package/docs/webapi/seeElement.mustache +4 -0
  13. package/docs/webapi/seeInField.mustache +5 -0
  14. package/docs/webapi/selectOption.mustache +5 -0
  15. package/docs/webapi/uncheckOption.mustache +1 -1
  16. package/lib/codecept.js +20 -17
  17. package/lib/command/init.js +0 -3
  18. package/lib/command/run-workers.js +1 -0
  19. package/lib/container.js +19 -4
  20. package/lib/helper/Appium.js +8 -8
  21. package/lib/helper/Playwright.js +163 -70
  22. package/lib/helper/Puppeteer.js +165 -59
  23. package/lib/helper/WebDriver.js +134 -49
  24. package/lib/listener/globalRetry.js +32 -6
  25. package/lib/plugin/aiTrace.js +464 -0
  26. package/lib/plugin/retryFailedStep.js +28 -19
  27. package/lib/plugin/stepByStepReport.js +5 -1
  28. package/lib/utils.js +48 -0
  29. package/lib/workers.js +49 -7
  30. package/package.json +5 -3
  31. package/lib/listener/enhancedGlobalRetry.js +0 -110
  32. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  33. package/lib/plugin/htmlReporter.js +0 -3648
  34. package/lib/retryCoordinator.js +0 -207
  35. package/typings/promiseBasedTypes.d.ts +0 -9469
  36. package/typings/types.d.ts +0 -11402
@@ -23,7 +23,11 @@ import {
23
23
  clearString,
24
24
  requireWithFallback,
25
25
  normalizeSpacesInString,
26
+ normalizePath,
27
+ resolveUrl,
26
28
  relativeDir,
29
+ getMimeType,
30
+ base64EncodeFile,
27
31
  } from '../utils.js'
28
32
  import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
29
33
  import ElementNotFound from './errors/ElementNotFound.js'
@@ -1944,8 +1948,15 @@ class Playwright extends Helper {
1944
1948
  * {{> seeElement }}
1945
1949
  *
1946
1950
  */
1947
- async seeElement(locator) {
1948
- let els = await this._locate(locator)
1951
+ async seeElement(locator, context = null) {
1952
+ let els
1953
+ if (context) {
1954
+ const contextEls = await this._locate(context)
1955
+ assertElementExists(contextEls, context, 'Context element')
1956
+ els = await findElements.call(this, contextEls[0], locator)
1957
+ } else {
1958
+ els = await this._locate(locator)
1959
+ }
1949
1960
  els = await Promise.all(els.map(el => el.isVisible()))
1950
1961
  try {
1951
1962
  return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
@@ -1958,8 +1969,15 @@ class Playwright extends Helper {
1958
1969
  * {{> dontSeeElement }}
1959
1970
  *
1960
1971
  */
1961
- async dontSeeElement(locator) {
1962
- let els = await this._locate(locator)
1972
+ async dontSeeElement(locator, context = null) {
1973
+ let els
1974
+ if (context) {
1975
+ const contextEls = await this._locate(context)
1976
+ assertElementExists(contextEls, context, 'Context element')
1977
+ els = await findElements.call(this, contextEls[0], locator)
1978
+ } else {
1979
+ els = await this._locate(locator)
1980
+ }
1963
1981
  els = await Promise.all(els.map(el => el.isVisible()))
1964
1982
  try {
1965
1983
  return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
@@ -2245,8 +2263,8 @@ class Playwright extends Helper {
2245
2263
  * {{> fillField }}
2246
2264
  *
2247
2265
  */
2248
- async fillField(field, value) {
2249
- const els = await findFields.call(this, field)
2266
+ async fillField(field, value, context = null) {
2267
+ const els = await findFields.call(this, field, context)
2250
2268
  assertElementExists(els, field, 'Field')
2251
2269
  if (this.options.strict) assertOnlyOneElement(els, field)
2252
2270
  const el = els[0]
@@ -2262,24 +2280,10 @@ class Playwright extends Helper {
2262
2280
  }
2263
2281
 
2264
2282
  /**
2265
- * Clears the text input element: `<input>`, `<textarea>` or `[contenteditable]` .
2266
- *
2267
- *
2268
- * Examples:
2269
- *
2270
- * ```js
2271
- * I.clearField('.text-area')
2272
- *
2273
- * // if this doesn't work use force option
2274
- * I.clearField('#submit', { force: true })
2275
- * ```
2276
- * Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
2277
- *
2278
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
2279
- * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
2283
+ * {{> clearField }}
2280
2284
  */
2281
- async clearField(locator, options = {}) {
2282
- const els = await findFields.call(this, locator)
2285
+ async clearField(locator, context = null) {
2286
+ const els = await findFields.call(this, locator, context)
2283
2287
  assertElementExists(els, locator, 'Field to clear')
2284
2288
  if (this.options.strict) assertOnlyOneElement(els, locator)
2285
2289
 
@@ -2295,8 +2299,8 @@ class Playwright extends Helper {
2295
2299
  /**
2296
2300
  * {{> appendField }}
2297
2301
  */
2298
- async appendField(field, value) {
2299
- const els = await findFields.call(this, field)
2302
+ async appendField(field, value, context = null) {
2303
+ const els = await findFields.call(this, field, context)
2300
2304
  assertElementExists(els, field, 'Field')
2301
2305
  if (this.options.strict) assertOnlyOneElement(els, field)
2302
2306
  await highlightActiveElement.call(this, els[0])
@@ -2308,63 +2312,94 @@ class Playwright extends Helper {
2308
2312
  /**
2309
2313
  * {{> seeInField }}
2310
2314
  */
2311
- async seeInField(field, value) {
2315
+ async seeInField(field, value, context = null) {
2312
2316
  const _value = typeof value === 'boolean' ? value : value.toString()
2313
- return proceedSeeInField.call(this, 'assert', field, _value)
2317
+ return proceedSeeInField.call(this, 'assert', field, _value, context)
2314
2318
  }
2315
2319
 
2316
2320
  /**
2317
2321
  * {{> dontSeeInField }}
2318
2322
  */
2319
- async dontSeeInField(field, value) {
2323
+ async dontSeeInField(field, value, context = null) {
2320
2324
  const _value = typeof value === 'boolean' ? value : value.toString()
2321
- return proceedSeeInField.call(this, 'negate', field, _value)
2325
+ return proceedSeeInField.call(this, 'negate', field, _value, context)
2322
2326
  }
2323
2327
 
2324
2328
  /**
2325
2329
  * {{> attachFile }}
2326
2330
  *
2327
2331
  */
2328
- async attachFile(locator, pathToFile) {
2332
+ async attachFile(locator, pathToFile, context = null) {
2329
2333
  const file = path.join(global.codecept_dir, pathToFile)
2330
2334
 
2331
2335
  if (!fileExists(file)) {
2332
2336
  throw new Error(`File at ${file} can not be found on local system`)
2333
2337
  }
2334
- const els = await findFields.call(this, locator)
2335
- assertElementExists(els, locator, 'Field')
2336
- await els[0].setInputFiles(file)
2338
+ const els = await findFields.call(this, locator, context)
2339
+ if (els.length) {
2340
+ const tag = await els[0].evaluate(el => el.tagName)
2341
+ const type = await els[0].evaluate(el => el.type)
2342
+ if (tag === 'INPUT' && type === 'file') {
2343
+ await els[0].setInputFiles(file)
2344
+ return this._waitForAction()
2345
+ }
2346
+ }
2347
+
2348
+ const targetEls = els.length ? els : await this._locate(locator)
2349
+ assertElementExists(targetEls, locator, 'Element')
2350
+ const base64Content = base64EncodeFile(file)
2351
+ const fileName = path.basename(file)
2352
+ const mimeType = getMimeType(fileName)
2353
+ await targetEls[0].evaluate((el, { base64Content, fileName, mimeType }) => {
2354
+ const binaryStr = atob(base64Content)
2355
+ const bytes = new Uint8Array(binaryStr.length)
2356
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
2357
+ const fileObj = new File([bytes], fileName, { type: mimeType })
2358
+ const dataTransfer = new DataTransfer()
2359
+ dataTransfer.items.add(fileObj)
2360
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
2361
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
2362
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
2363
+ }, { base64Content, fileName, mimeType })
2337
2364
  return this._waitForAction()
2338
2365
  }
2339
2366
 
2340
2367
  /**
2341
2368
  * {{> selectOption }}
2342
2369
  */
2343
- async selectOption(select, option) {
2344
- const context = await this.context
2370
+ async selectOption(select, option, context = null) {
2371
+ const pageContext = await this.context
2345
2372
  const matchedLocator = new Locator(select)
2346
2373
 
2374
+ let contextEl
2375
+ if (context) {
2376
+ const contextEls = await this._locate(context)
2377
+ assertElementExists(contextEls, context, 'Context element')
2378
+ contextEl = contextEls[0]
2379
+ }
2380
+
2347
2381
  // Strict locator
2348
2382
  if (!matchedLocator.isFuzzy()) {
2349
2383
  this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
2350
- const els = await this._locate(matchedLocator)
2384
+ const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
2351
2385
  assertElementExists(els, select, 'Selectable element')
2352
- return proceedSelect.call(this, context, els[0], option)
2386
+ return proceedSelect.call(this, pageContext, els[0], option)
2353
2387
  }
2354
2388
 
2355
2389
  // Fuzzy: try combobox
2356
2390
  this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
2357
- let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value })
2358
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
2391
+ const comboboxSearchCtx = contextEl || pageContext
2392
+ let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
2393
+ if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
2359
2394
 
2360
2395
  // Fuzzy: try listbox
2361
- els = await findByRole(context, { role: 'listbox', name: matchedLocator.value })
2362
- if (els?.length) return proceedSelect.call(this, context, els[0], option)
2396
+ els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
2397
+ if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
2363
2398
 
2364
2399
  // Fuzzy: try native select
2365
- els = await findFields.call(this, select)
2400
+ els = await findFields.call(this, select, context)
2366
2401
  assertElementExists(els, select, 'Selectable element')
2367
- return proceedSelect.call(this, context, els[0], option)
2402
+ return proceedSelect.call(this, pageContext, els[0], option)
2368
2403
  }
2369
2404
 
2370
2405
  /**
@@ -2405,6 +2440,26 @@ class Playwright extends Helper {
2405
2440
  urlEquals(this.options.url).negate(url, await this._getPageUrl())
2406
2441
  }
2407
2442
 
2443
+ /**
2444
+ * {{> seeCurrentPathEquals }}
2445
+ */
2446
+ async seeCurrentPathEquals(path) {
2447
+ const currentUrl = await this._getPageUrl()
2448
+ const baseUrl = this.options.url || 'http://localhost'
2449
+ const actualPath = new URL(currentUrl, baseUrl).pathname
2450
+ return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
2451
+ }
2452
+
2453
+ /**
2454
+ * {{> dontSeeCurrentPathEquals }}
2455
+ */
2456
+ async dontSeeCurrentPathEquals(path) {
2457
+ const currentUrl = await this._getPageUrl()
2458
+ const baseUrl = this.options.url || 'http://localhost'
2459
+ const actualPath = new URL(currentUrl, baseUrl).pathname
2460
+ return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
2461
+ }
2462
+
2408
2463
  /**
2409
2464
  * {{> see }}
2410
2465
  *
@@ -3362,6 +3417,7 @@ class Playwright extends Helper {
3362
3417
  */
3363
3418
  async waitInUrl(urlPart, sec = null) {
3364
3419
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3420
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3365
3421
 
3366
3422
  return this.page
3367
3423
  .waitForFunction(
@@ -3369,13 +3425,13 @@ class Playwright extends Helper {
3369
3425
  const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
3370
3426
  return currUrl.indexOf(urlPart) > -1
3371
3427
  },
3372
- urlPart,
3428
+ expectedUrl,
3373
3429
  { timeout: waitTimeout },
3374
3430
  )
3375
3431
  .catch(async e => {
3376
- const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data.
3432
+ const currUrl = await this._getPageUrl()
3377
3433
  if (/Timeout/i.test(e.message)) {
3378
- throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`)
3434
+ throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
3379
3435
  } else {
3380
3436
  throw e
3381
3437
  }
@@ -3387,26 +3443,46 @@ class Playwright extends Helper {
3387
3443
  */
3388
3444
  async waitUrlEquals(urlPart, sec = null) {
3389
3445
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3390
-
3391
- const baseUrl = this.options.url
3392
- let expectedUrl = urlPart
3393
- if (urlPart.indexOf('http') < 0) {
3394
- expectedUrl = baseUrl + urlPart
3395
- }
3446
+ const expectedUrl = resolveUrl(urlPart, this.options.url)
3396
3447
 
3397
3448
  try {
3398
3449
  await this.page.waitForURL(
3399
- url => url.href.includes(expectedUrl),
3450
+ url => url.href === expectedUrl,
3400
3451
  { timeout: waitTimeout },
3401
3452
  )
3402
3453
  } catch (e) {
3403
3454
  const currUrl = await this._getPageUrl()
3404
3455
  if (/Timeout/i.test(e.message)) {
3405
- if (!currUrl.includes(expectedUrl)) {
3406
- throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3407
- } else {
3408
- throw new Error(`expected url not loaded, error message: ${e.message}`)
3409
- }
3456
+ throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
3457
+ } else {
3458
+ throw e
3459
+ }
3460
+ }
3461
+ }
3462
+
3463
+ /**
3464
+ * {{> waitCurrentPathEquals }}
3465
+ */
3466
+ async waitCurrentPathEquals(path, sec = null) {
3467
+ const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
3468
+ const normalizedPath = normalizePath(path)
3469
+
3470
+ try {
3471
+ await this.page.waitForFunction(
3472
+ expectedPath => {
3473
+ const actualPath = window.location.pathname
3474
+ const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
3475
+ return normalizePath(actualPath) === expectedPath
3476
+ },
3477
+ normalizedPath,
3478
+ { timeout: waitTimeout },
3479
+ )
3480
+ } catch (e) {
3481
+ const currentUrl = await this._getPageUrl()
3482
+ const baseUrl = this.options.url || 'http://localhost'
3483
+ const actualPath = new URL(currentUrl, baseUrl).pathname
3484
+ if (/Timeout/i.test(e.message)) {
3485
+ throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
3410
3486
  } else {
3411
3487
  throw e
3412
3488
  }
@@ -4092,9 +4168,15 @@ class Playwright extends Helper {
4092
4168
 
4093
4169
  export default Playwright
4094
4170
 
4095
- function buildLocatorString(locator) {
4171
+ export function buildLocatorString(locator) {
4096
4172
  if (locator.isXPath()) {
4097
- return `xpath=${locator.value}`
4173
+ // Make XPath relative so it works correctly within scoped contexts (e.g. within()).
4174
+ // Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
4175
+ // but only when the selector starts with "/". Locator methods like at() wrap XPath in
4176
+ // parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
4177
+ // We fix this by prepending "." before the first "//" that follows any leading parentheses.
4178
+ const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
4179
+ return `xpath=${value}`
4098
4180
  }
4099
4181
  if (locator.isShadow()) {
4100
4182
  // Convert shadow locator to CSS with >> chaining operator
@@ -4335,34 +4417,45 @@ async function proceedIsChecked(assertType, option) {
4335
4417
  return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
4336
4418
  }
4337
4419
 
4338
- async function findFields(locator) {
4420
+ async function findFields(locator, context = null) {
4421
+ let contextEl
4422
+ if (context) {
4423
+ const contextEls = await this._locate(context)
4424
+ assertElementExists(contextEls, context, 'Context element')
4425
+ contextEl = contextEls[0]
4426
+ }
4427
+
4428
+ const locateFn = contextEl
4429
+ ? loc => findElements.call(this, contextEl, loc)
4430
+ : loc => this._locate(loc)
4431
+
4339
4432
  // Handle role locators with text/exact options
4340
4433
  if (isRoleLocatorObject(locator)) {
4341
- const page = await this.page
4342
- const roleElements = await handleRoleLocator(page, locator)
4434
+ const matcher = contextEl || (await this.page)
4435
+ const roleElements = await handleRoleLocator(matcher, locator)
4343
4436
  if (roleElements) return roleElements
4344
4437
  }
4345
4438
 
4346
4439
  const matchedLocator = new Locator(locator)
4347
4440
  if (!matchedLocator.isFuzzy()) {
4348
- return this._locate(matchedLocator)
4441
+ return locateFn(matchedLocator)
4349
4442
  }
4350
4443
  const literal = xpathLocator.literal(locator)
4351
4444
 
4352
- let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
4445
+ let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
4353
4446
  if (els.length) {
4354
4447
  return els
4355
4448
  }
4356
4449
 
4357
- els = await this._locate({ xpath: Locator.field.labelContains(literal) })
4450
+ els = await locateFn({ xpath: Locator.field.labelContains(literal) })
4358
4451
  if (els.length) {
4359
4452
  return els
4360
4453
  }
4361
- els = await this._locate({ xpath: Locator.field.byName(literal) })
4454
+ els = await locateFn({ xpath: Locator.field.byName(literal) })
4362
4455
  if (els.length) {
4363
4456
  return els
4364
4457
  }
4365
- return this._locate({ css: locator })
4458
+ return locateFn({ css: locator })
4366
4459
  }
4367
4460
 
4368
4461
  async function proceedSelect(context, el, option) {
@@ -4411,8 +4504,8 @@ async function proceedSelect(context, el, option) {
4411
4504
  return this._waitForAction()
4412
4505
  }
4413
4506
 
4414
- async function proceedSeeInField(assertType, field, value) {
4415
- const els = await findFields.call(this, field)
4507
+ async function proceedSeeInField(assertType, field, value, context) {
4508
+ const els = await findFields.call(this, field, context)
4416
4509
  assertElementExists(els, field, 'Field')
4417
4510
  const el = els[0]
4418
4511
  const tag = await el.evaluate(e => e.tagName)