@@ -106,23 +106,121 @@ export async function findByText(options = {}) {
106106}
107107
108108/**
109- * Normalize selector to handle Puppeteer text selectors
109+ * Check if a selector is a Playwright-specific text selector
110+ * @param {string } selector - The selector to check
111+ * @returns {boolean } - True if selector contains Playwright text pseudo-selectors
112+ */
113+ function isPlaywrightTextSelector ( selector ) {
114+ if ( typeof selector !== 'string' ) {
115+ return false ;
116+ }
117+ return selector . includes ( ':has-text(' ) || selector . includes ( ':text-is(' ) ;
118+ }
119+
120+ /**
121+ * Parse a Playwright text selector to extract base selector and text
122+ * @param {string } selector - Playwright text selector like 'a:has-text("text")'
123+ * @returns {Object|null } - { baseSelector, text, exact } or null if not parseable
124+ */
125+ function parsePlaywrightTextSelector ( selector ) {
126+ // Match patterns like 'a:has-text("text")' or 'button:text-is("exact text")'
127+ const hasTextMatch = selector . match ( / ^ ( .+ ?) : h a s - t e x t \( " ( .+ ?) " \) $ / ) ;
128+ if ( hasTextMatch ) {
129+ return {
130+ baseSelector : hasTextMatch [ 1 ] ,
131+ text : hasTextMatch [ 2 ] ,
132+ exact : false ,
133+ } ;
134+ }
135+
136+ const textIsMatch = selector . match ( / ^ ( .+ ?) : t e x t - i s \( " ( .+ ?) " \) $ / ) ;
137+ if ( textIsMatch ) {
138+ return {
139+ baseSelector : textIsMatch [ 1 ] ,
140+ text : textIsMatch [ 2 ] ,
141+ exact : true ,
142+ } ;
143+ }
144+
145+ return null ;
146+ }
147+
148+ /**
149+ * Normalize selector to handle both Puppeteer and Playwright text selectors
150+ * Converts engine-specific text selectors to valid CSS selectors for browser context
151+ *
110152 * @param {Object } options - Configuration options
111153 * @param {Object } options.page - Browser page object
154+ * @param {string } options.engine - Engine type ('playwright' or 'puppeteer')
112155 * @param {string|Object } options.selector - CSS selector or text selector object
113- * @returns {Promise<string|null> } - CSS selector or null if not found
156+ * @returns {Promise<string|null> } - Valid CSS selector or null if not found
114157 */
115158export async function normalizeSelector ( options = { } ) {
116- const { page, selector } = options ;
159+ const { page, engine , selector } = options ;
117160
118161 if ( ! selector ) {
119162 throw new Error ( 'selector is required in options' ) ;
120163 }
121164
165+ // Handle Playwright text selectors (strings containing :has-text or :text-is)
166+ // These are valid for Playwright's locator API but NOT for document.querySelectorAll
167+ if (
168+ typeof selector === 'string' &&
169+ engine === 'playwright' &&
170+ isPlaywrightTextSelector ( selector )
171+ ) {
172+ const parsed = parsePlaywrightTextSelector ( selector ) ;
173+ if ( ! parsed ) {
174+ // Could not parse, return as-is and hope for the best
175+ return selector ;
176+ }
177+
178+ try {
179+ // Use page.evaluate to find matching element and generate a valid CSS selector
180+ const result = await page . evaluate ( ( { baseSelector, text, exact } ) => {
181+ const elements = Array . from ( document . querySelectorAll ( baseSelector ) ) ;
182+ const matchingElement = elements . find ( ( el ) => {
183+ const elementText = el . textContent . trim ( ) ;
184+ return exact ? elementText === text : elementText . includes ( text ) ;
185+ } ) ;
186+
187+ if ( ! matchingElement ) {
188+ return null ;
189+ }
190+
191+ // Generate a unique selector using data-qa or nth-of-type
192+ const dataQa = matchingElement . getAttribute ( 'data-qa' ) ;
193+ if ( dataQa ) {
194+ return `[data-qa="${ dataQa } "]` ;
195+ }
196+
197+ // Use nth-of-type as fallback
198+ const tagName = matchingElement . tagName . toLowerCase ( ) ;
199+ const siblings = Array . from (
200+ matchingElement . parentElement . children
201+ ) . filter ( ( el ) => el . tagName . toLowerCase ( ) === tagName ) ;
202+ const index = siblings . indexOf ( matchingElement ) ;
203+ return `${ tagName } :nth-of-type(${ index + 1 } )` ;
204+ } , parsed ) ;
205+
206+ return result ;
207+ } catch ( error ) {
208+ if ( isNavigationError ( error ) ) {
209+ console . log (
210+ '⚠️ Navigation detected during normalizeSelector (Playwright), returning null'
211+ ) ;
212+ return null ;
213+ }
214+ throw error ;
215+ }
216+ }
217+
218+ // Plain string selector - return as-is
122219 if ( typeof selector === 'string' ) {
123220 return selector ;
124221 }
125222
223+ // Handle Puppeteer text selector objects
126224 if ( selector . _isPuppeteerTextSelector ) {
127225 try {
128226 // Find element by text and generate a unique selector
@@ -161,7 +259,7 @@ export async function normalizeSelector(options = {}) {
161259 } catch ( error ) {
162260 if ( isNavigationError ( error ) ) {
163261 console . log (
164- '⚠️ Navigation detected during normalizeSelector, returning null'
262+ '⚠️ Navigation detected during normalizeSelector (Puppeteer) , returning null'
165263 ) ;
166264 return null ;
167265 }
@@ -183,13 +281,25 @@ export function withTextSelectorSupport(fn, engine, page) {
183281 return async ( options = { } ) => {
184282 let { selector } = options ;
185283
186- // Normalize Puppeteer text selectors
284+ // Normalize Puppeteer text selectors (object format)
187285 if (
188286 engine === 'puppeteer' &&
189287 typeof selector === 'object' &&
190288 selector . _isPuppeteerTextSelector
191289 ) {
192- selector = await normalizeSelector ( { page, selector } ) ;
290+ selector = await normalizeSelector ( { page, engine, selector } ) ;
291+ if ( ! selector ) {
292+ throw new Error ( 'Element with specified text not found' ) ;
293+ }
294+ }
295+
296+ // Normalize Playwright text selectors (string format with :has-text or :text-is)
297+ if (
298+ engine === 'playwright' &&
299+ typeof selector === 'string' &&
300+ isPlaywrightTextSelector ( selector )
301+ ) {
302+ selector = await normalizeSelector ( { page, engine, selector } ) ;
193303 if ( ! selector ) {
194304 throw new Error ( 'Element with specified text not found' ) ;
195305 }
0 commit comments