From a8d4a8aa52ae77de48d7fef4a915882f34ffe4b0 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 30 Mar 2022 09:33:32 -0700 Subject: [PATCH] fix(text selector): ignore non-leading quote when parsing (#13170) Previously, any unpaired quote in the text selector "escaped" everything till the end of the selector string, and so any subsequent chained selectors, including ">>" separator were ignored. An example of misbehaving selector: `text=19" >> nth=1`. Now, when text selector contains a non-leading quote, selector parser does not assume it should escape ">>" separator and correctly tokenizes all selectors from the chain. Note that this behavior is a workaround for the fact that our text selectors is somewhat poorly defined in this area. That said, this workaround seems to be safe enough. It still does not work for unpaired leading quotes like this: `text="19 >> nth=1`. --- .../src/server/common/selectorParser.ts | 9 +++++- tests/page/selectors-text.spec.ts | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/common/selectorParser.ts b/packages/playwright-core/src/server/common/selectorParser.ts index 9ded595647..7e3de28b4f 100644 --- a/packages/playwright-core/src/server/common/selectorParser.ts +++ b/packages/playwright-core/src/server/common/selectorParser.ts @@ -173,6 +173,13 @@ function parseSelectorString(selector: string): ParsedSelectorStrings { return result; } + const shouldIgnoreTextSelectorQuote = () => { + const prefix = selector.substring(start, index); + const match = prefix.match(/^\s*text\s*=(.*)$/); + // Must be a text selector with some text before the quote. + return !!match && !!match[1]; + }; + while (index < selector.length) { const c = selector[index]; if (c === '\\' && index + 1 < selector.length) { @@ -180,7 +187,7 @@ function parseSelectorString(selector: string): ParsedSelectorStrings { } else if (c === quote) { quote = undefined; index++; - } else if (!quote && (c === '"' || c === '\'' || c === '`')) { + } else if (!quote && (c === '"' || c === '\'' || c === '`') && !shouldIgnoreTextSelectorQuote()) { quote = c; index++; } else if (!quote && c === '>' && selector[index + 1] === '>') { diff --git a/tests/page/selectors-text.spec.ts b/tests/page/selectors-text.spec.ts index f837b7b1ef..5975e5cdc2 100644 --- a/tests/page/selectors-text.spec.ts +++ b/tests/page/selectors-text.spec.ts @@ -303,6 +303,7 @@ it('should be case sensitive if quotes are specified', async ({ page }) => { await page.setContent(`
yo
ya
\nye
`); expect(await page.$eval(`text=yA`, e => e.outerHTML)).toBe('
ya
'); expect(await page.$(`text="yA"`)).toBe(null); + expect(await page.$(`text= "ya"`)).toBe(null); }); it('should search for a substring without quotes', async ({ page }) => { @@ -411,3 +412,30 @@ it('should work with leading and trailing spaces', async ({ page }) => { await expect(page.locator('text=Add widget')).toBeVisible(); await expect(page.locator('text= Add widget ')).toBeVisible(); }); + +it('should work with unpaired quotes when not at the start', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/12719' }); + await page.setContent(` +
hello"worldyay
+
hello'worldnay
+
hello\`worldoh
+
hello\`worldoh2
+ `); + expect(await page.$eval('text=lo" >> span', e => e.outerHTML)).toBe('yay'); + expect(await page.$eval(' text=lo" >> span', e => e.outerHTML)).toBe('yay'); + expect(await page.$eval('text =lo" >> span', e => e.outerHTML)).toBe('yay'); + expect(await page.$eval('text= lo" >> span', e => e.outerHTML)).toBe('yay'); + expect(await page.$eval(' text = lo" >> span', e => e.outerHTML)).toBe('yay'); + expect(await page.$eval('text=o"wor >> span', e => e.outerHTML)).toBe('yay'); + + expect(await page.$eval(`text=lo'wor >> span`, e => e.outerHTML)).toBe('nay'); + expect(await page.$eval(`text=o' >> span`, e => e.outerHTML)).toBe('nay'); + + expect(await page.$eval(`text=ello\`wor >> span`, e => e.outerHTML)).toBe('oh'); + await expect(page.locator(`text=ello\`wor`).locator('span').first()).toHaveText('oh'); + await expect(page.locator(`text=ello\`wor`).locator('span').nth(1)).toHaveText('oh2'); + + expect(await page.$(`text='wor >> span`)).toBe(null); + expect(await page.$(`text=" >> span`)).toBe(null); + expect(await page.$(`text=\` >> span`)).toBe(null); +});