codeceptjs 4.0.0-beta.6.esm-aria → 4.0.0-beta.8.esm-aria
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 +46 -3
- package/bin/codecept.js +9 -0
- package/bin/test-server.js +64 -0
- package/docs/webapi/click.mustache +5 -1
- package/lib/ai.js +66 -102
- package/lib/codecept.js +99 -24
- package/lib/command/generate.js +33 -1
- package/lib/command/init.js +7 -3
- package/lib/command/run-workers.js +31 -2
- package/lib/command/run.js +15 -0
- package/lib/command/workers/runTests.js +331 -58
- package/lib/config.js +16 -5
- package/lib/container.js +15 -13
- package/lib/effects.js +1 -1
- package/lib/element/WebElement.js +327 -0
- package/lib/event.js +10 -1
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +34 -6
- package/lib/helper/Appium.js +156 -42
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +48 -40
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +841 -153
- package/lib/helper/Puppeteer.js +263 -67
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +116 -26
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
- package/lib/helper/network/actions.js +8 -6
- package/lib/listener/config.js +11 -3
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/helpers.js +8 -2
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +12 -0
- package/lib/mocha/asyncWrapper.js +13 -3
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/factory.js +3 -0
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +62 -18
- package/lib/plugin/coverage.js +16 -3
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/recorder.js +28 -3
- package/lib/result.js +100 -23
- package/lib/retryCoordinator.js +207 -0
- package/lib/step/base.js +1 -1
- package/lib/step/comment.js +2 -2
- package/lib/step/meta.js +1 -1
- package/lib/template/heal.js +1 -1
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +87 -6
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +179 -23
- package/package.json +60 -52
- package/translations/utils.js +2 -10
- package/typings/index.d.ts +19 -7
- package/typings/promiseBasedTypes.d.ts +5525 -3759
- package/typings/types.d.ts +5791 -3781
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -36,6 +36,7 @@ import { highlightElement } from './scripts/highlightElement.js'
|
|
|
36
36
|
import { blurElement } from './scripts/blurElement.js'
|
|
37
37
|
import { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } from './errors/ElementAssertion.js'
|
|
38
38
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
39
|
+
import WebElement from '../element/WebElement.js'
|
|
39
40
|
|
|
40
41
|
let puppeteer
|
|
41
42
|
|
|
@@ -74,7 +75,7 @@ const consoleLogStore = new Console()
|
|
|
74
75
|
* @prop {boolean} [keepBrowserState=false] - keep browser state between tests when `restart` is set to false.
|
|
75
76
|
* @prop {boolean} [keepCookies=false] - keep cookies between tests when `restart` is set to false.
|
|
76
77
|
* @prop {number} [waitForAction=100] - how long to wait after click, doubleClick or PressKey actions in ms. Default: 100.
|
|
77
|
-
* @prop {string} [waitForNavigation=load] - when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/
|
|
78
|
+
* @prop {string|string[]} [waitForNavigation=load] - when to consider navigation succeeded. Possible options: `load`, `domcontentloaded`, `networkidle0`, `networkidle2`. See [Puppeteer API](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.waitforoptions.md). Array values are accepted as well.
|
|
78
79
|
* @prop {number} [pressKeyDelay=10] - delay between key presses in ms. Used when calling Puppeteers page.type(...) in fillField/appendField
|
|
79
80
|
* @prop {number} [getPageTimeout=30000] - config option to set maximum navigation time in milliseconds. If the timeout is set to 0, then timeout will be disabled.
|
|
80
81
|
* @prop {number} [waitForTimeout=1000] - default wait* timeout in ms.
|
|
@@ -82,13 +83,13 @@ const consoleLogStore = new Console()
|
|
|
82
83
|
* @prop {string} [userAgent] - user-agent string.
|
|
83
84
|
* @prop {boolean} [manualStart=false] - do not start browser before a test, start it manually inside a helper with `this.helpers["Puppeteer"]._startBrowser()`.
|
|
84
85
|
* @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
|
|
85
|
-
* @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/
|
|
86
|
+
* @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.launchoptions.md).
|
|
86
87
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
87
88
|
*/
|
|
88
89
|
const config = {}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* Uses [Google Chrome's Puppeteer](https://github.com/
|
|
92
|
+
* Uses [Google Chrome's Puppeteer](https://github.com/puppeteer/puppeteer) library to run tests inside headless Chrome.
|
|
92
93
|
* Browser control is executed via DevTools Protocol (instead of Selenium).
|
|
93
94
|
* This helper works with a browser out of the box with no additional tools required to install.
|
|
94
95
|
*
|
|
@@ -361,6 +362,9 @@ class Puppeteer extends Helper {
|
|
|
361
362
|
async _after() {
|
|
362
363
|
if (!this.isRunning) return
|
|
363
364
|
|
|
365
|
+
// Clear popup state to prevent leakage between tests
|
|
366
|
+
popupStore.clear()
|
|
367
|
+
|
|
364
368
|
// close other sessions
|
|
365
369
|
const contexts = this.browser.browserContexts()
|
|
366
370
|
const defaultCtx = contexts.shift()
|
|
@@ -432,9 +436,27 @@ class Puppeteer extends Helper {
|
|
|
432
436
|
} else {
|
|
433
437
|
this.activeSessionName = session
|
|
434
438
|
}
|
|
439
|
+
|
|
435
440
|
const defaultCtx = this.browser.defaultBrowserContext()
|
|
436
|
-
|
|
437
|
-
|
|
441
|
+
if (!defaultCtx) {
|
|
442
|
+
this.debug('Cannot restore session vars: default browser context is undefined')
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const existingPages = defaultCtx.targets().filter(t => t.type() === 'page')
|
|
448
|
+
if (existingPages && existingPages.length > 0) {
|
|
449
|
+
await this._setPage(await existingPages[0].page())
|
|
450
|
+
// Reset context-related variables to ensure clean state after session
|
|
451
|
+
this.context = await this.page
|
|
452
|
+
this.contextLocator = null
|
|
453
|
+
} else {
|
|
454
|
+
this.debug('Cannot restore session vars: no pages available')
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
this.debug(`Failed to restore session vars: ${err.message}`)
|
|
458
|
+
return
|
|
459
|
+
}
|
|
438
460
|
|
|
439
461
|
return this._waitForAction()
|
|
440
462
|
},
|
|
@@ -650,9 +672,14 @@ class Puppeteer extends Helper {
|
|
|
650
672
|
}
|
|
651
673
|
}
|
|
652
674
|
|
|
653
|
-
async _evaluateHandeInContext(...args) {
|
|
675
|
+
async _evaluateHandeInContext(fn, handle, ...args) {
|
|
676
|
+
// If handle is provided, evaluate directly on it to avoid "JavaScript world" errors
|
|
677
|
+
if (handle) {
|
|
678
|
+
return handle.evaluate(fn, ...args)
|
|
679
|
+
}
|
|
680
|
+
// Otherwise use the context
|
|
654
681
|
const context = await this._getContext()
|
|
655
|
-
return context.evaluateHandle(...args)
|
|
682
|
+
return context.evaluateHandle(fn, ...args)
|
|
656
683
|
}
|
|
657
684
|
|
|
658
685
|
async _withinBegin(locator) {
|
|
@@ -671,16 +698,22 @@ class Puppeteer extends Helper {
|
|
|
671
698
|
return
|
|
672
699
|
}
|
|
673
700
|
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
701
|
+
const el = await this._locateElement(locator)
|
|
702
|
+
if (!el) {
|
|
703
|
+
throw new ElementNotFound(locator, 'Element for within context')
|
|
704
|
+
}
|
|
705
|
+
this.context = el
|
|
677
706
|
|
|
678
707
|
this.withinLocator = new Locator(locator)
|
|
679
708
|
}
|
|
680
709
|
|
|
681
710
|
async _withinEnd() {
|
|
682
711
|
this.withinLocator = null
|
|
683
|
-
this.
|
|
712
|
+
if (this.page && !this.page.isClosed?.()) {
|
|
713
|
+
this.context = await this.page.mainFrame().$('body')
|
|
714
|
+
} else {
|
|
715
|
+
this.context = null
|
|
716
|
+
}
|
|
684
717
|
}
|
|
685
718
|
|
|
686
719
|
_extractDataFromPerformanceTiming(timing, ...dataNames) {
|
|
@@ -781,11 +814,13 @@ class Puppeteer extends Helper {
|
|
|
781
814
|
* {{ react }}
|
|
782
815
|
*/
|
|
783
816
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
784
|
-
const
|
|
785
|
-
|
|
817
|
+
const el = await this._locateElement(locator)
|
|
818
|
+
if (!el) {
|
|
819
|
+
throw new ElementNotFound(locator, 'Element to move cursor to')
|
|
820
|
+
}
|
|
786
821
|
|
|
787
822
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
788
|
-
const { x, y } = await getClickablePoint(
|
|
823
|
+
const { x, y } = await getClickablePoint(el)
|
|
789
824
|
await this.page.mouse.move(x + offsetX, y + offsetY)
|
|
790
825
|
return this._waitForAction()
|
|
791
826
|
}
|
|
@@ -795,9 +830,10 @@ class Puppeteer extends Helper {
|
|
|
795
830
|
*
|
|
796
831
|
*/
|
|
797
832
|
async focus(locator) {
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
833
|
+
const el = await this._locateElement(locator)
|
|
834
|
+
if (!el) {
|
|
835
|
+
throw new ElementNotFound(locator, 'Element to focus')
|
|
836
|
+
}
|
|
801
837
|
|
|
802
838
|
await el.click()
|
|
803
839
|
await el.focus()
|
|
@@ -809,10 +845,12 @@ class Puppeteer extends Helper {
|
|
|
809
845
|
*
|
|
810
846
|
*/
|
|
811
847
|
async blur(locator) {
|
|
812
|
-
const
|
|
813
|
-
|
|
848
|
+
const el = await this._locateElement(locator)
|
|
849
|
+
if (!el) {
|
|
850
|
+
throw new ElementNotFound(locator, 'Element to blur')
|
|
851
|
+
}
|
|
814
852
|
|
|
815
|
-
await blurElement(
|
|
853
|
+
await blurElement(el, this.page)
|
|
816
854
|
return this._waitForAction()
|
|
817
855
|
}
|
|
818
856
|
|
|
@@ -861,11 +899,12 @@ class Puppeteer extends Helper {
|
|
|
861
899
|
}
|
|
862
900
|
|
|
863
901
|
if (locator) {
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
902
|
+
const el = await this._locateElement(locator)
|
|
903
|
+
if (!el) {
|
|
904
|
+
throw new ElementNotFound(locator, 'Element to scroll into view')
|
|
905
|
+
}
|
|
867
906
|
await el.evaluate(el => el.scrollIntoView())
|
|
868
|
-
const elementCoordinates = await getClickablePoint(
|
|
907
|
+
const elementCoordinates = await getClickablePoint(el)
|
|
869
908
|
await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
|
|
870
909
|
} else {
|
|
871
910
|
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
|
|
@@ -933,6 +972,21 @@ class Puppeteer extends Helper {
|
|
|
933
972
|
return findElements.call(this, context, locator)
|
|
934
973
|
}
|
|
935
974
|
|
|
975
|
+
/**
|
|
976
|
+
* Get single element by different locator types, including strict locator
|
|
977
|
+
* Should be used in custom helpers:
|
|
978
|
+
*
|
|
979
|
+
* ```js
|
|
980
|
+
* const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
|
|
981
|
+
* ```
|
|
982
|
+
*
|
|
983
|
+
* {{ react }}
|
|
984
|
+
*/
|
|
985
|
+
async _locateElement(locator) {
|
|
986
|
+
const context = await this.context
|
|
987
|
+
return findElement.call(this, context, locator)
|
|
988
|
+
}
|
|
989
|
+
|
|
936
990
|
/**
|
|
937
991
|
* Find a checkbox by providing human-readable text:
|
|
938
992
|
* NOTE: Assumes the checkable element exists
|
|
@@ -944,7 +998,9 @@ class Puppeteer extends Helper {
|
|
|
944
998
|
async _locateCheckable(locator, providedContext = null) {
|
|
945
999
|
const context = providedContext || (await this._getContext())
|
|
946
1000
|
const els = await findCheckable.call(this, locator, context)
|
|
947
|
-
|
|
1001
|
+
if (!els || els.length === 0) {
|
|
1002
|
+
throw new ElementNotFound(locator, 'Checkbox or radio')
|
|
1003
|
+
}
|
|
948
1004
|
return els[0]
|
|
949
1005
|
}
|
|
950
1006
|
|
|
@@ -976,7 +1032,20 @@ class Puppeteer extends Helper {
|
|
|
976
1032
|
*
|
|
977
1033
|
*/
|
|
978
1034
|
async grabWebElements(locator) {
|
|
979
|
-
|
|
1035
|
+
const elements = await this._locate(locator)
|
|
1036
|
+
return elements.map(element => new WebElement(element, this))
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* {{> grabWebElement }}
|
|
1041
|
+
*
|
|
1042
|
+
*/
|
|
1043
|
+
async grabWebElement(locator) {
|
|
1044
|
+
const elements = await this._locate(locator)
|
|
1045
|
+
if (elements.length === 0) {
|
|
1046
|
+
throw new ElementNotFound(locator, 'Element')
|
|
1047
|
+
}
|
|
1048
|
+
return new WebElement(elements[0], this)
|
|
980
1049
|
}
|
|
981
1050
|
|
|
982
1051
|
async grabWebElement(locator) {
|
|
@@ -1146,7 +1215,7 @@ class Puppeteer extends Helper {
|
|
|
1146
1215
|
*
|
|
1147
1216
|
* {{ react }}
|
|
1148
1217
|
*/
|
|
1149
|
-
async click(locator, context = null) {
|
|
1218
|
+
async click(locator = '//body', context = null) {
|
|
1150
1219
|
return proceedClick.call(this, locator, context)
|
|
1151
1220
|
}
|
|
1152
1221
|
|
|
@@ -1306,6 +1375,49 @@ class Puppeteer extends Helper {
|
|
|
1306
1375
|
return proceedClick.call(this, locator, context, { button: 'right' })
|
|
1307
1376
|
}
|
|
1308
1377
|
|
|
1378
|
+
/**
|
|
1379
|
+
* Performs click at specific coordinates.
|
|
1380
|
+
* If locator is provided, the coordinates are relative to the element.
|
|
1381
|
+
* If locator is not provided, the coordinates are global page coordinates.
|
|
1382
|
+
*
|
|
1383
|
+
* ```js
|
|
1384
|
+
* // Click at global coordinates (100, 200)
|
|
1385
|
+
* I.clickXY(100, 200);
|
|
1386
|
+
*
|
|
1387
|
+
* // Click at coordinates (50, 30) relative to element
|
|
1388
|
+
* I.clickXY('#someElement', 50, 30);
|
|
1389
|
+
* ```
|
|
1390
|
+
*
|
|
1391
|
+
* @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
|
|
1392
|
+
* @param {number} [x] X coordinate relative to element, or Y coordinate if locator is a number.
|
|
1393
|
+
* @param {number} [y] Y coordinate relative to element.
|
|
1394
|
+
* @returns {Promise<void>}
|
|
1395
|
+
*/
|
|
1396
|
+
async clickXY(locator, x, y) {
|
|
1397
|
+
// If locator is a number, treat it as global X coordinate
|
|
1398
|
+
if (typeof locator === 'number') {
|
|
1399
|
+
const globalX = locator
|
|
1400
|
+
const globalY = x
|
|
1401
|
+
await this.page.mouse.click(globalX, globalY)
|
|
1402
|
+
return this._waitForAction()
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Locator is provided, click relative to element
|
|
1406
|
+
const els = await this._locate(locator)
|
|
1407
|
+
assertElementExists(els, locator, 'Element to click')
|
|
1408
|
+
|
|
1409
|
+
const box = await els[0].boundingBox()
|
|
1410
|
+
if (!box) {
|
|
1411
|
+
throw new Error(`Element ${locator} is not visible or has no bounding box`)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const absoluteX = box.x + x
|
|
1415
|
+
const absoluteY = box.y + y
|
|
1416
|
+
|
|
1417
|
+
await this.page.mouse.click(absoluteX, absoluteY)
|
|
1418
|
+
return this._waitForAction()
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1309
1421
|
/**
|
|
1310
1422
|
* {{> checkOption }}
|
|
1311
1423
|
*/
|
|
@@ -1381,7 +1493,7 @@ class Puppeteer extends Helper {
|
|
|
1381
1493
|
}
|
|
1382
1494
|
|
|
1383
1495
|
/**
|
|
1384
|
-
* _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([
|
|
1496
|
+
* _Note:_ Shortcuts like `'Meta'` + `'A'` do not work on macOS ([puppeteer/puppeteer#1313](https://github.com/puppeteer/puppeteer/issues/1313)).
|
|
1385
1497
|
*
|
|
1386
1498
|
* {{> pressKeyWithKeyNormalization }}
|
|
1387
1499
|
*/
|
|
@@ -1840,7 +1952,7 @@ class Puppeteer extends Helper {
|
|
|
1840
1952
|
*/
|
|
1841
1953
|
async grabHTMLFromAll(locator) {
|
|
1842
1954
|
const els = await this._locate(locator)
|
|
1843
|
-
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML
|
|
1955
|
+
const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML)))
|
|
1844
1956
|
return values
|
|
1845
1957
|
}
|
|
1846
1958
|
|
|
@@ -1863,7 +1975,7 @@ class Puppeteer extends Helper {
|
|
|
1863
1975
|
*/
|
|
1864
1976
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1865
1977
|
const els = await this._locate(locator)
|
|
1866
|
-
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el)))
|
|
1978
|
+
const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))))))
|
|
1867
1979
|
const cssValues = res.map(props => props[toCamelCase(cssProperty)])
|
|
1868
1980
|
|
|
1869
1981
|
return cssValues
|
|
@@ -1988,7 +2100,7 @@ class Puppeteer extends Helper {
|
|
|
1988
2100
|
const array = []
|
|
1989
2101
|
for (let index = 0; index < els.length; index++) {
|
|
1990
2102
|
const a = await this._evaluateHandeInContext((el, attr) => el[attr] || el.getAttribute(attr), els[index], attr)
|
|
1991
|
-
array.push(
|
|
2103
|
+
array.push(a)
|
|
1992
2104
|
}
|
|
1993
2105
|
return array
|
|
1994
2106
|
}
|
|
@@ -2030,6 +2142,12 @@ class Puppeteer extends Helper {
|
|
|
2030
2142
|
|
|
2031
2143
|
this.debug(`Screenshot is saving to ${outputFile}`)
|
|
2032
2144
|
|
|
2145
|
+
// Safety check: ensure page exists and is not closed
|
|
2146
|
+
if (!this.page || this.page.isClosed?.()) {
|
|
2147
|
+
this.debugSection('Screenshot', 'Page is not available, skipping screenshot')
|
|
2148
|
+
return
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2033
2151
|
await this.page.screenshot({
|
|
2034
2152
|
path: outputFile,
|
|
2035
2153
|
fullPage: fullPageOption,
|
|
@@ -2043,7 +2161,7 @@ class Puppeteer extends Helper {
|
|
|
2043
2161
|
|
|
2044
2162
|
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
|
|
2045
2163
|
|
|
2046
|
-
if (activeSessionPage) {
|
|
2164
|
+
if (activeSessionPage && !activeSessionPage.isClosed?.()) {
|
|
2047
2165
|
await activeSessionPage.screenshot({
|
|
2048
2166
|
path: outputFile,
|
|
2049
2167
|
fullPage: fullPageOption,
|
|
@@ -2184,12 +2302,13 @@ class Puppeteer extends Helper {
|
|
|
2184
2302
|
* {{> waitForClickable }}
|
|
2185
2303
|
*/
|
|
2186
2304
|
async waitForClickable(locator, waitTimeout) {
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2305
|
+
const el = await this._locateElement(locator)
|
|
2306
|
+
if (!el) {
|
|
2307
|
+
throw new ElementNotFound(locator, 'Element to wait for clickable')
|
|
2308
|
+
}
|
|
2189
2309
|
|
|
2190
|
-
return this.waitForFunction(isElementClickable, [
|
|
2191
|
-
|
|
2192
|
-
if (/Waiting failed/i.test(errorMessage) || /failed: timeout/i.test(errorMessage)) {
|
|
2310
|
+
return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
|
|
2311
|
+
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2193
2312
|
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
|
|
2194
2313
|
} else {
|
|
2195
2314
|
throw e
|
|
@@ -2494,7 +2613,7 @@ class Puppeteer extends Helper {
|
|
|
2494
2613
|
/**
|
|
2495
2614
|
* Waits for navigation to finish. By default, takes configured `waitForNavigation` option.
|
|
2496
2615
|
*
|
|
2497
|
-
* See [Puppeteer's reference](https://github.com/
|
|
2616
|
+
* See [Puppeteer's reference](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.page.waitfornavigation.md)
|
|
2498
2617
|
*
|
|
2499
2618
|
* @param {*} opts
|
|
2500
2619
|
*/
|
|
@@ -2622,7 +2741,8 @@ class Puppeteer extends Helper {
|
|
|
2622
2741
|
*
|
|
2623
2742
|
* {{> stopRecordingTraffic }}
|
|
2624
2743
|
*/
|
|
2625
|
-
stopRecordingTraffic() {
|
|
2744
|
+
async stopRecordingTraffic() {
|
|
2745
|
+
await this.page.setRequestInterception(false)
|
|
2626
2746
|
stopRecordingTraffic.call(this)
|
|
2627
2747
|
}
|
|
2628
2748
|
|
|
@@ -2760,27 +2880,98 @@ class Puppeteer extends Helper {
|
|
|
2760
2880
|
}
|
|
2761
2881
|
}
|
|
2762
2882
|
|
|
2763
|
-
|
|
2764
|
-
const matchedLocator = new Locator(locator, 'css')
|
|
2765
|
-
|
|
2766
|
-
if (matchedLocator.type === 'react') return findReactElements.call(this, matchedLocator)
|
|
2767
|
-
if (matchedLocator.isRole()) return findByRole.call(this, matcher, matchedLocator)
|
|
2768
|
-
|
|
2769
|
-
if (!matchedLocator.isXPath()) return matcher.$$(matchedLocator.simplify())
|
|
2883
|
+
export default Puppeteer
|
|
2770
2884
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2885
|
+
/**
|
|
2886
|
+
* Find elements using Puppeteer's native element discovery methods
|
|
2887
|
+
* Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
|
|
2888
|
+
* @param {Object} matcher - Puppeteer context to search within
|
|
2889
|
+
* @param {Object|string} locator - Locator specification
|
|
2890
|
+
* @returns {Promise<Array>} Array of ElementHandle objects
|
|
2891
|
+
*/
|
|
2892
|
+
async function findElements(matcher, locator) {
|
|
2893
|
+
// Check if locator is a Locator object with react type, or a raw object with react property
|
|
2894
|
+
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
2895
|
+
if (isReactLocator) return findReactElements.call(this, locator)
|
|
2896
|
+
|
|
2897
|
+
locator = new Locator(locator, 'css')
|
|
2898
|
+
|
|
2899
|
+
// Check if locator is a role locator and call findByRole
|
|
2900
|
+
if (locator.isRole()) return findByRole.call(this, matcher, locator)
|
|
2901
|
+
|
|
2902
|
+
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
|
|
2903
|
+
if (!locator.isXPath()) return matcher.$$(locator.simplify())
|
|
2904
|
+
|
|
2905
|
+
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
2906
|
+
if (puppeteer.default?.defaultBrowserRevision) {
|
|
2907
|
+
return matcher.$$(`xpath/${locator.value}`)
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// For Puppeteer 24.x+, $x method was removed
|
|
2911
|
+
// Use ::-p-xpath() selector syntax
|
|
2912
|
+
// Check if matcher has $$ method (Page, Frame, or ElementHandle)
|
|
2913
|
+
if (matcher && typeof matcher.$$ === 'function') {
|
|
2914
|
+
const xpathSelector = `::-p-xpath(${locator.value})`
|
|
2915
|
+
try {
|
|
2916
|
+
return await matcher.$$(xpathSelector)
|
|
2917
|
+
} catch (e) {
|
|
2918
|
+
// XPath selector may not work on ElementHandle, fall through to evaluate method
|
|
2919
|
+
this.debug && this.debug(`XPath selector failed on ${matcher.constructor?.name}: ${e.message}`)
|
|
2780
2920
|
}
|
|
2781
|
-
// If both methods fail, re-throw the original error
|
|
2782
|
-
throw error
|
|
2783
2921
|
}
|
|
2922
|
+
|
|
2923
|
+
// ElementHandles don't support XPath directly // Search within the element by making XPath relative
|
|
2924
|
+
try {
|
|
2925
|
+
const relativeXPath = locator.value.startsWith('.//') ? locator.value : `.//${locator.value.replace(/^\/\//, '')}`
|
|
2926
|
+
|
|
2927
|
+
// Use the element as context by evaluating XPath from it
|
|
2928
|
+
const elements = await matcher.evaluateHandle((element, xpath) => {
|
|
2929
|
+
const iterator = document.evaluate(xpath, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
|
2930
|
+
const results = []
|
|
2931
|
+
for (let i = 0; i < iterator.snapshotLength; i++) {
|
|
2932
|
+
results.push(iterator.snapshotItem(i))
|
|
2933
|
+
}
|
|
2934
|
+
return results
|
|
2935
|
+
}, relativeXPath)
|
|
2936
|
+
|
|
2937
|
+
// Convert JSHandle to array of ElementHandles
|
|
2938
|
+
const properties = await elements.getProperties()
|
|
2939
|
+
return Array.from(properties.values())
|
|
2940
|
+
} catch (e) {
|
|
2941
|
+
this.debug(`XPath within element failed: ${e.message}`)
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// Fallback: return empty array
|
|
2945
|
+
return []
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* Find a single element using Puppeteer's native element discovery methods
|
|
2950
|
+
* Note: Puppeteer Locator API doesn't have .first() method like Playwright
|
|
2951
|
+
* @param {Object} matcher - Puppeteer context to search within
|
|
2952
|
+
* @param {Object|string} locator - Locator specification
|
|
2953
|
+
* @returns {Promise<Object>} Single ElementHandle object
|
|
2954
|
+
*/
|
|
2955
|
+
async function findElement(matcher, locator) {
|
|
2956
|
+
if (locator.react) return findReactElements.call(this, locator)
|
|
2957
|
+
locator = new Locator(locator, 'css')
|
|
2958
|
+
|
|
2959
|
+
// Check if locator is a role locator and call findByRole
|
|
2960
|
+
if (locator.isRole()) {
|
|
2961
|
+
const elements = await findByRole.call(this, matcher, locator)
|
|
2962
|
+
return elements[0]
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
|
|
2966
|
+
if (!locator.isXPath()) {
|
|
2967
|
+
const elements = await matcher.$$(locator.simplify())
|
|
2968
|
+
return elements[0]
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// For XPath in Puppeteer 24.x+, use the same approach as findElements
|
|
2972
|
+
// $x method was removed, so we use ::-p-xpath() or fallback
|
|
2973
|
+
const elements = await findElements.call(this, matcher, locator)
|
|
2974
|
+
return elements[0]
|
|
2784
2975
|
}
|
|
2785
2976
|
|
|
2786
2977
|
async function proceedClick(locator, context = null, options = {}) {
|
|
@@ -2970,15 +3161,19 @@ async function findFields(locator) {
|
|
|
2970
3161
|
}
|
|
2971
3162
|
|
|
2972
3163
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
2973
|
-
const src = await this.
|
|
2974
|
-
|
|
3164
|
+
const src = await this._locateElement(sourceLocator)
|
|
3165
|
+
if (!src) {
|
|
3166
|
+
throw new ElementNotFound(sourceLocator, 'Source Element')
|
|
3167
|
+
}
|
|
2975
3168
|
|
|
2976
|
-
const dst = await this.
|
|
2977
|
-
|
|
3169
|
+
const dst = await this._locateElement(destinationLocator)
|
|
3170
|
+
if (!dst) {
|
|
3171
|
+
throw new ElementNotFound(destinationLocator, 'Destination Element')
|
|
3172
|
+
}
|
|
2978
3173
|
|
|
2979
|
-
// Note: Using public api .getClickablePoint
|
|
2980
|
-
const dragSource = await getClickablePoint(src
|
|
2981
|
-
const dragDestination = await getClickablePoint(dst
|
|
3174
|
+
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
|
|
3175
|
+
const dragSource = await getClickablePoint(src)
|
|
3176
|
+
const dragDestination = await getClickablePoint(dst)
|
|
2982
3177
|
|
|
2983
3178
|
// Drag start point
|
|
2984
3179
|
await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
|
|
@@ -3148,7 +3343,7 @@ async function getClickablePoint(el) {
|
|
|
3148
3343
|
}
|
|
3149
3344
|
|
|
3150
3345
|
// List of key values to key definitions
|
|
3151
|
-
// https://github.com/
|
|
3346
|
+
// https://github.com/puppeteer/puppeteer/blob/v1.20.0/lib/USKeyboardLayout.js
|
|
3152
3347
|
const keyDefinitionMap = {
|
|
3153
3348
|
0: 'Digit0',
|
|
3154
3349
|
1: 'Digit1',
|
|
@@ -3226,7 +3421,9 @@ function _waitForElement(locator, options) {
|
|
|
3226
3421
|
}
|
|
3227
3422
|
|
|
3228
3423
|
async function findReactElements(locator) {
|
|
3229
|
-
|
|
3424
|
+
// Handle both Locator objects and raw locator objects
|
|
3425
|
+
const resolved = locator.locator ? locator.locator : toLocatorConfig(locator, 'react')
|
|
3426
|
+
this.debug(`Finding React elements: ${JSON.stringify(resolved)}`)
|
|
3230
3427
|
|
|
3231
3428
|
// Use createRequire to access require.resolve in ESM
|
|
3232
3429
|
const { createRequire } = await import('module')
|
|
@@ -3341,4 +3538,3 @@ function createRoleTextMatcher(expected, exactMatch) {
|
|
|
3341
3538
|
return value => typeof value === 'string' && value.includes(target)
|
|
3342
3539
|
}
|
|
3343
3540
|
|
|
3344
|
-
export { Puppeteer as default }
|
package/lib/helper/REST.js
CHANGED
|
@@ -298,6 +298,27 @@ class REST extends Helper {
|
|
|
298
298
|
return this._executeRequest(request)
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Send HEAD request to REST API
|
|
303
|
+
*
|
|
304
|
+
* ```js
|
|
305
|
+
* I.sendHeadRequest('/api/users.json');
|
|
306
|
+
* ```
|
|
307
|
+
*
|
|
308
|
+
* @param {*} url
|
|
309
|
+
* @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
|
|
310
|
+
*
|
|
311
|
+
* @returns {Promise<*>} response
|
|
312
|
+
*/
|
|
313
|
+
async sendHeadRequest(url, headers = {}) {
|
|
314
|
+
const request = {
|
|
315
|
+
baseURL: this._url(url),
|
|
316
|
+
method: 'HEAD',
|
|
317
|
+
headers,
|
|
318
|
+
}
|
|
319
|
+
return this._executeRequest(request)
|
|
320
|
+
}
|
|
321
|
+
|
|
301
322
|
/**
|
|
302
323
|
* Sends POST request to API.
|
|
303
324
|
*
|