@@ -634,9 +634,11 @@ class Puppeteer extends Helper {
634634 return
635635 }
636636
637- const els = await this . _locate ( locator )
638- assertElementExists ( els , locator )
639- this . context = els [ 0 ]
637+ const el = await this . _locateElement ( locator )
638+ if ( ! el ) {
639+ throw new ElementNotFound ( locator , 'Element for within context' )
640+ }
641+ this . context = el
640642
641643 this . withinLocator = new Locator ( locator )
642644 }
@@ -730,11 +732,13 @@ class Puppeteer extends Helper {
730732 * {{ react }}
731733 */
732734 async moveCursorTo ( locator , offsetX = 0 , offsetY = 0 ) {
733- const els = await this . _locate ( locator )
734- assertElementExists ( els , locator )
735+ const el = await this . _locateElement ( locator )
736+ if ( ! el ) {
737+ throw new ElementNotFound ( locator , 'Element to move cursor to' )
738+ }
735739
736740 // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
737- const { x, y } = await getClickablePoint ( els [ 0 ] )
741+ const { x, y } = await getClickablePoint ( el )
738742 await this . page . mouse . move ( x + offsetX , y + offsetY )
739743 return this . _waitForAction ( )
740744 }
@@ -744,9 +748,10 @@ class Puppeteer extends Helper {
744748 *
745749 */
746750 async focus ( locator ) {
747- const els = await this . _locate ( locator )
748- assertElementExists ( els , locator , 'Element to focus' )
749- const el = els [ 0 ]
751+ const el = await this . _locateElement ( locator )
752+ if ( ! el ) {
753+ throw new ElementNotFound ( locator , 'Element to focus' )
754+ }
750755
751756 await el . click ( )
752757 await el . focus ( )
@@ -758,10 +763,12 @@ class Puppeteer extends Helper {
758763 *
759764 */
760765 async blur ( locator ) {
761- const els = await this . _locate ( locator )
762- assertElementExists ( els , locator , 'Element to blur' )
766+ const el = await this . _locateElement ( locator )
767+ if ( ! el ) {
768+ throw new ElementNotFound ( locator , 'Element to blur' )
769+ }
763770
764- await blurElement ( els [ 0 ] , this . page )
771+ await blurElement ( el , this . page )
765772 return this . _waitForAction ( )
766773 }
767774
@@ -810,11 +817,12 @@ class Puppeteer extends Helper {
810817 }
811818
812819 if ( locator ) {
813- const els = await this . _locate ( locator )
814- assertElementExists ( els , locator , 'Element' )
815- const el = els [ 0 ]
820+ const el = await this . _locateElement ( locator )
821+ if ( ! el ) {
822+ throw new ElementNotFound ( locator , 'Element to scroll into view' )
823+ }
816824 await el . evaluate ( el => el . scrollIntoView ( ) )
817- const elementCoordinates = await getClickablePoint ( els [ 0 ] )
825+ const elementCoordinates = await getClickablePoint ( el )
818826 await this . executeScript ( ( x , y ) => window . scrollBy ( x , y ) , elementCoordinates . x + offsetX , elementCoordinates . y + offsetY )
819827 } else {
820828 await this . executeScript ( ( x , y ) => window . scrollTo ( x , y ) , offsetX , offsetY )
@@ -882,6 +890,21 @@ class Puppeteer extends Helper {
882890 return findElements . call ( this , context , locator )
883891 }
884892
893+ /**
894+ * Get single element by different locator types, including strict locator
895+ * Should be used in custom helpers:
896+ *
897+ * ```js
898+ * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
899+ * ```
900+ *
901+ * {{ react }}
902+ */
903+ async _locateElement ( locator ) {
904+ const context = await this . context
905+ return findElement . call ( this , context , locator )
906+ }
907+
885908 /**
886909 * Find a checkbox by providing human-readable text:
887910 * NOTE: Assumes the checkable element exists
@@ -893,7 +916,9 @@ class Puppeteer extends Helper {
893916 async _locateCheckable ( locator , providedContext = null ) {
894917 const context = providedContext || ( await this . _getContext ( ) )
895918 const els = await findCheckable . call ( this , locator , context )
896- assertElementExists ( els [ 0 ] , locator , 'Checkbox or radio' )
919+ if ( ! els || els . length === 0 ) {
920+ throw new ElementNotFound ( locator , 'Checkbox or radio' )
921+ }
897922 return els [ 0 ]
898923 }
899924
@@ -2124,10 +2149,12 @@ class Puppeteer extends Helper {
21242149 * {{> waitForClickable }}
21252150 */
21262151 async waitForClickable ( locator , waitTimeout ) {
2127- const els = await this . _locate ( locator )
2128- assertElementExists ( els , locator )
2152+ const el = await this . _locateElement ( locator )
2153+ if ( ! el ) {
2154+ throw new ElementNotFound ( locator , 'Element to wait for clickable' )
2155+ }
21292156
2130- return this . waitForFunction ( isElementClickable , [ els [ 0 ] ] , waitTimeout ) . catch ( async e => {
2157+ return this . waitForFunction ( isElementClickable , [ el ] , waitTimeout ) . catch ( async e => {
21312158 if ( / W a i t i n g f a i l e d / i. test ( e . message ) || / f a i l e d : t i m e o u t / i. test ( e . message ) ) {
21322159 throw new Error ( `element ${ new Locator ( locator ) . toString ( ) } still not clickable after ${ waitTimeout || this . options . waitForTimeout / 1000 } sec` )
21332160 } else {
@@ -2701,9 +2728,52 @@ class Puppeteer extends Helper {
27012728
27022729module . exports = Puppeteer
27032730
2731+ /**
2732+ * Build locator string for Puppeteer's Locator API
2733+ * Similar to Playwright's buildLocatorString function for consistency
2734+ * @param {Locator } locator - CodeceptJS Locator object
2735+ * @returns {string } Locator string compatible with Puppeteer's locator() method
2736+ */
2737+ function buildLocatorString ( locator ) {
2738+ if ( locator . isCustom ( ) ) {
2739+ return `${ locator . type } =${ locator . value } `
2740+ }
2741+ if ( locator . isXPath ( ) ) {
2742+ return `xpath=${ locator . value } `
2743+ }
2744+ return locator . simplify ( )
2745+ }
2746+
2747+ /**
2748+ * Find elements using Puppeteer's modern Locator API with ElementHandle fallback
2749+ * This provides better reliability and waiting behavior while maintaining backward compatibility
2750+ * @param {Page|Frame|ElementHandle } matcher - Puppeteer context to search within
2751+ * @param {Object|string } locator - Locator specification
2752+ * @returns {Promise<ElementHandle[]> } Array of ElementHandle objects for compatibility
2753+ */
27042754async function findElements ( matcher , locator ) {
27052755 if ( locator . react ) return findReactElements . call ( this , locator )
27062756 locator = new Locator ( locator , 'css' )
2757+
2758+ // Use Locator API for better reliability, then convert to ElementHandles for compatibility
2759+ if ( matcher . locator ) {
2760+ const locatorElement = matcher . locator ( buildLocatorString ( locator ) )
2761+ const handles = await locatorElement . all ( )
2762+ // Convert Locator elements to ElementHandles for backward compatibility
2763+ return Promise . all (
2764+ handles . map ( async handle => {
2765+ // For Puppeteer Locators, we can get ElementHandle using waitHandle()
2766+ try {
2767+ return await handle . waitHandle ( )
2768+ } catch ( e ) {
2769+ // Fallback for edge cases
2770+ return handle
2771+ }
2772+ } ) ,
2773+ )
2774+ }
2775+
2776+ // Fallback to legacy approach if Locator not available
27072777 if ( ! locator . isXPath ( ) ) return matcher . $$ ( locator . simplify ( ) )
27082778 // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
27092779 if ( puppeteer . default ?. defaultBrowserRevision ) {
@@ -2712,6 +2782,43 @@ async function findElements(matcher, locator) {
27122782 return matcher . $x ( locator . value )
27132783}
27142784
2785+ /**
2786+ * Find a single element using Puppeteer's modern Locator API with ElementHandle fallback
2787+ * @param {Page|Frame|ElementHandle } matcher - Puppeteer context to search within
2788+ * @param {Object|string } locator - Locator specification
2789+ * @returns {Promise<ElementHandle> } Single ElementHandle object for compatibility
2790+ */
2791+ async function findElement ( matcher , locator ) {
2792+ if ( locator . react ) return findReactElements . call ( this , locator )
2793+ locator = new Locator ( locator , 'css' )
2794+
2795+ // Use Locator API for better reliability, then get first ElementHandle for compatibility
2796+ if ( matcher . locator ) {
2797+ const locatorElement = matcher . locator ( buildLocatorString ( locator ) )
2798+ const handle = await locatorElement . first ( )
2799+ // Convert to ElementHandle for backward compatibility
2800+ try {
2801+ return await handle . waitHandle ( )
2802+ } catch ( e ) {
2803+ // Fallback for edge cases
2804+ return handle
2805+ }
2806+ }
2807+
2808+ // Fallback to legacy approach if Locator not available
2809+ if ( ! locator . isXPath ( ) ) {
2810+ const elements = await matcher . $$ ( locator . simplify ( ) )
2811+ return elements [ 0 ]
2812+ }
2813+ // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2814+ if ( puppeteer . default ?. defaultBrowserRevision ) {
2815+ const elements = await matcher . $$ ( `xpath/${ locator . value } ` )
2816+ return elements [ 0 ]
2817+ }
2818+ const elements = await matcher . $x ( locator . value )
2819+ return elements [ 0 ]
2820+ }
2821+
27152822async function proceedClick ( locator , context = null , options = { } ) {
27162823 let matcher = await this . context
27172824 if ( context ) {
@@ -2857,15 +2964,19 @@ async function findFields(locator) {
28572964}
28582965
28592966async function proceedDragAndDrop ( sourceLocator , destinationLocator ) {
2860- const src = await this . _locate ( sourceLocator )
2861- assertElementExists ( src , sourceLocator , 'Source Element' )
2967+ const src = await this . _locateElement ( sourceLocator )
2968+ if ( ! src ) {
2969+ throw new ElementNotFound ( sourceLocator , 'Source Element' )
2970+ }
28622971
2863- const dst = await this . _locate ( destinationLocator )
2864- assertElementExists ( dst , destinationLocator , 'Destination Element' )
2972+ const dst = await this . _locateElement ( destinationLocator )
2973+ if ( ! dst ) {
2974+ throw new ElementNotFound ( destinationLocator , 'Destination Element' )
2975+ }
28652976
2866- // Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
2867- const dragSource = await getClickablePoint ( src [ 0 ] )
2868- const dragDestination = await getClickablePoint ( dst [ 0 ] )
2977+ // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
2978+ const dragSource = await getClickablePoint ( src )
2979+ const dragDestination = await getClickablePoint ( dst )
28692980
28702981 // Drag start point
28712982 await this . page . mouse . move ( dragSource . x , dragSource . y , { steps : 5 } )
0 commit comments