* Find elements using Puppeteer's native element discovery methods * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements * @param {Object} matcher - Puppeteer context to search within * @param {Object|string} locator - Locator specification * @return
(matcher, locator)
| 2993 | * @returns {Promise<Array>} Array of ElementHandle objects |
| 2994 | */ |
| 2995 | async function findElements(matcher, locator) { |
| 2996 | locator = new Locator(locator, 'css') |
| 2997 | |
| 2998 | // Check if locator is a role locator and call findByRole |
| 2999 | if (locator.isRole()) return findByRole.call(this, matcher, locator) |
| 3000 | |
| 3001 | // Handle shadow DOM locators with >>> deep descendant combinator |
| 3002 | // { shadow: ['my-app', 'recipe-hello', 'button'] } => 'my-app >>> recipe-hello >>> button' |
| 3003 | if (locator.isShadow()) { |
| 3004 | const shadowSelector = locator.value.join(' >>> ') |
| 3005 | return matcher.$$(shadowSelector) |
| 3006 | } |
| 3007 | |
| 3008 | // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method |
| 3009 | if (!locator.isXPath()) return matcher.$$(locator.simplify()) |
| 3010 | |
| 3011 | // puppeteer version < 19.4.0 is no longer supported. This one is backward support. |
| 3012 | if (puppeteer.default?.defaultBrowserRevision) { |
| 3013 | return matcher.$$(`xpath/${locator.value}`) |
| 3014 | } |
| 3015 | |
| 3016 | // For Puppeteer 24.x+, $x method was removed |
| 3017 | // Use ::-p-xpath() selector syntax |
| 3018 | // Check if matcher has $$ method (Page, Frame, or ElementHandle) |
| 3019 | if (matcher && typeof matcher.$$ === 'function') { |
| 3020 | const xpathSelector = `::-p-xpath(${locator.value})` |
| 3021 | try { |
| 3022 | return await matcher.$$(xpathSelector) |
| 3023 | } catch (e) { |
| 3024 | // XPath selector may not work on ElementHandle, fall through to evaluate method |
| 3025 | this.debug && this.debug(`XPath selector failed on ${matcher.constructor?.name}: ${e.message}`) |
| 3026 | } |
| 3027 | } |
| 3028 | |
| 3029 | // ElementHandles don't support XPath directly // Search within the element by making XPath relative |
| 3030 | try { |
| 3031 | const relativeXPath = locator.value.startsWith('.//') ? locator.value : `.//${locator.value.replace(/^\/\//, '')}` |
| 3032 | |
| 3033 | // Use the element as context by evaluating XPath from it |
| 3034 | const elements = await matcher.evaluateHandle((element, xpath) => { |
| 3035 | const iterator = document.evaluate(xpath, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null) |
| 3036 | const results = [] |
| 3037 | for (let i = 0; i < iterator.snapshotLength; i++) { |
| 3038 | results.push(iterator.snapshotItem(i)) |
| 3039 | } |
| 3040 | return results |
| 3041 | }, relativeXPath) |
| 3042 | |
| 3043 | // Convert JSHandle to array of ElementHandles |
| 3044 | const properties = await elements.getProperties() |
| 3045 | return Array.from(properties.values()) |
| 3046 | } catch (e) { |
| 3047 | this.debug(`XPath within element failed: ${e.message}`) |
| 3048 | } |
| 3049 | |
| 3050 | // Fallback: return empty array |
| 3051 | return [] |
| 3052 | } |