codeceptjs 4.0.0-rc.1 → 4.0.0-rc.10
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.
- package/README.md +39 -27
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/codecept.js +20 -17
- package/lib/command/init.js +0 -3
- package/lib/command/run-workers.js +1 -0
- package/lib/container.js +19 -4
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/Playwright.js +219 -137
- package/lib/helper/Puppeteer.js +207 -69
- package/lib/helper/WebDriver.js +179 -64
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/mocha/cli.js +10 -0
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +1 -1
- package/lib/utils.js +48 -0
- package/lib/workers.js +49 -7
- package/package.json +5 -3
- package/typings/index.d.ts +19 -0
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
package/lib/helper/Playwright.js
CHANGED
|
@@ -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'
|
|
@@ -32,7 +36,9 @@ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefu
|
|
|
32
36
|
import Popup from './extras/Popup.js'
|
|
33
37
|
import Console from './extras/Console.js'
|
|
34
38
|
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
39
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
35
40
|
import WebElement from '../element/WebElement.js'
|
|
41
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
36
42
|
|
|
37
43
|
let playwright
|
|
38
44
|
let perfTiming
|
|
@@ -1490,8 +1496,23 @@ class Playwright extends Helper {
|
|
|
1490
1496
|
*
|
|
1491
1497
|
*/
|
|
1492
1498
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1499
|
+
let context = null
|
|
1500
|
+
if (typeof offsetX !== 'number') {
|
|
1501
|
+
context = offsetX
|
|
1502
|
+
offsetX = 0
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
let el
|
|
1506
|
+
if (context) {
|
|
1507
|
+
const contextEls = await this._locate(context)
|
|
1508
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1509
|
+
el = await findElements.call(this, contextEls[0], locator)
|
|
1510
|
+
assertElementExists(el, locator)
|
|
1511
|
+
el = el[0]
|
|
1512
|
+
} else {
|
|
1513
|
+
el = await this._locateElement(locator)
|
|
1514
|
+
assertElementExists(el, locator)
|
|
1515
|
+
}
|
|
1495
1516
|
|
|
1496
1517
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1497
1518
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1759,8 +1780,7 @@ class Playwright extends Helper {
|
|
|
1759
1780
|
if (elements.length === 0) {
|
|
1760
1781
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1761
1782
|
}
|
|
1762
|
-
|
|
1763
|
-
return elements[0]
|
|
1783
|
+
return selectElement(elements, locator, this)
|
|
1764
1784
|
}
|
|
1765
1785
|
|
|
1766
1786
|
/**
|
|
@@ -1775,8 +1795,7 @@ class Playwright extends Helper {
|
|
|
1775
1795
|
const context = providedContext || (await this._getContext())
|
|
1776
1796
|
const els = await findCheckable.call(this, locator, context)
|
|
1777
1797
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1778
|
-
|
|
1779
|
-
return els[0]
|
|
1798
|
+
return selectElement(els, locator, this)
|
|
1780
1799
|
}
|
|
1781
1800
|
|
|
1782
1801
|
/**
|
|
@@ -1944,8 +1963,15 @@ class Playwright extends Helper {
|
|
|
1944
1963
|
* {{> seeElement }}
|
|
1945
1964
|
*
|
|
1946
1965
|
*/
|
|
1947
|
-
async seeElement(locator) {
|
|
1948
|
-
let els
|
|
1966
|
+
async seeElement(locator, context = null) {
|
|
1967
|
+
let els
|
|
1968
|
+
if (context) {
|
|
1969
|
+
const contextEls = await this._locate(context)
|
|
1970
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1971
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1972
|
+
} else {
|
|
1973
|
+
els = await this._locate(locator)
|
|
1974
|
+
}
|
|
1949
1975
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1950
1976
|
try {
|
|
1951
1977
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -1958,8 +1984,15 @@ class Playwright extends Helper {
|
|
|
1958
1984
|
* {{> dontSeeElement }}
|
|
1959
1985
|
*
|
|
1960
1986
|
*/
|
|
1961
|
-
async dontSeeElement(locator) {
|
|
1962
|
-
let els
|
|
1987
|
+
async dontSeeElement(locator, context = null) {
|
|
1988
|
+
let els
|
|
1989
|
+
if (context) {
|
|
1990
|
+
const contextEls = await this._locate(context)
|
|
1991
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1992
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1993
|
+
} else {
|
|
1994
|
+
els = await this._locate(locator)
|
|
1995
|
+
}
|
|
1963
1996
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1964
1997
|
try {
|
|
1965
1998
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2245,11 +2278,10 @@ class Playwright extends Helper {
|
|
|
2245
2278
|
* {{> fillField }}
|
|
2246
2279
|
*
|
|
2247
2280
|
*/
|
|
2248
|
-
async fillField(field, value) {
|
|
2249
|
-
const els = await findFields.call(this, field)
|
|
2281
|
+
async fillField(field, value, context = null) {
|
|
2282
|
+
const els = await findFields.call(this, field, context)
|
|
2250
2283
|
assertElementExists(els, field, 'Field')
|
|
2251
|
-
|
|
2252
|
-
const el = els[0]
|
|
2284
|
+
const el = selectElement(els, field, this)
|
|
2253
2285
|
|
|
2254
2286
|
await el.clear()
|
|
2255
2287
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
@@ -2262,28 +2294,13 @@ class Playwright extends Helper {
|
|
|
2262
2294
|
}
|
|
2263
2295
|
|
|
2264
2296
|
/**
|
|
2265
|
-
*
|
|
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.
|
|
2297
|
+
* {{> clearField }}
|
|
2280
2298
|
*/
|
|
2281
|
-
async clearField(locator,
|
|
2282
|
-
const els = await findFields.call(this, locator)
|
|
2299
|
+
async clearField(locator, context = null) {
|
|
2300
|
+
const els = await findFields.call(this, locator, context)
|
|
2283
2301
|
assertElementExists(els, locator, 'Field to clear')
|
|
2284
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
2285
2302
|
|
|
2286
|
-
const el = els
|
|
2303
|
+
const el = selectElement(els, locator, this)
|
|
2287
2304
|
|
|
2288
2305
|
await highlightActiveElement.call(this, el)
|
|
2289
2306
|
|
|
@@ -2295,76 +2312,101 @@ class Playwright extends Helper {
|
|
|
2295
2312
|
/**
|
|
2296
2313
|
* {{> appendField }}
|
|
2297
2314
|
*/
|
|
2298
|
-
async appendField(field, value) {
|
|
2299
|
-
const els = await findFields.call(this, field)
|
|
2315
|
+
async appendField(field, value, context = null) {
|
|
2316
|
+
const els = await findFields.call(this, field, context)
|
|
2300
2317
|
assertElementExists(els, field, 'Field')
|
|
2301
|
-
|
|
2302
|
-
await highlightActiveElement.call(this,
|
|
2303
|
-
await
|
|
2304
|
-
await
|
|
2318
|
+
const el = selectElement(els, field, this)
|
|
2319
|
+
await highlightActiveElement.call(this, el)
|
|
2320
|
+
await el.press('End')
|
|
2321
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2305
2322
|
return this._waitForAction()
|
|
2306
2323
|
}
|
|
2307
2324
|
|
|
2308
2325
|
/**
|
|
2309
2326
|
* {{> seeInField }}
|
|
2310
2327
|
*/
|
|
2311
|
-
async seeInField(field, value) {
|
|
2328
|
+
async seeInField(field, value, context = null) {
|
|
2312
2329
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2313
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2330
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2314
2331
|
}
|
|
2315
2332
|
|
|
2316
2333
|
/**
|
|
2317
2334
|
* {{> dontSeeInField }}
|
|
2318
2335
|
*/
|
|
2319
|
-
async dontSeeInField(field, value) {
|
|
2336
|
+
async dontSeeInField(field, value, context = null) {
|
|
2320
2337
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2321
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2338
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2322
2339
|
}
|
|
2323
2340
|
|
|
2324
2341
|
/**
|
|
2325
2342
|
* {{> attachFile }}
|
|
2326
2343
|
*
|
|
2327
2344
|
*/
|
|
2328
|
-
async attachFile(locator, pathToFile) {
|
|
2345
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2329
2346
|
const file = path.join(global.codecept_dir, pathToFile)
|
|
2330
2347
|
|
|
2331
2348
|
if (!fileExists(file)) {
|
|
2332
2349
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2333
2350
|
}
|
|
2334
|
-
const els = await findFields.call(this, locator)
|
|
2335
|
-
|
|
2336
|
-
|
|
2351
|
+
const els = await findFields.call(this, locator, context)
|
|
2352
|
+
if (els.length) {
|
|
2353
|
+
const el = selectElement(els, locator, this)
|
|
2354
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2355
|
+
const type = await el.evaluate(el => el.type)
|
|
2356
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2357
|
+
await el.setInputFiles(file)
|
|
2358
|
+
return this._waitForAction()
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2363
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2364
|
+
const el = selectElement(targetEls, locator, this)
|
|
2365
|
+
const fileData = {
|
|
2366
|
+
base64Content: base64EncodeFile(file),
|
|
2367
|
+
fileName: path.basename(file),
|
|
2368
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2369
|
+
}
|
|
2370
|
+
await el.evaluate(dropFile, fileData)
|
|
2337
2371
|
return this._waitForAction()
|
|
2338
2372
|
}
|
|
2339
2373
|
|
|
2340
2374
|
/**
|
|
2341
2375
|
* {{> selectOption }}
|
|
2342
2376
|
*/
|
|
2343
|
-
async selectOption(select, option) {
|
|
2344
|
-
const
|
|
2377
|
+
async selectOption(select, option, context = null) {
|
|
2378
|
+
const pageContext = await this.context
|
|
2345
2379
|
const matchedLocator = new Locator(select)
|
|
2346
2380
|
|
|
2381
|
+
let contextEl
|
|
2382
|
+
if (context) {
|
|
2383
|
+
const contextEls = await this._locate(context)
|
|
2384
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2385
|
+
contextEl = contextEls[0]
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2347
2388
|
// Strict locator
|
|
2348
2389
|
if (!matchedLocator.isFuzzy()) {
|
|
2349
2390
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2350
|
-
const els = await this._locate(matchedLocator)
|
|
2391
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2351
2392
|
assertElementExists(els, select, 'Selectable element')
|
|
2352
|
-
return proceedSelect.call(this,
|
|
2393
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2353
2394
|
}
|
|
2354
2395
|
|
|
2355
2396
|
// Fuzzy: try combobox
|
|
2356
2397
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2357
|
-
|
|
2358
|
-
|
|
2398
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2399
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2400
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2359
2401
|
|
|
2360
2402
|
// Fuzzy: try listbox
|
|
2361
|
-
els = await findByRole(
|
|
2362
|
-
if (els?.length) return proceedSelect.call(this,
|
|
2403
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2404
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2363
2405
|
|
|
2364
2406
|
// Fuzzy: try native select
|
|
2365
|
-
els = await findFields.call(this, select)
|
|
2407
|
+
els = await findFields.call(this, select, context)
|
|
2366
2408
|
assertElementExists(els, select, 'Selectable element')
|
|
2367
|
-
return proceedSelect.call(this,
|
|
2409
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2368
2410
|
}
|
|
2369
2411
|
|
|
2370
2412
|
/**
|
|
@@ -2405,6 +2447,26 @@ class Playwright extends Helper {
|
|
|
2405
2447
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
2406
2448
|
}
|
|
2407
2449
|
|
|
2450
|
+
/**
|
|
2451
|
+
* {{> seeCurrentPathEquals }}
|
|
2452
|
+
*/
|
|
2453
|
+
async seeCurrentPathEquals(path) {
|
|
2454
|
+
const currentUrl = await this._getPageUrl()
|
|
2455
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2456
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2457
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/**
|
|
2461
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
2462
|
+
*/
|
|
2463
|
+
async dontSeeCurrentPathEquals(path) {
|
|
2464
|
+
const currentUrl = await this._getPageUrl()
|
|
2465
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2466
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2467
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2408
2470
|
/**
|
|
2409
2471
|
* {{> see }}
|
|
2410
2472
|
*
|
|
@@ -2638,15 +2700,12 @@ class Playwright extends Helper {
|
|
|
2638
2700
|
*
|
|
2639
2701
|
*/
|
|
2640
2702
|
async grabTextFrom(locator) {
|
|
2641
|
-
|
|
2642
|
-
if (
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
this.debugSection('Text', text)
|
|
2648
|
-
return text
|
|
2649
|
-
}
|
|
2703
|
+
const roleElements = await handleRoleLocator(this.page, locator)
|
|
2704
|
+
if (roleElements && roleElements.length > 0) {
|
|
2705
|
+
const text = await roleElements[0].textContent()
|
|
2706
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2707
|
+
this.debugSection('Text', text)
|
|
2708
|
+
return text
|
|
2650
2709
|
}
|
|
2651
2710
|
|
|
2652
2711
|
const locatorObj = new Locator(locator, 'css')
|
|
@@ -3362,6 +3421,7 @@ class Playwright extends Helper {
|
|
|
3362
3421
|
*/
|
|
3363
3422
|
async waitInUrl(urlPart, sec = null) {
|
|
3364
3423
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3424
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3365
3425
|
|
|
3366
3426
|
return this.page
|
|
3367
3427
|
.waitForFunction(
|
|
@@ -3369,13 +3429,13 @@ class Playwright extends Helper {
|
|
|
3369
3429
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3370
3430
|
return currUrl.indexOf(urlPart) > -1
|
|
3371
3431
|
},
|
|
3372
|
-
|
|
3432
|
+
expectedUrl,
|
|
3373
3433
|
{ timeout: waitTimeout },
|
|
3374
3434
|
)
|
|
3375
3435
|
.catch(async e => {
|
|
3376
|
-
const currUrl = await this._getPageUrl()
|
|
3436
|
+
const currUrl = await this._getPageUrl()
|
|
3377
3437
|
if (/Timeout/i.test(e.message)) {
|
|
3378
|
-
throw new Error(`expected url to include ${
|
|
3438
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3379
3439
|
} else {
|
|
3380
3440
|
throw e
|
|
3381
3441
|
}
|
|
@@ -3387,26 +3447,46 @@ class Playwright extends Helper {
|
|
|
3387
3447
|
*/
|
|
3388
3448
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3389
3449
|
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
|
-
}
|
|
3450
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3396
3451
|
|
|
3397
3452
|
try {
|
|
3398
3453
|
await this.page.waitForURL(
|
|
3399
|
-
url => url.href
|
|
3454
|
+
url => url.href === expectedUrl,
|
|
3400
3455
|
{ timeout: waitTimeout },
|
|
3401
3456
|
)
|
|
3402
3457
|
} catch (e) {
|
|
3403
3458
|
const currUrl = await this._getPageUrl()
|
|
3404
3459
|
if (/Timeout/i.test(e.message)) {
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3460
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
3461
|
+
} else {
|
|
3462
|
+
throw e
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
/**
|
|
3468
|
+
* {{> waitCurrentPathEquals }}
|
|
3469
|
+
*/
|
|
3470
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
3471
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3472
|
+
const normalizedPath = normalizePath(path)
|
|
3473
|
+
|
|
3474
|
+
try {
|
|
3475
|
+
await this.page.waitForFunction(
|
|
3476
|
+
expectedPath => {
|
|
3477
|
+
const actualPath = window.location.pathname
|
|
3478
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
3479
|
+
return normalizePath(actualPath) === expectedPath
|
|
3480
|
+
},
|
|
3481
|
+
normalizedPath,
|
|
3482
|
+
{ timeout: waitTimeout },
|
|
3483
|
+
)
|
|
3484
|
+
} catch (e) {
|
|
3485
|
+
const currentUrl = await this._getPageUrl()
|
|
3486
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
3487
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
3488
|
+
if (/Timeout/i.test(e.message)) {
|
|
3489
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
3410
3490
|
} else {
|
|
3411
3491
|
throw e
|
|
3412
3492
|
}
|
|
@@ -4092,9 +4172,15 @@ class Playwright extends Helper {
|
|
|
4092
4172
|
|
|
4093
4173
|
export default Playwright
|
|
4094
4174
|
|
|
4095
|
-
function buildLocatorString(locator) {
|
|
4175
|
+
export function buildLocatorString(locator) {
|
|
4096
4176
|
if (locator.isXPath()) {
|
|
4097
|
-
|
|
4177
|
+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
|
|
4178
|
+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
|
|
4179
|
+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
|
|
4180
|
+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
|
|
4181
|
+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
|
|
4182
|
+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
|
|
4183
|
+
return `xpath=${value}`
|
|
4098
4184
|
}
|
|
4099
4185
|
if (locator.isShadow()) {
|
|
4100
4186
|
// Convert shadow locator to CSS with >> chaining operator
|
|
@@ -4105,25 +4191,22 @@ function buildLocatorString(locator) {
|
|
|
4105
4191
|
return locator.simplify()
|
|
4106
4192
|
}
|
|
4107
4193
|
|
|
4108
|
-
/**
|
|
4109
|
-
* Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4110
|
-
*/
|
|
4111
|
-
function isRoleLocatorObject(locator) {
|
|
4112
|
-
return locator && typeof locator === 'object' && locator.role && !locator.type
|
|
4113
|
-
}
|
|
4114
|
-
|
|
4115
4194
|
/**
|
|
4116
4195
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4196
|
+
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
4117
4197
|
* Returns elements array if role locator, null otherwise
|
|
4118
4198
|
*/
|
|
4119
4199
|
async function handleRoleLocator(context, locator) {
|
|
4120
|
-
|
|
4200
|
+
const loc = new Locator(locator)
|
|
4201
|
+
if (!loc.isRole()) return null
|
|
4121
4202
|
|
|
4203
|
+
const roleObj = loc.locator || {}
|
|
4122
4204
|
const options = {}
|
|
4123
|
-
if (
|
|
4124
|
-
if (
|
|
4205
|
+
if (roleObj.text) options.name = roleObj.text
|
|
4206
|
+
if (roleObj.name) options.name = roleObj.name
|
|
4207
|
+
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4125
4208
|
|
|
4126
|
-
return context.getByRole(
|
|
4209
|
+
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4127
4210
|
}
|
|
4128
4211
|
|
|
4129
4212
|
async function findByRole(context, locator) {
|
|
@@ -4192,16 +4275,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4192
4275
|
assertElementExists(els, locator, 'Clickable element')
|
|
4193
4276
|
}
|
|
4194
4277
|
|
|
4195
|
-
|
|
4196
|
-
|
|
4278
|
+
const opts = store.currentStep?.opts
|
|
4279
|
+
let element
|
|
4280
|
+
if (opts?.elementIndex != null) {
|
|
4281
|
+
element = selectElement(els, locator, this)
|
|
4282
|
+
} else {
|
|
4283
|
+
const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
|
|
4284
|
+
if (strict) assertOnlyOneElement(els, locator, this)
|
|
4285
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
await highlightActiveElement.call(this, element)
|
|
4289
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4197
4290
|
|
|
4198
|
-
/*
|
|
4199
|
-
using the force true options itself but instead dispatching a click
|
|
4200
|
-
*/
|
|
4201
4291
|
if (options.force) {
|
|
4202
|
-
await
|
|
4292
|
+
await element.dispatchEvent('click')
|
|
4203
4293
|
} else {
|
|
4204
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4205
4294
|
await element.click(options)
|
|
4206
4295
|
}
|
|
4207
4296
|
const promises = []
|
|
@@ -4218,7 +4307,6 @@ async function findClickable(matcher, locator) {
|
|
|
4218
4307
|
|
|
4219
4308
|
if (!matchedLocator.isFuzzy()) {
|
|
4220
4309
|
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4221
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4222
4310
|
return els
|
|
4223
4311
|
}
|
|
4224
4312
|
|
|
@@ -4227,42 +4315,27 @@ async function findClickable(matcher, locator) {
|
|
|
4227
4315
|
|
|
4228
4316
|
try {
|
|
4229
4317
|
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4230
|
-
if (els.length)
|
|
4231
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4232
|
-
return els
|
|
4233
|
-
}
|
|
4318
|
+
if (els.length) return els
|
|
4234
4319
|
} catch (err) {
|
|
4235
4320
|
// getByRole not supported or failed
|
|
4236
4321
|
}
|
|
4237
4322
|
|
|
4238
4323
|
try {
|
|
4239
4324
|
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4240
|
-
if (els.length)
|
|
4241
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4242
|
-
return els
|
|
4243
|
-
}
|
|
4325
|
+
if (els.length) return els
|
|
4244
4326
|
} catch (err) {
|
|
4245
4327
|
// getByRole not supported or failed
|
|
4246
4328
|
}
|
|
4247
4329
|
|
|
4248
4330
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
4249
|
-
if (els.length)
|
|
4250
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4251
|
-
return els
|
|
4252
|
-
}
|
|
4331
|
+
if (els.length) return els
|
|
4253
4332
|
|
|
4254
4333
|
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
4255
|
-
if (els.length)
|
|
4256
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4257
|
-
return els
|
|
4258
|
-
}
|
|
4334
|
+
if (els.length) return els
|
|
4259
4335
|
|
|
4260
4336
|
try {
|
|
4261
4337
|
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
4262
|
-
if (els.length)
|
|
4263
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4264
|
-
return els
|
|
4265
|
-
}
|
|
4338
|
+
if (els.length) return els
|
|
4266
4339
|
} catch (err) {
|
|
4267
4340
|
// Do nothing
|
|
4268
4341
|
}
|
|
@@ -4335,34 +4408,42 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4335
4408
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4336
4409
|
}
|
|
4337
4410
|
|
|
4338
|
-
async function findFields(locator) {
|
|
4339
|
-
|
|
4340
|
-
if (
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4411
|
+
async function findFields(locator, context = null) {
|
|
4412
|
+
let contextEl
|
|
4413
|
+
if (context) {
|
|
4414
|
+
const contextEls = await this._locate(context)
|
|
4415
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4416
|
+
contextEl = contextEls[0]
|
|
4344
4417
|
}
|
|
4345
4418
|
|
|
4419
|
+
const locateFn = contextEl
|
|
4420
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4421
|
+
: loc => this._locate(loc)
|
|
4422
|
+
|
|
4423
|
+
const matcher = contextEl || (await this.page)
|
|
4424
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4425
|
+
if (roleElements) return roleElements
|
|
4426
|
+
|
|
4346
4427
|
const matchedLocator = new Locator(locator)
|
|
4347
4428
|
if (!matchedLocator.isFuzzy()) {
|
|
4348
|
-
return
|
|
4429
|
+
return locateFn(matchedLocator)
|
|
4349
4430
|
}
|
|
4350
4431
|
const literal = xpathLocator.literal(locator)
|
|
4351
4432
|
|
|
4352
|
-
let els = await
|
|
4433
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4353
4434
|
if (els.length) {
|
|
4354
4435
|
return els
|
|
4355
4436
|
}
|
|
4356
4437
|
|
|
4357
|
-
els = await
|
|
4438
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4358
4439
|
if (els.length) {
|
|
4359
4440
|
return els
|
|
4360
4441
|
}
|
|
4361
|
-
els = await
|
|
4442
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4362
4443
|
if (els.length) {
|
|
4363
4444
|
return els
|
|
4364
4445
|
}
|
|
4365
|
-
return
|
|
4446
|
+
return locateFn({ css: locator })
|
|
4366
4447
|
}
|
|
4367
4448
|
|
|
4368
4449
|
async function proceedSelect(context, el, option) {
|
|
@@ -4411,8 +4492,8 @@ async function proceedSelect(context, el, option) {
|
|
|
4411
4492
|
return this._waitForAction()
|
|
4412
4493
|
}
|
|
4413
4494
|
|
|
4414
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
4415
|
-
const els = await findFields.call(this, field)
|
|
4495
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4496
|
+
const els = await findFields.call(this, field, context)
|
|
4416
4497
|
assertElementExists(els, field, 'Field')
|
|
4417
4498
|
const el = els[0]
|
|
4418
4499
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4526,9 +4607,10 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4526
4607
|
}
|
|
4527
4608
|
}
|
|
4528
4609
|
|
|
4529
|
-
function assertOnlyOneElement(elements, locator) {
|
|
4610
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4530
4611
|
if (elements.length > 1) {
|
|
4531
|
-
|
|
4612
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4613
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4532
4614
|
}
|
|
4533
4615
|
}
|
|
4534
4616
|
|