codeceptjs 4.0.0-rc.2 → 4.0.0-rc.8
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/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/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 +52 -0
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/Playwright.js +169 -87
- package/lib/helper/Puppeteer.js +181 -64
- package/lib/helper/WebDriver.js +141 -53
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- 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/utils.js +48 -0
- package/lib/workers.js +49 -7
- package/package.json +5 -3
- 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,6 +36,7 @@ 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'
|
|
36
41
|
|
|
37
42
|
let playwright
|
|
@@ -1490,8 +1495,23 @@ class Playwright extends Helper {
|
|
|
1490
1495
|
*
|
|
1491
1496
|
*/
|
|
1492
1497
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1498
|
+
let context = null
|
|
1499
|
+
if (typeof offsetX !== 'number') {
|
|
1500
|
+
context = offsetX
|
|
1501
|
+
offsetX = 0
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
let el
|
|
1505
|
+
if (context) {
|
|
1506
|
+
const contextEls = await this._locate(context)
|
|
1507
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1508
|
+
el = await findElements.call(this, contextEls[0], locator)
|
|
1509
|
+
assertElementExists(el, locator)
|
|
1510
|
+
el = el[0]
|
|
1511
|
+
} else {
|
|
1512
|
+
el = await this._locateElement(locator)
|
|
1513
|
+
assertElementExists(el, locator)
|
|
1514
|
+
}
|
|
1495
1515
|
|
|
1496
1516
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1497
1517
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1759,7 +1779,7 @@ class Playwright extends Helper {
|
|
|
1759
1779
|
if (elements.length === 0) {
|
|
1760
1780
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1761
1781
|
}
|
|
1762
|
-
if (this.options.strict) assertOnlyOneElement(elements, locator)
|
|
1782
|
+
if (this.options.strict) assertOnlyOneElement(elements, locator, this)
|
|
1763
1783
|
return elements[0]
|
|
1764
1784
|
}
|
|
1765
1785
|
|
|
@@ -1775,7 +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
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
1798
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
1779
1799
|
return els[0]
|
|
1780
1800
|
}
|
|
1781
1801
|
|
|
@@ -1944,8 +1964,15 @@ class Playwright extends Helper {
|
|
|
1944
1964
|
* {{> seeElement }}
|
|
1945
1965
|
*
|
|
1946
1966
|
*/
|
|
1947
|
-
async seeElement(locator) {
|
|
1948
|
-
let els
|
|
1967
|
+
async seeElement(locator, context = null) {
|
|
1968
|
+
let els
|
|
1969
|
+
if (context) {
|
|
1970
|
+
const contextEls = await this._locate(context)
|
|
1971
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1972
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1973
|
+
} else {
|
|
1974
|
+
els = await this._locate(locator)
|
|
1975
|
+
}
|
|
1949
1976
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1950
1977
|
try {
|
|
1951
1978
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -1958,8 +1985,15 @@ class Playwright extends Helper {
|
|
|
1958
1985
|
* {{> dontSeeElement }}
|
|
1959
1986
|
*
|
|
1960
1987
|
*/
|
|
1961
|
-
async dontSeeElement(locator) {
|
|
1962
|
-
let els
|
|
1988
|
+
async dontSeeElement(locator, context = null) {
|
|
1989
|
+
let els
|
|
1990
|
+
if (context) {
|
|
1991
|
+
const contextEls = await this._locate(context)
|
|
1992
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1993
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1994
|
+
} else {
|
|
1995
|
+
els = await this._locate(locator)
|
|
1996
|
+
}
|
|
1963
1997
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1964
1998
|
try {
|
|
1965
1999
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2245,10 +2279,10 @@ class Playwright extends Helper {
|
|
|
2245
2279
|
* {{> fillField }}
|
|
2246
2280
|
*
|
|
2247
2281
|
*/
|
|
2248
|
-
async fillField(field, value) {
|
|
2249
|
-
const els = await findFields.call(this, field)
|
|
2282
|
+
async fillField(field, value, context = null) {
|
|
2283
|
+
const els = await findFields.call(this, field, context)
|
|
2250
2284
|
assertElementExists(els, field, 'Field')
|
|
2251
|
-
if (this.options.strict) assertOnlyOneElement(els, field)
|
|
2285
|
+
if (this.options.strict) assertOnlyOneElement(els, field, this)
|
|
2252
2286
|
const el = els[0]
|
|
2253
2287
|
|
|
2254
2288
|
await el.clear()
|
|
@@ -2262,26 +2296,12 @@ class Playwright extends Helper {
|
|
|
2262
2296
|
}
|
|
2263
2297
|
|
|
2264
2298
|
/**
|
|
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.
|
|
2299
|
+
* {{> clearField }}
|
|
2280
2300
|
*/
|
|
2281
|
-
async clearField(locator,
|
|
2282
|
-
const els = await findFields.call(this, locator)
|
|
2301
|
+
async clearField(locator, context = null) {
|
|
2302
|
+
const els = await findFields.call(this, locator, context)
|
|
2283
2303
|
assertElementExists(els, locator, 'Field to clear')
|
|
2284
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
2304
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
2285
2305
|
|
|
2286
2306
|
const el = els[0]
|
|
2287
2307
|
|
|
@@ -2295,10 +2315,10 @@ class Playwright extends Helper {
|
|
|
2295
2315
|
/**
|
|
2296
2316
|
* {{> appendField }}
|
|
2297
2317
|
*/
|
|
2298
|
-
async appendField(field, value) {
|
|
2299
|
-
const els = await findFields.call(this, field)
|
|
2318
|
+
async appendField(field, value, context = null) {
|
|
2319
|
+
const els = await findFields.call(this, field, context)
|
|
2300
2320
|
assertElementExists(els, field, 'Field')
|
|
2301
|
-
if (this.options.strict) assertOnlyOneElement(els, field)
|
|
2321
|
+
if (this.options.strict) assertOnlyOneElement(els, field, this)
|
|
2302
2322
|
await highlightActiveElement.call(this, els[0])
|
|
2303
2323
|
await els[0].press('End')
|
|
2304
2324
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
@@ -2308,63 +2328,86 @@ class Playwright extends Helper {
|
|
|
2308
2328
|
/**
|
|
2309
2329
|
* {{> seeInField }}
|
|
2310
2330
|
*/
|
|
2311
|
-
async seeInField(field, value) {
|
|
2331
|
+
async seeInField(field, value, context = null) {
|
|
2312
2332
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2313
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2333
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2314
2334
|
}
|
|
2315
2335
|
|
|
2316
2336
|
/**
|
|
2317
2337
|
* {{> dontSeeInField }}
|
|
2318
2338
|
*/
|
|
2319
|
-
async dontSeeInField(field, value) {
|
|
2339
|
+
async dontSeeInField(field, value, context = null) {
|
|
2320
2340
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2321
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2341
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2322
2342
|
}
|
|
2323
2343
|
|
|
2324
2344
|
/**
|
|
2325
2345
|
* {{> attachFile }}
|
|
2326
2346
|
*
|
|
2327
2347
|
*/
|
|
2328
|
-
async attachFile(locator, pathToFile) {
|
|
2348
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2329
2349
|
const file = path.join(global.codecept_dir, pathToFile)
|
|
2330
2350
|
|
|
2331
2351
|
if (!fileExists(file)) {
|
|
2332
2352
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2333
2353
|
}
|
|
2334
|
-
const els = await findFields.call(this, locator)
|
|
2335
|
-
|
|
2336
|
-
|
|
2354
|
+
const els = await findFields.call(this, locator, context)
|
|
2355
|
+
if (els.length) {
|
|
2356
|
+
const tag = await els[0].evaluate(el => el.tagName)
|
|
2357
|
+
const type = await els[0].evaluate(el => el.type)
|
|
2358
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2359
|
+
await els[0].setInputFiles(file)
|
|
2360
|
+
return this._waitForAction()
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2365
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2366
|
+
const fileData = {
|
|
2367
|
+
base64Content: base64EncodeFile(file),
|
|
2368
|
+
fileName: path.basename(file),
|
|
2369
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2370
|
+
}
|
|
2371
|
+
await targetEls[0].evaluate(dropFile, fileData)
|
|
2337
2372
|
return this._waitForAction()
|
|
2338
2373
|
}
|
|
2339
2374
|
|
|
2340
2375
|
/**
|
|
2341
2376
|
* {{> selectOption }}
|
|
2342
2377
|
*/
|
|
2343
|
-
async selectOption(select, option) {
|
|
2344
|
-
const
|
|
2378
|
+
async selectOption(select, option, context = null) {
|
|
2379
|
+
const pageContext = await this.context
|
|
2345
2380
|
const matchedLocator = new Locator(select)
|
|
2346
2381
|
|
|
2382
|
+
let contextEl
|
|
2383
|
+
if (context) {
|
|
2384
|
+
const contextEls = await this._locate(context)
|
|
2385
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2386
|
+
contextEl = contextEls[0]
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2347
2389
|
// Strict locator
|
|
2348
2390
|
if (!matchedLocator.isFuzzy()) {
|
|
2349
2391
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2350
|
-
const els = await this._locate(matchedLocator)
|
|
2392
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2351
2393
|
assertElementExists(els, select, 'Selectable element')
|
|
2352
|
-
return proceedSelect.call(this,
|
|
2394
|
+
return proceedSelect.call(this, pageContext, els[0], option)
|
|
2353
2395
|
}
|
|
2354
2396
|
|
|
2355
2397
|
// Fuzzy: try combobox
|
|
2356
2398
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2357
|
-
|
|
2358
|
-
|
|
2399
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2400
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2401
|
+
if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
|
|
2359
2402
|
|
|
2360
2403
|
// Fuzzy: try listbox
|
|
2361
|
-
els = await findByRole(
|
|
2362
|
-
if (els?.length) return proceedSelect.call(this,
|
|
2404
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2405
|
+
if (els?.length) return proceedSelect.call(this, pageContext, els[0], option)
|
|
2363
2406
|
|
|
2364
2407
|
// Fuzzy: try native select
|
|
2365
|
-
els = await findFields.call(this, select)
|
|
2408
|
+
els = await findFields.call(this, select, context)
|
|
2366
2409
|
assertElementExists(els, select, 'Selectable element')
|
|
2367
|
-
return proceedSelect.call(this,
|
|
2410
|
+
return proceedSelect.call(this, pageContext, els[0], option)
|
|
2368
2411
|
}
|
|
2369
2412
|
|
|
2370
2413
|
/**
|
|
@@ -2412,7 +2455,7 @@ class Playwright extends Helper {
|
|
|
2412
2455
|
const currentUrl = await this._getPageUrl()
|
|
2413
2456
|
const baseUrl = this.options.url || 'http://localhost'
|
|
2414
2457
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2415
|
-
return equals('url path').assert(path, actualPath)
|
|
2458
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2416
2459
|
}
|
|
2417
2460
|
|
|
2418
2461
|
/**
|
|
@@ -2422,7 +2465,7 @@ class Playwright extends Helper {
|
|
|
2422
2465
|
const currentUrl = await this._getPageUrl()
|
|
2423
2466
|
const baseUrl = this.options.url || 'http://localhost'
|
|
2424
2467
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2425
|
-
return equals('url path').negate(path, actualPath)
|
|
2468
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2426
2469
|
}
|
|
2427
2470
|
|
|
2428
2471
|
/**
|
|
@@ -3382,6 +3425,7 @@ class Playwright extends Helper {
|
|
|
3382
3425
|
*/
|
|
3383
3426
|
async waitInUrl(urlPart, sec = null) {
|
|
3384
3427
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3428
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3385
3429
|
|
|
3386
3430
|
return this.page
|
|
3387
3431
|
.waitForFunction(
|
|
@@ -3389,13 +3433,13 @@ class Playwright extends Helper {
|
|
|
3389
3433
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3390
3434
|
return currUrl.indexOf(urlPart) > -1
|
|
3391
3435
|
},
|
|
3392
|
-
|
|
3436
|
+
expectedUrl,
|
|
3393
3437
|
{ timeout: waitTimeout },
|
|
3394
3438
|
)
|
|
3395
3439
|
.catch(async e => {
|
|
3396
|
-
const currUrl = await this._getPageUrl()
|
|
3440
|
+
const currUrl = await this._getPageUrl()
|
|
3397
3441
|
if (/Timeout/i.test(e.message)) {
|
|
3398
|
-
throw new Error(`expected url to include ${
|
|
3442
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3399
3443
|
} else {
|
|
3400
3444
|
throw e
|
|
3401
3445
|
}
|
|
@@ -3407,26 +3451,46 @@ class Playwright extends Helper {
|
|
|
3407
3451
|
*/
|
|
3408
3452
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3409
3453
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3410
|
-
|
|
3411
|
-
const baseUrl = this.options.url
|
|
3412
|
-
let expectedUrl = urlPart
|
|
3413
|
-
if (urlPart.indexOf('http') < 0) {
|
|
3414
|
-
expectedUrl = baseUrl + urlPart
|
|
3415
|
-
}
|
|
3454
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3416
3455
|
|
|
3417
3456
|
try {
|
|
3418
3457
|
await this.page.waitForURL(
|
|
3419
|
-
url => url.href
|
|
3458
|
+
url => url.href === expectedUrl,
|
|
3420
3459
|
{ timeout: waitTimeout },
|
|
3421
3460
|
)
|
|
3422
3461
|
} catch (e) {
|
|
3423
3462
|
const currUrl = await this._getPageUrl()
|
|
3424
3463
|
if (/Timeout/i.test(e.message)) {
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3464
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
3465
|
+
} else {
|
|
3466
|
+
throw e
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
/**
|
|
3472
|
+
* {{> waitCurrentPathEquals }}
|
|
3473
|
+
*/
|
|
3474
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
3475
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3476
|
+
const normalizedPath = normalizePath(path)
|
|
3477
|
+
|
|
3478
|
+
try {
|
|
3479
|
+
await this.page.waitForFunction(
|
|
3480
|
+
expectedPath => {
|
|
3481
|
+
const actualPath = window.location.pathname
|
|
3482
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
3483
|
+
return normalizePath(actualPath) === expectedPath
|
|
3484
|
+
},
|
|
3485
|
+
normalizedPath,
|
|
3486
|
+
{ timeout: waitTimeout },
|
|
3487
|
+
)
|
|
3488
|
+
} catch (e) {
|
|
3489
|
+
const currentUrl = await this._getPageUrl()
|
|
3490
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
3491
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
3492
|
+
if (/Timeout/i.test(e.message)) {
|
|
3493
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
3430
3494
|
} else {
|
|
3431
3495
|
throw e
|
|
3432
3496
|
}
|
|
@@ -4112,9 +4176,15 @@ class Playwright extends Helper {
|
|
|
4112
4176
|
|
|
4113
4177
|
export default Playwright
|
|
4114
4178
|
|
|
4115
|
-
function buildLocatorString(locator) {
|
|
4179
|
+
export function buildLocatorString(locator) {
|
|
4116
4180
|
if (locator.isXPath()) {
|
|
4117
|
-
|
|
4181
|
+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
|
|
4182
|
+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
|
|
4183
|
+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
|
|
4184
|
+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
|
|
4185
|
+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
|
|
4186
|
+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
|
|
4187
|
+
return `xpath=${value}`
|
|
4118
4188
|
}
|
|
4119
4189
|
if (locator.isShadow()) {
|
|
4120
4190
|
// Convert shadow locator to CSS with >> chaining operator
|
|
@@ -4238,7 +4308,7 @@ async function findClickable(matcher, locator) {
|
|
|
4238
4308
|
|
|
4239
4309
|
if (!matchedLocator.isFuzzy()) {
|
|
4240
4310
|
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4241
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4311
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4242
4312
|
return els
|
|
4243
4313
|
}
|
|
4244
4314
|
|
|
@@ -4248,7 +4318,7 @@ async function findClickable(matcher, locator) {
|
|
|
4248
4318
|
try {
|
|
4249
4319
|
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4250
4320
|
if (els.length) {
|
|
4251
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4321
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4252
4322
|
return els
|
|
4253
4323
|
}
|
|
4254
4324
|
} catch (err) {
|
|
@@ -4258,7 +4328,7 @@ async function findClickable(matcher, locator) {
|
|
|
4258
4328
|
try {
|
|
4259
4329
|
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4260
4330
|
if (els.length) {
|
|
4261
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4331
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4262
4332
|
return els
|
|
4263
4333
|
}
|
|
4264
4334
|
} catch (err) {
|
|
@@ -4267,20 +4337,20 @@ async function findClickable(matcher, locator) {
|
|
|
4267
4337
|
|
|
4268
4338
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
4269
4339
|
if (els.length) {
|
|
4270
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4340
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4271
4341
|
return els
|
|
4272
4342
|
}
|
|
4273
4343
|
|
|
4274
4344
|
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
4275
4345
|
if (els.length) {
|
|
4276
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4346
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4277
4347
|
return els
|
|
4278
4348
|
}
|
|
4279
4349
|
|
|
4280
4350
|
try {
|
|
4281
4351
|
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
4282
4352
|
if (els.length) {
|
|
4283
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4353
|
+
if (this.options.strict) assertOnlyOneElement(els, locator, this)
|
|
4284
4354
|
return els
|
|
4285
4355
|
}
|
|
4286
4356
|
} catch (err) {
|
|
@@ -4355,34 +4425,45 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4355
4425
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4356
4426
|
}
|
|
4357
4427
|
|
|
4358
|
-
async function findFields(locator) {
|
|
4428
|
+
async function findFields(locator, context = null) {
|
|
4429
|
+
let contextEl
|
|
4430
|
+
if (context) {
|
|
4431
|
+
const contextEls = await this._locate(context)
|
|
4432
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4433
|
+
contextEl = contextEls[0]
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
const locateFn = contextEl
|
|
4437
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4438
|
+
: loc => this._locate(loc)
|
|
4439
|
+
|
|
4359
4440
|
// Handle role locators with text/exact options
|
|
4360
4441
|
if (isRoleLocatorObject(locator)) {
|
|
4361
|
-
const
|
|
4362
|
-
const roleElements = await handleRoleLocator(
|
|
4442
|
+
const matcher = contextEl || (await this.page)
|
|
4443
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4363
4444
|
if (roleElements) return roleElements
|
|
4364
4445
|
}
|
|
4365
4446
|
|
|
4366
4447
|
const matchedLocator = new Locator(locator)
|
|
4367
4448
|
if (!matchedLocator.isFuzzy()) {
|
|
4368
|
-
return
|
|
4449
|
+
return locateFn(matchedLocator)
|
|
4369
4450
|
}
|
|
4370
4451
|
const literal = xpathLocator.literal(locator)
|
|
4371
4452
|
|
|
4372
|
-
let els = await
|
|
4453
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4373
4454
|
if (els.length) {
|
|
4374
4455
|
return els
|
|
4375
4456
|
}
|
|
4376
4457
|
|
|
4377
|
-
els = await
|
|
4458
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4378
4459
|
if (els.length) {
|
|
4379
4460
|
return els
|
|
4380
4461
|
}
|
|
4381
|
-
els = await
|
|
4462
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4382
4463
|
if (els.length) {
|
|
4383
4464
|
return els
|
|
4384
4465
|
}
|
|
4385
|
-
return
|
|
4466
|
+
return locateFn({ css: locator })
|
|
4386
4467
|
}
|
|
4387
4468
|
|
|
4388
4469
|
async function proceedSelect(context, el, option) {
|
|
@@ -4431,8 +4512,8 @@ async function proceedSelect(context, el, option) {
|
|
|
4431
4512
|
return this._waitForAction()
|
|
4432
4513
|
}
|
|
4433
4514
|
|
|
4434
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
4435
|
-
const els = await findFields.call(this, field)
|
|
4515
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4516
|
+
const els = await findFields.call(this, field, context)
|
|
4436
4517
|
assertElementExists(els, field, 'Field')
|
|
4437
4518
|
const el = els[0]
|
|
4438
4519
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4546,9 +4627,10 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4546
4627
|
}
|
|
4547
4628
|
}
|
|
4548
4629
|
|
|
4549
|
-
function assertOnlyOneElement(elements, locator) {
|
|
4630
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4550
4631
|
if (elements.length > 1) {
|
|
4551
|
-
|
|
4632
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4633
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4552
4634
|
}
|
|
4553
4635
|
}
|
|
4554
4636
|
|