codeceptjs 4.0.0-rc.1 → 4.0.0-rc.11
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 +637 -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 +224 -138
- package/lib/helper/Puppeteer.js +211 -69
- package/lib/helper/WebDriver.js +183 -64
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -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
|
@@ -7,6 +7,7 @@ import promiseRetry from 'promise-retry'
|
|
|
7
7
|
import Locator from '../locator.js'
|
|
8
8
|
import recorder from '../recorder.js'
|
|
9
9
|
import store from '../store.js'
|
|
10
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
10
11
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
11
12
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
12
13
|
import { empty } from '../assert/empty.js'
|
|
@@ -23,7 +24,11 @@ import {
|
|
|
23
24
|
clearString,
|
|
24
25
|
requireWithFallback,
|
|
25
26
|
normalizeSpacesInString,
|
|
27
|
+
normalizePath,
|
|
28
|
+
resolveUrl,
|
|
26
29
|
relativeDir,
|
|
30
|
+
getMimeType,
|
|
31
|
+
base64EncodeFile,
|
|
27
32
|
} from '../utils.js'
|
|
28
33
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
29
34
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
@@ -32,7 +37,9 @@ import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefu
|
|
|
32
37
|
import Popup from './extras/Popup.js'
|
|
33
38
|
import Console from './extras/Console.js'
|
|
34
39
|
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
40
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
35
41
|
import WebElement from '../element/WebElement.js'
|
|
42
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
36
43
|
|
|
37
44
|
let playwright
|
|
38
45
|
let perfTiming
|
|
@@ -1490,8 +1497,23 @@ class Playwright extends Helper {
|
|
|
1490
1497
|
*
|
|
1491
1498
|
*/
|
|
1492
1499
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1500
|
+
let context = null
|
|
1501
|
+
if (typeof offsetX !== 'number') {
|
|
1502
|
+
context = offsetX
|
|
1503
|
+
offsetX = 0
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
let el
|
|
1507
|
+
if (context) {
|
|
1508
|
+
const contextEls = await this._locate(context)
|
|
1509
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1510
|
+
el = await findElements.call(this, contextEls[0], locator)
|
|
1511
|
+
assertElementExists(el, locator)
|
|
1512
|
+
el = el[0]
|
|
1513
|
+
} else {
|
|
1514
|
+
el = await this._locateElement(locator)
|
|
1515
|
+
assertElementExists(el, locator)
|
|
1516
|
+
}
|
|
1495
1517
|
|
|
1496
1518
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1497
1519
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1759,8 +1781,7 @@ class Playwright extends Helper {
|
|
|
1759
1781
|
if (elements.length === 0) {
|
|
1760
1782
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1761
1783
|
}
|
|
1762
|
-
|
|
1763
|
-
return elements[0]
|
|
1784
|
+
return selectElement(elements, locator, this)
|
|
1764
1785
|
}
|
|
1765
1786
|
|
|
1766
1787
|
/**
|
|
@@ -1775,8 +1796,7 @@ class Playwright extends Helper {
|
|
|
1775
1796
|
const context = providedContext || (await this._getContext())
|
|
1776
1797
|
const els = await findCheckable.call(this, locator, context)
|
|
1777
1798
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1778
|
-
|
|
1779
|
-
return els[0]
|
|
1799
|
+
return selectElement(els, locator, this)
|
|
1780
1800
|
}
|
|
1781
1801
|
|
|
1782
1802
|
/**
|
|
@@ -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'))
|
|
@@ -2198,6 +2232,7 @@ class Playwright extends Helper {
|
|
|
2198
2232
|
* {{> pressKeyWithKeyNormalization }}
|
|
2199
2233
|
*/
|
|
2200
2234
|
async pressKey(key) {
|
|
2235
|
+
await checkFocusBeforePressKey(this, key)
|
|
2201
2236
|
const modifiers = []
|
|
2202
2237
|
if (Array.isArray(key)) {
|
|
2203
2238
|
for (let k of key) {
|
|
@@ -2226,6 +2261,8 @@ class Playwright extends Helper {
|
|
|
2226
2261
|
* {{> type }}
|
|
2227
2262
|
*/
|
|
2228
2263
|
async type(keys, delay = null) {
|
|
2264
|
+
await checkFocusBeforeType(this)
|
|
2265
|
+
|
|
2229
2266
|
// Always use page.keyboard.type for any string (including single character and national characters).
|
|
2230
2267
|
if (!Array.isArray(keys)) {
|
|
2231
2268
|
keys = keys.toString()
|
|
@@ -2245,11 +2282,10 @@ class Playwright extends Helper {
|
|
|
2245
2282
|
* {{> fillField }}
|
|
2246
2283
|
*
|
|
2247
2284
|
*/
|
|
2248
|
-
async fillField(field, value) {
|
|
2249
|
-
const els = await findFields.call(this, field)
|
|
2285
|
+
async fillField(field, value, context = null) {
|
|
2286
|
+
const els = await findFields.call(this, field, context)
|
|
2250
2287
|
assertElementExists(els, field, 'Field')
|
|
2251
|
-
|
|
2252
|
-
const el = els[0]
|
|
2288
|
+
const el = selectElement(els, field, this)
|
|
2253
2289
|
|
|
2254
2290
|
await el.clear()
|
|
2255
2291
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
@@ -2262,28 +2298,13 @@ class Playwright extends Helper {
|
|
|
2262
2298
|
}
|
|
2263
2299
|
|
|
2264
2300
|
/**
|
|
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.
|
|
2301
|
+
* {{> clearField }}
|
|
2280
2302
|
*/
|
|
2281
|
-
async clearField(locator,
|
|
2282
|
-
const els = await findFields.call(this, locator)
|
|
2303
|
+
async clearField(locator, context = null) {
|
|
2304
|
+
const els = await findFields.call(this, locator, context)
|
|
2283
2305
|
assertElementExists(els, locator, 'Field to clear')
|
|
2284
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
2285
2306
|
|
|
2286
|
-
const el = els
|
|
2307
|
+
const el = selectElement(els, locator, this)
|
|
2287
2308
|
|
|
2288
2309
|
await highlightActiveElement.call(this, el)
|
|
2289
2310
|
|
|
@@ -2295,76 +2316,101 @@ class Playwright extends Helper {
|
|
|
2295
2316
|
/**
|
|
2296
2317
|
* {{> appendField }}
|
|
2297
2318
|
*/
|
|
2298
|
-
async appendField(field, value) {
|
|
2299
|
-
const els = await findFields.call(this, field)
|
|
2319
|
+
async appendField(field, value, context = null) {
|
|
2320
|
+
const els = await findFields.call(this, field, context)
|
|
2300
2321
|
assertElementExists(els, field, 'Field')
|
|
2301
|
-
|
|
2302
|
-
await highlightActiveElement.call(this,
|
|
2303
|
-
await
|
|
2304
|
-
await
|
|
2322
|
+
const el = selectElement(els, field, this)
|
|
2323
|
+
await highlightActiveElement.call(this, el)
|
|
2324
|
+
await el.press('End')
|
|
2325
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2305
2326
|
return this._waitForAction()
|
|
2306
2327
|
}
|
|
2307
2328
|
|
|
2308
2329
|
/**
|
|
2309
2330
|
* {{> seeInField }}
|
|
2310
2331
|
*/
|
|
2311
|
-
async seeInField(field, value) {
|
|
2332
|
+
async seeInField(field, value, context = null) {
|
|
2312
2333
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2313
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2334
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2314
2335
|
}
|
|
2315
2336
|
|
|
2316
2337
|
/**
|
|
2317
2338
|
* {{> dontSeeInField }}
|
|
2318
2339
|
*/
|
|
2319
|
-
async dontSeeInField(field, value) {
|
|
2340
|
+
async dontSeeInField(field, value, context = null) {
|
|
2320
2341
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2321
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2342
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2322
2343
|
}
|
|
2323
2344
|
|
|
2324
2345
|
/**
|
|
2325
2346
|
* {{> attachFile }}
|
|
2326
2347
|
*
|
|
2327
2348
|
*/
|
|
2328
|
-
async attachFile(locator, pathToFile) {
|
|
2349
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2329
2350
|
const file = path.join(global.codecept_dir, pathToFile)
|
|
2330
2351
|
|
|
2331
2352
|
if (!fileExists(file)) {
|
|
2332
2353
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2333
2354
|
}
|
|
2334
|
-
const els = await findFields.call(this, locator)
|
|
2335
|
-
|
|
2336
|
-
|
|
2355
|
+
const els = await findFields.call(this, locator, context)
|
|
2356
|
+
if (els.length) {
|
|
2357
|
+
const el = selectElement(els, locator, this)
|
|
2358
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2359
|
+
const type = await el.evaluate(el => el.type)
|
|
2360
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2361
|
+
await el.setInputFiles(file)
|
|
2362
|
+
return this._waitForAction()
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2367
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2368
|
+
const el = selectElement(targetEls, locator, this)
|
|
2369
|
+
const fileData = {
|
|
2370
|
+
base64Content: base64EncodeFile(file),
|
|
2371
|
+
fileName: path.basename(file),
|
|
2372
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2373
|
+
}
|
|
2374
|
+
await el.evaluate(dropFile, fileData)
|
|
2337
2375
|
return this._waitForAction()
|
|
2338
2376
|
}
|
|
2339
2377
|
|
|
2340
2378
|
/**
|
|
2341
2379
|
* {{> selectOption }}
|
|
2342
2380
|
*/
|
|
2343
|
-
async selectOption(select, option) {
|
|
2344
|
-
const
|
|
2381
|
+
async selectOption(select, option, context = null) {
|
|
2382
|
+
const pageContext = await this.context
|
|
2345
2383
|
const matchedLocator = new Locator(select)
|
|
2346
2384
|
|
|
2385
|
+
let contextEl
|
|
2386
|
+
if (context) {
|
|
2387
|
+
const contextEls = await this._locate(context)
|
|
2388
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2389
|
+
contextEl = contextEls[0]
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2347
2392
|
// Strict locator
|
|
2348
2393
|
if (!matchedLocator.isFuzzy()) {
|
|
2349
2394
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2350
|
-
const els = await this._locate(matchedLocator)
|
|
2395
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2351
2396
|
assertElementExists(els, select, 'Selectable element')
|
|
2352
|
-
return proceedSelect.call(this,
|
|
2397
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2353
2398
|
}
|
|
2354
2399
|
|
|
2355
2400
|
// Fuzzy: try combobox
|
|
2356
2401
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2357
|
-
|
|
2358
|
-
|
|
2402
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2403
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2404
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2359
2405
|
|
|
2360
2406
|
// Fuzzy: try listbox
|
|
2361
|
-
els = await findByRole(
|
|
2362
|
-
if (els?.length) return proceedSelect.call(this,
|
|
2407
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2408
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2363
2409
|
|
|
2364
2410
|
// Fuzzy: try native select
|
|
2365
|
-
els = await findFields.call(this, select)
|
|
2411
|
+
els = await findFields.call(this, select, context)
|
|
2366
2412
|
assertElementExists(els, select, 'Selectable element')
|
|
2367
|
-
return proceedSelect.call(this,
|
|
2413
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2368
2414
|
}
|
|
2369
2415
|
|
|
2370
2416
|
/**
|
|
@@ -2405,6 +2451,26 @@ class Playwright extends Helper {
|
|
|
2405
2451
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
2406
2452
|
}
|
|
2407
2453
|
|
|
2454
|
+
/**
|
|
2455
|
+
* {{> seeCurrentPathEquals }}
|
|
2456
|
+
*/
|
|
2457
|
+
async seeCurrentPathEquals(path) {
|
|
2458
|
+
const currentUrl = await this._getPageUrl()
|
|
2459
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2460
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2461
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
2466
|
+
*/
|
|
2467
|
+
async dontSeeCurrentPathEquals(path) {
|
|
2468
|
+
const currentUrl = await this._getPageUrl()
|
|
2469
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2470
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2471
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2408
2474
|
/**
|
|
2409
2475
|
* {{> see }}
|
|
2410
2476
|
*
|
|
@@ -2638,15 +2704,12 @@ class Playwright extends Helper {
|
|
|
2638
2704
|
*
|
|
2639
2705
|
*/
|
|
2640
2706
|
async grabTextFrom(locator) {
|
|
2641
|
-
|
|
2642
|
-
if (
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
this.debugSection('Text', text)
|
|
2648
|
-
return text
|
|
2649
|
-
}
|
|
2707
|
+
const roleElements = await handleRoleLocator(this.page, locator)
|
|
2708
|
+
if (roleElements && roleElements.length > 0) {
|
|
2709
|
+
const text = await roleElements[0].textContent()
|
|
2710
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2711
|
+
this.debugSection('Text', text)
|
|
2712
|
+
return text
|
|
2650
2713
|
}
|
|
2651
2714
|
|
|
2652
2715
|
const locatorObj = new Locator(locator, 'css')
|
|
@@ -2874,7 +2937,7 @@ class Playwright extends Helper {
|
|
|
2874
2937
|
const els = await this._locate(matchedLocator)
|
|
2875
2938
|
assertElementExists(els, locator)
|
|
2876
2939
|
const snapshot = await els[0].ariaSnapshot()
|
|
2877
|
-
this.debugSection('Aria Snapshot', snapshot)
|
|
2940
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
2878
2941
|
return snapshot
|
|
2879
2942
|
}
|
|
2880
2943
|
|
|
@@ -3362,6 +3425,7 @@ class Playwright extends Helper {
|
|
|
3362
3425
|
*/
|
|
3363
3426
|
async waitInUrl(urlPart, sec = null) {
|
|
3364
3427
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3428
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3365
3429
|
|
|
3366
3430
|
return this.page
|
|
3367
3431
|
.waitForFunction(
|
|
@@ -3369,13 +3433,13 @@ class Playwright extends Helper {
|
|
|
3369
3433
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3370
3434
|
return currUrl.indexOf(urlPart) > -1
|
|
3371
3435
|
},
|
|
3372
|
-
|
|
3436
|
+
expectedUrl,
|
|
3373
3437
|
{ timeout: waitTimeout },
|
|
3374
3438
|
)
|
|
3375
3439
|
.catch(async e => {
|
|
3376
|
-
const currUrl = await this._getPageUrl()
|
|
3440
|
+
const currUrl = await this._getPageUrl()
|
|
3377
3441
|
if (/Timeout/i.test(e.message)) {
|
|
3378
|
-
throw new Error(`expected url to include ${
|
|
3442
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3379
3443
|
} else {
|
|
3380
3444
|
throw e
|
|
3381
3445
|
}
|
|
@@ -3387,26 +3451,46 @@ class Playwright extends Helper {
|
|
|
3387
3451
|
*/
|
|
3388
3452
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3389
3453
|
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
|
-
}
|
|
3454
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3396
3455
|
|
|
3397
3456
|
try {
|
|
3398
3457
|
await this.page.waitForURL(
|
|
3399
|
-
url => url.href
|
|
3458
|
+
url => url.href === expectedUrl,
|
|
3400
3459
|
{ timeout: waitTimeout },
|
|
3401
3460
|
)
|
|
3402
3461
|
} catch (e) {
|
|
3403
3462
|
const currUrl = await this._getPageUrl()
|
|
3404
3463
|
if (/Timeout/i.test(e.message)) {
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
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)}`)
|
|
3410
3494
|
} else {
|
|
3411
3495
|
throw e
|
|
3412
3496
|
}
|
|
@@ -4092,9 +4176,15 @@ class Playwright extends Helper {
|
|
|
4092
4176
|
|
|
4093
4177
|
export default Playwright
|
|
4094
4178
|
|
|
4095
|
-
function buildLocatorString(locator) {
|
|
4179
|
+
export function buildLocatorString(locator) {
|
|
4096
4180
|
if (locator.isXPath()) {
|
|
4097
|
-
|
|
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}`
|
|
4098
4188
|
}
|
|
4099
4189
|
if (locator.isShadow()) {
|
|
4100
4190
|
// Convert shadow locator to CSS with >> chaining operator
|
|
@@ -4105,25 +4195,22 @@ function buildLocatorString(locator) {
|
|
|
4105
4195
|
return locator.simplify()
|
|
4106
4196
|
}
|
|
4107
4197
|
|
|
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
4198
|
/**
|
|
4116
4199
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4200
|
+
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
4117
4201
|
* Returns elements array if role locator, null otherwise
|
|
4118
4202
|
*/
|
|
4119
4203
|
async function handleRoleLocator(context, locator) {
|
|
4120
|
-
|
|
4204
|
+
const loc = new Locator(locator)
|
|
4205
|
+
if (!loc.isRole()) return null
|
|
4121
4206
|
|
|
4207
|
+
const roleObj = loc.locator || {}
|
|
4122
4208
|
const options = {}
|
|
4123
|
-
if (
|
|
4124
|
-
if (
|
|
4209
|
+
if (roleObj.text) options.name = roleObj.text
|
|
4210
|
+
if (roleObj.name) options.name = roleObj.name
|
|
4211
|
+
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4125
4212
|
|
|
4126
|
-
return context.getByRole(
|
|
4213
|
+
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4127
4214
|
}
|
|
4128
4215
|
|
|
4129
4216
|
async function findByRole(context, locator) {
|
|
@@ -4192,16 +4279,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4192
4279
|
assertElementExists(els, locator, 'Clickable element')
|
|
4193
4280
|
}
|
|
4194
4281
|
|
|
4195
|
-
|
|
4196
|
-
|
|
4282
|
+
const opts = store.currentStep?.opts
|
|
4283
|
+
let element
|
|
4284
|
+
if (opts?.elementIndex != null) {
|
|
4285
|
+
element = selectElement(els, locator, this)
|
|
4286
|
+
} else {
|
|
4287
|
+
const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
|
|
4288
|
+
if (strict) assertOnlyOneElement(els, locator, this)
|
|
4289
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
await highlightActiveElement.call(this, element)
|
|
4293
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4197
4294
|
|
|
4198
|
-
/*
|
|
4199
|
-
using the force true options itself but instead dispatching a click
|
|
4200
|
-
*/
|
|
4201
4295
|
if (options.force) {
|
|
4202
|
-
await
|
|
4296
|
+
await element.dispatchEvent('click')
|
|
4203
4297
|
} else {
|
|
4204
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4205
4298
|
await element.click(options)
|
|
4206
4299
|
}
|
|
4207
4300
|
const promises = []
|
|
@@ -4218,7 +4311,6 @@ async function findClickable(matcher, locator) {
|
|
|
4218
4311
|
|
|
4219
4312
|
if (!matchedLocator.isFuzzy()) {
|
|
4220
4313
|
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4221
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4222
4314
|
return els
|
|
4223
4315
|
}
|
|
4224
4316
|
|
|
@@ -4227,42 +4319,27 @@ async function findClickable(matcher, locator) {
|
|
|
4227
4319
|
|
|
4228
4320
|
try {
|
|
4229
4321
|
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
|
-
}
|
|
4322
|
+
if (els.length) return els
|
|
4234
4323
|
} catch (err) {
|
|
4235
4324
|
// getByRole not supported or failed
|
|
4236
4325
|
}
|
|
4237
4326
|
|
|
4238
4327
|
try {
|
|
4239
4328
|
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
|
-
}
|
|
4329
|
+
if (els.length) return els
|
|
4244
4330
|
} catch (err) {
|
|
4245
4331
|
// getByRole not supported or failed
|
|
4246
4332
|
}
|
|
4247
4333
|
|
|
4248
4334
|
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
|
-
}
|
|
4335
|
+
if (els.length) return els
|
|
4253
4336
|
|
|
4254
4337
|
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
|
-
}
|
|
4338
|
+
if (els.length) return els
|
|
4259
4339
|
|
|
4260
4340
|
try {
|
|
4261
4341
|
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
|
-
}
|
|
4342
|
+
if (els.length) return els
|
|
4266
4343
|
} catch (err) {
|
|
4267
4344
|
// Do nothing
|
|
4268
4345
|
}
|
|
@@ -4335,34 +4412,42 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4335
4412
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4336
4413
|
}
|
|
4337
4414
|
|
|
4338
|
-
async function findFields(locator) {
|
|
4339
|
-
|
|
4340
|
-
if (
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4415
|
+
async function findFields(locator, context = null) {
|
|
4416
|
+
let contextEl
|
|
4417
|
+
if (context) {
|
|
4418
|
+
const contextEls = await this._locate(context)
|
|
4419
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4420
|
+
contextEl = contextEls[0]
|
|
4344
4421
|
}
|
|
4345
4422
|
|
|
4423
|
+
const locateFn = contextEl
|
|
4424
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4425
|
+
: loc => this._locate(loc)
|
|
4426
|
+
|
|
4427
|
+
const matcher = contextEl || (await this.page)
|
|
4428
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4429
|
+
if (roleElements) return roleElements
|
|
4430
|
+
|
|
4346
4431
|
const matchedLocator = new Locator(locator)
|
|
4347
4432
|
if (!matchedLocator.isFuzzy()) {
|
|
4348
|
-
return
|
|
4433
|
+
return locateFn(matchedLocator)
|
|
4349
4434
|
}
|
|
4350
4435
|
const literal = xpathLocator.literal(locator)
|
|
4351
4436
|
|
|
4352
|
-
let els = await
|
|
4437
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4353
4438
|
if (els.length) {
|
|
4354
4439
|
return els
|
|
4355
4440
|
}
|
|
4356
4441
|
|
|
4357
|
-
els = await
|
|
4442
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4358
4443
|
if (els.length) {
|
|
4359
4444
|
return els
|
|
4360
4445
|
}
|
|
4361
|
-
els = await
|
|
4446
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4362
4447
|
if (els.length) {
|
|
4363
4448
|
return els
|
|
4364
4449
|
}
|
|
4365
|
-
return
|
|
4450
|
+
return locateFn({ css: locator })
|
|
4366
4451
|
}
|
|
4367
4452
|
|
|
4368
4453
|
async function proceedSelect(context, el, option) {
|
|
@@ -4411,8 +4496,8 @@ async function proceedSelect(context, el, option) {
|
|
|
4411
4496
|
return this._waitForAction()
|
|
4412
4497
|
}
|
|
4413
4498
|
|
|
4414
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
4415
|
-
const els = await findFields.call(this, field)
|
|
4499
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4500
|
+
const els = await findFields.call(this, field, context)
|
|
4416
4501
|
assertElementExists(els, field, 'Field')
|
|
4417
4502
|
const el = els[0]
|
|
4418
4503
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4526,9 +4611,10 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4526
4611
|
}
|
|
4527
4612
|
}
|
|
4528
4613
|
|
|
4529
|
-
function assertOnlyOneElement(elements, locator) {
|
|
4614
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4530
4615
|
if (elements.length > 1) {
|
|
4531
|
-
|
|
4616
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4617
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4532
4618
|
}
|
|
4533
4619
|
}
|
|
4534
4620
|
|