diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 50cb762e37..c5ea914e24 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -57,12 +57,12 @@ export class Locator implements api.Locator { private static getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string { if (!isString(text)) - return `attr=[${attrName}=${text}]`; - return `attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`; + return `internal:attr=[${attrName}=${text}]`; + return `internal:attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`; } static getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string { - return Locator.getByTextSelector(text, options) + ' >> control=resolve-label'; + return 'internal:label=' + escapeForTextSelector(text, !!options?.exact); } static getByAltTextSelector(text: string | RegExp, options?: { exact?: boolean }): string { @@ -108,14 +108,14 @@ export class Locator implements api.Locator { if (options?.hasText) { const textSelector = 'text=' + escapeForTextSelector(options.hasText, false); - this._selector += ` >> has=${JSON.stringify(textSelector)}`; + this._selector += ` >> internal:has=${JSON.stringify(textSelector)}`; } if (options?.has) { const locator = options.has; if (locator._frame !== frame) throw new Error(`Inner "has" locator must belong to the same frame.`); - this._selector += ` >> has=` + JSON.stringify(locator._selector); + this._selector += ` >> internal:has=` + JSON.stringify(locator._selector); } } @@ -395,7 +395,7 @@ export class FrameLocator implements api.FrameLocator { } locator(selector: string, options?: { hasText?: string | RegExp }): Locator { - return new Locator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector, options); + return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector, options); } getByTestId(testId: string): Locator { @@ -427,7 +427,7 @@ export class FrameLocator implements api.FrameLocator { } frameLocator(selector: string): FrameLocator { - return new FrameLocator(this._frame, this._frameSelector + ' >> control=enter-frame >> ' + selector); + return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector); } first(): FrameLocator { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 6760b3753f..464a5d4d50 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1501,7 +1501,7 @@ export class Frame extends SdkObject { return this.retryWithProgress(progress, selector, options, async selectorInFrame => { // Be careful, |this| can be different from |frame|. progress.log(`waiting for selector "${selector}"`); - const { frame, info } = selectorInFrame || { frame: this, info: { parsed: { parts: [{ name: 'control', body: 'return-empty', source: 'control=return-empty' }] }, world: 'utility', strict: !!options.strict } }; + const { frame, info } = selectorInFrame || { frame: this, info: { parsed: { parts: [{ name: 'internal:control', body: 'return-empty', source: 'internal:control=return-empty' }] }, world: 'utility', strict: !!options.strict } }; return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options); }); } diff --git a/packages/playwright-core/src/server/injected/consoleApi.ts b/packages/playwright-core/src/server/injected/consoleApi.ts index cea839dd74..d0d2752ab8 100644 --- a/packages/playwright-core/src/server/injected/consoleApi.ts +++ b/packages/playwright-core/src/server/injected/consoleApi.ts @@ -28,10 +28,10 @@ function createLocator(injectedScript: InjectedScript, initial: string, options? this.selector = selector; if (options?.hasText) { const textSelector = 'text=' + escapeForTextSelector(options.hasText, false); - this.selector += ` >> has=${JSON.stringify(textSelector)}`; + this.selector += ` >> internal:has=${JSON.stringify(textSelector)}`; } if (options?.has) - this.selector += ` >> has=` + JSON.stringify(options.has.selector); + this.selector += ` >> internal:has=` + JSON.stringify(options.has.selector); const parsed = injectedScript.parseSelector(this.selector); this.element = injectedScript.querySelector(parsed, document, false); this.elements = injectedScript.querySelectorAll(parsed, document); diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 804896d1a9..4a48b26e32 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -22,7 +22,7 @@ import { RoleEngine } from './roleSelectorEngine'; import { parseAttributeSelector } from '../isomorphic/selectorParser'; import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser'; import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser'; -import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText } from './selectorUtils'; +import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText, createStrictFullTextMatcher } from './selectorUtils'; import { SelectorEvaluatorImpl } from './selectorEvaluator'; import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils'; import type { CSSComplexSelectorList } from '../isomorphic/cssParser'; @@ -103,9 +103,10 @@ export class InjectedScript { this._engines.set('css', this._createCSSEngine()); this._engines.set('nth', { queryAll: () => [] }); this._engines.set('visible', this._createVisibleEngine()); - this._engines.set('control', this._createControlEngine()); - this._engines.set('has', this._createHasEngine()); - this._engines.set('attr', this._createNamedAttributeEngine()); + this._engines.set('internal:control', this._createControlEngine()); + this._engines.set('internal:has', this._createHasEngine()); + this._engines.set('internal:label', this._createLabelEngine()); + this._engines.set('internal:attr', this._createNamedAttributeEngine()); for (const { name, engine } of customEngines) this._engines.set(name, engine); @@ -173,7 +174,7 @@ export class InjectedScript { const withHas: ParsedSelector = { parts: selector.parts.slice(0, selector.capture + 1) }; if (selector.capture < selector.parts.length - 1) { const parsed: ParsedSelector = { parts: selector.parts.slice(selector.capture + 1) }; - const has: ParsedSelectorPart = { name: 'has', body: { parsed }, source: stringifySelector(parsed) }; + const has: ParsedSelectorPart = { name: 'internal:has', body: { parsed }, source: stringifySelector(parsed) }; withHas.parts.push(has); } return this.querySelectorAll(withHas, root); @@ -243,7 +244,7 @@ export class InjectedScript { private _createTextEngine(shadow: boolean): SelectorEngine { const queryList = (root: SelectorRoot, selector: string): Element[] => { - const { matcher, kind } = createTextMatcher(selector); + const { matcher, kind } = createTextMatcher(selector, false); const result: Element[] = []; let lastDidNotMatchSelf: Element | null = null; @@ -273,6 +274,23 @@ export class InjectedScript { }; } + private _createLabelEngine(): SelectorEngine { + const evaluator = this._evaluator; + return { + queryAll: (root: SelectorRoot, selector: string): Element[] => { + const { matcher } = createTextMatcher(selector, true); + const result: Element[] = []; + const labels = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, 'label') as HTMLLabelElement[]; + for (const label of labels) { + const control = label.control; + if (control && matcher(elementText(evaluator._cacheText, label))) + result.push(control); + } + return result; + } + }; + } + private _createNamedAttributeEngine(): SelectorEngine { const queryList = (root: SelectorRoot, selector: string): Element[] => { const parsed = parseAttributeSelector(selector, true); @@ -305,10 +323,6 @@ export class InjectedScript { return []; if (body === 'return-empty') return []; - if (body === 'resolve-label') { - const control = (root as HTMLLabelElement).control; - return control ? [control] : []; - } if (body === 'component') { if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) return []; @@ -316,7 +330,7 @@ export class InjectedScript { // However, when mounting fragments, return the root instead. return [root.childElementCount === 1 ? root.firstElementChild! : root as Element]; } - throw new Error(`Internal error, unknown control selector ${body}`); + throw new Error(`Internal error, unknown internal:control selector ${body}`); } }; } @@ -1280,7 +1294,7 @@ function unescape(s: string): string { return r.join(''); } -function createTextMatcher(selector: string): { matcher: TextMatcher, kind: 'regex' | 'strict' | 'lax' } { +function createTextMatcher(selector: string, strictMatchesFullText: boolean): { matcher: TextMatcher, kind: 'regex' | 'strict' | 'lax' } { if (selector[0] === '/' && selector.lastIndexOf('/') > 0) { const lastSlash = selector.lastIndexOf('/'); const matcher: TextMatcher = createRegexTextMatcher(selector.substring(1, lastSlash), selector.substring(lastSlash + 1)); @@ -1295,8 +1309,9 @@ function createTextMatcher(selector: string): { matcher: TextMatcher, kind: 'reg selector = unescape(selector.substring(1, selector.length - 1)); strict = true; } - const matcher = strict ? createStrictTextMatcher(selector) : createLaxTextMatcher(selector); - return { matcher, kind: strict ? 'strict' : 'lax' }; + if (strict) + return { matcher: strictMatchesFullText ? createStrictFullTextMatcher(selector) : createStrictTextMatcher(selector), kind: 'strict' }; + return { matcher: createLaxTextMatcher(selector), kind: 'lax' }; } class ExpectedTextMatcher { diff --git a/packages/playwright-core/src/server/injected/selectorGenerator.ts b/packages/playwright-core/src/server/injected/selectorGenerator.ts index 35ab65efe8..c091256814 100644 --- a/packages/playwright-core/src/server/injected/selectorGenerator.ts +++ b/packages/playwright-core/src/server/injected/selectorGenerator.ts @@ -148,7 +148,7 @@ function buildCandidates(element: Element, accessibleNameCache: Map { + return elementText.full.trim().replace(/\s+/g, ' ') === text; + }; +} + export function createRegexTextMatcher(source: string, flags?: string): TextMatcher { const re = new RegExp(source, flags); return (elementText: ElementText) => { diff --git a/packages/playwright-core/src/server/isomorphic/selectorParser.ts b/packages/playwright-core/src/server/isomorphic/selectorParser.ts index bb931f867f..d71af4c117 100644 --- a/packages/playwright-core/src/server/isomorphic/selectorParser.ts +++ b/packages/playwright-core/src/server/isomorphic/selectorParser.ts @@ -19,7 +19,7 @@ import { InvalidSelectorError, parseCSS } from './cssParser'; export { InvalidSelectorError, isInvalidSelectorError } from './cssParser'; export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number }; -const kNestedSelectorNames = new Set(['has', 'left-of', 'right-of', 'above', 'below', 'near']); +const kNestedSelectorNames = new Set(['internal:has', 'left-of', 'right-of', 'above', 'below', 'near']); const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']); export type ParsedSelectorPart = { @@ -70,7 +70,7 @@ export function parseSelector(selector: string): ParsedSelector { throw new Error(`Malformed selector: ${part.name}=` + part.body); } const result = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } }; - if (result.body.parsed.parts.some(part => part.name === 'control' && part.body === 'enter-frame')) + if (result.body.parsed.parts.some(part => part.name === 'internal:control' && part.body === 'enter-frame')) throw new Error(`Frames are not allowed inside "${part.name}" selectors`); return result; } @@ -93,7 +93,7 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] { let chunkStartIndex = 0; for (let i = 0; i < selector.parts.length; ++i) { const part = selector.parts[i]; - if (part.name === 'control' && part.body === 'enter-frame') { + if (part.name === 'internal:control' && part.body === 'enter-frame') { if (!chunk.parts.length) throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first'); result.push(chunk); diff --git a/packages/playwright-core/src/server/recorder/language.ts b/packages/playwright-core/src/server/recorder/language.ts index fc1e8751b5..c6a281b490 100644 --- a/packages/playwright-core/src/server/recorder/language.ts +++ b/packages/playwright-core/src/server/recorder/language.ts @@ -121,7 +121,7 @@ export function asLocator(generator: LanguageGenerator, selector: string, isFram } } - if (part.name === 'attr') { + if (part.name === 'internal:attr') { const attrSelector = parseAttributeSelector(part.body as string, true); const { name, value } = attrSelector.attributes[0]; if (name === 'data-testid') { diff --git a/packages/playwright-core/src/server/selectors.ts b/packages/playwright-core/src/server/selectors.ts index ea449efc4e..9765f0a378 100644 --- a/packages/playwright-core/src/server/selectors.ts +++ b/packages/playwright-core/src/server/selectors.ts @@ -45,8 +45,8 @@ export class Selectors { 'data-testid', 'data-testid:light', 'data-test-id', 'data-test-id:light', 'data-test', 'data-test:light', - 'nth', 'visible', 'control', 'has', - 'role', 'attr' + 'nth', 'visible', 'internal:control', 'internal:has', + 'role', 'internal:attr', 'internal:label' ]); this._builtinEnginesInMainWorld = new Set([ '_react', '_vue', diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 8f0727732f..d759e658db 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -64,7 +64,7 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean, cas if (exact) return '"' + text.replace(/["]/g, '\\"') + '"'; if (text.includes('"') || text.includes('>>') || text[0] === '/') - return `/.*${escapeForRegex(text).replace(/\s+/, '\\s+')}.*/` + (caseSensitive ? '' : 'i'); + return `/${escapeForRegex(text).replace(/\s+/, '\\s+')}/` + (caseSensitive ? '' : 'i'); return text; } diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts index 33711783de..d4a14c8fe9 100644 --- a/packages/playwright-test/src/mount.ts +++ b/packages/playwright-test/src/mount.ts @@ -124,7 +124,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: await window.playwrightMount(component, rootElement, hooksConfig); - return '#root >> control=component'; + return '#root >> internal:control=component'; }, { component, hooksConfig: options.hooksConfig }); return selector; } diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 298646f4a9..52421de213 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -235,7 +235,7 @@ test.describe('cli codegen', () => { await recorder.setContentAndWait(`
Submit
`); const selector = await recorder.hoverOverElement('div'); - expect(selector).toBe('attr=[data-testid="testid"]'); + expect(selector).toBe('internal:attr=[data-testid="testid"]'); const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), @@ -267,7 +267,7 @@ test.describe('cli codegen', () => { await recorder.setContentAndWait(``); const selector = await recorder.hoverOverElement('input'); - expect(selector).toBe('attr=[placeholder="Country"]'); + expect(selector).toBe('internal:attr=[placeholder="Country"]'); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), @@ -296,7 +296,7 @@ test.describe('cli codegen', () => { await recorder.setContentAndWait(``); const selector = await recorder.hoverOverElement('input'); - expect(selector).toBe('attr=[alt="Country"]'); + expect(selector).toBe('internal:attr=[alt="Country"]'); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), diff --git a/tests/library/selector-generator.spec.ts b/tests/library/selector-generator.spec.ts index d6358c1738..e18138041c 100644 --- a/tests/library/selector-generator.spec.ts +++ b/tests/library/selector-generator.spec.ts @@ -45,7 +45,7 @@ it.describe('selector generator', () => { it('should not escape spaces inside named attr selectors', async ({ page }) => { await page.setContent(``); - expect(await generate(page, 'input')).toBe('attr=[placeholder=\"Foo b ar\"]'); + expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Foo b ar\"]'); }); it('should generate text for ', async ({ page }) => { @@ -60,17 +60,17 @@ it.describe('selector generator', () => { it('should escape text with >>', async ({ page }) => { await page.setContent(`
text>>text
`); - expect(await generate(page, 'div')).toBe('text=/.*text\\>\\>text.*/'); + expect(await generate(page, 'div')).toBe('text=/text\\>\\>text/'); }); it('should escape text with quote', async ({ page }) => { await page.setContent(`
text"text
`); - expect(await generate(page, 'div')).toBe('text=/.*text"text.*/'); + expect(await generate(page, 'div')).toBe('text=/text"text/'); }); it('should escape text with slash', async ({ page }) => { await page.setContent(`
/text
`); - expect(await generate(page, 'div')).toBe('text=/.*\/text.*/'); + expect(await generate(page, 'div')).toBe('text=/\/text/'); }); it('should not use text for select', async ({ page }) => { @@ -88,7 +88,7 @@ it.describe('selector generator', () => { it('should prefer data-testid', async ({ page }) => { await page.setContent(`
Text
Text
Text
Text
`); - expect(await generate(page, '[data-testid="a"]')).toBe('attr=[data-testid=\"a\"]'); + expect(await generate(page, '[data-testid="a"]')).toBe('internal:attr=[data-testid=\"a\"]'); }); it('should handle first non-unique data-testid', async ({ page }) => { @@ -99,7 +99,7 @@ it.describe('selector generator', () => {
Text
`); - expect(await generate(page, 'div[mark="1"]')).toBe('attr=[data-testid=\"a\"] >> nth=0'); + expect(await generate(page, 'div[mark="1"]')).toBe('internal:attr=[data-testid=\"a\"] >> nth=0'); }); it('should handle second non-unique data-testid', async ({ page }) => { @@ -110,7 +110,7 @@ it.describe('selector generator', () => {
Text
`); - expect(await generate(page, 'div[mark="1"]')).toBe(`attr=[data-testid=\"a\"] >> nth=1`); + expect(await generate(page, 'div[mark="1"]')).toBe(`internal:attr=[data-testid=\"a\"] >> nth=1`); }); it('should use readable id', async ({ page }) => { @@ -232,7 +232,7 @@ it.describe('selector generator', () => { }); it('placeholder', async ({ page }) => { await page.setContent(``); - expect(await generate(page, 'input')).toBe('attr=[placeholder=\"foobar\"]'); + expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"]'); }); it('type', async ({ page }) => { await page.setContent(``); diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index cd8e789898..80e477254d 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -180,31 +180,3 @@ it('alias methods coverage', async ({ page }) => { await expect(page.locator('div').getByRole('button')).toHaveCount(1); await expect(page.mainFrame().locator('button')).toHaveCount(1); }); - -it('getBy escaping', async ({ page }) => { - await page.setContent(``); - await page.$eval('input', input => { - input.setAttribute('placeholder', 'hello\nwo"rld'); - input.setAttribute('title', 'hello\nwo"rld'); - input.setAttribute('alt', 'hello\nwo"rld'); - }); - await expect(page.getByText('hello\nwo"rld')).toHaveAttribute('id', 'label'); - await expect(page.getByLabel('hello\nwo"rld')).toHaveAttribute('id', 'control'); - await expect(page.getByPlaceholder('hello\nwo"rld')).toHaveAttribute('id', 'control'); - await expect(page.getByAltText('hello\nwo"rld')).toHaveAttribute('id', 'control'); - await expect(page.getByTitle('hello\nwo"rld')).toHaveAttribute('id', 'control'); - - await page.setContent(``); - await page.$eval('input', input => { - input.setAttribute('placeholder', 'hello\nworld'); - input.setAttribute('title', 'hello\nworld'); - input.setAttribute('alt', 'hello\nworld'); - }); - await expect(page.getByText('hello\nworld')).toHaveAttribute('id', 'label'); - await expect(page.getByLabel('hello\nworld')).toHaveAttribute('id', 'control'); - await expect(page.getByPlaceholder('hello\nworld')).toHaveAttribute('id', 'control'); - await expect(page.getByAltText('hello\nworld')).toHaveAttribute('id', 'control'); - await expect(page.getByTitle('hello\nworld')).toHaveAttribute('id', 'control'); -}); diff --git a/tests/page/selectors-frame.spec.ts b/tests/page/selectors-frame.spec.ts index 7c704e734a..7d9f02b45b 100644 --- a/tests/page/selectors-frame.spec.ts +++ b/tests/page/selectors-frame.spec.ts @@ -49,7 +49,7 @@ async function routeIframe(page: Page) { it('should work for iframe @smoke', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const button = page.locator('iframe >> control=enter-frame >> button'); + const button = page.locator('iframe >> internal:control=enter-frame >> button'); await button.waitFor(); expect(await button.innerText()).toBe('Hello iframe'); await expect(button).toHaveText('Hello iframe'); @@ -60,7 +60,7 @@ it('should work for iframe (handle)', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); const body = await page.$('body'); - const button = await body.waitForSelector('iframe >> control=enter-frame >> button'); + const button = await body.waitForSelector('iframe >> internal:control=enter-frame >> button'); expect(await button.innerText()).toBe('Hello iframe'); expect(await button.textContent()).toBe('Hello iframe'); await button.click(); @@ -69,7 +69,7 @@ it('should work for iframe (handle)', async ({ page, server }) => { it('should work for nested iframe', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const button = page.locator('iframe >> control=enter-frame >> iframe >> control=enter-frame >> button'); + const button = page.locator('iframe >> internal:control=enter-frame >> iframe >> internal:control=enter-frame >> button'); await button.waitFor(); expect(await button.innerText()).toBe('Hello nested iframe'); await expect(button).toHaveText('Hello nested iframe'); @@ -80,7 +80,7 @@ it('should work for nested iframe (handle)', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); const body = await page.$('body'); - const button = await body.waitForSelector('iframe >> control=enter-frame >> iframe >> control=enter-frame >> button'); + const button = await body.waitForSelector('iframe >> internal:control=enter-frame >> iframe >> internal:control=enter-frame >> button'); expect(await button.innerText()).toBe('Hello nested iframe'); expect(await button.textContent()).toBe('Hello nested iframe'); await button.click(); @@ -89,35 +89,35 @@ it('should work for nested iframe (handle)', async ({ page, server }) => { it('should work for $ and $$', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const element = await page.$('iframe >> control=enter-frame >> button'); + const element = await page.$('iframe >> internal:control=enter-frame >> button'); expect(await element.textContent()).toBe('Hello iframe'); - const elements = await page.$$('iframe >> control=enter-frame >> span'); + const elements = await page.$$('iframe >> internal:control=enter-frame >> span'); expect(elements).toHaveLength(2); }); it('$ should not wait for frame', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); - expect(await page.$('iframe >> control=enter-frame >> canvas')).toBeFalsy(); + expect(await page.$('iframe >> internal:control=enter-frame >> canvas')).toBeFalsy(); const body = await page.$('body'); - expect(await body.$('iframe >> control=enter-frame >> canvas')).toBeFalsy(); + expect(await body.$('iframe >> internal:control=enter-frame >> canvas')).toBeFalsy(); }); it('$$ should not wait for frame', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); - expect(await page.$$('iframe >> control=enter-frame >> canvas')).toHaveLength(0); + expect(await page.$$('iframe >> internal:control=enter-frame >> canvas')).toHaveLength(0); const body = await page.$('body'); - expect(await body.$$('iframe >> control=enter-frame >> canvas')).toHaveLength(0); + expect(await body.$$('iframe >> internal:control=enter-frame >> canvas')).toHaveLength(0); }); it('$eval should throw for missing frame', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); { - const error = await page.$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e); + const error = await page.$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e); expect(error.message).toContain('Error: failed to find element matching selector'); } { const body = await page.$('body'); - const error = await body.$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e); + const error = await body.$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e); expect(error.message).toContain('Error: failed to find element matching selector'); } }); @@ -125,12 +125,12 @@ it('$eval should throw for missing frame', async ({ page, server }) => { it('$$eval should throw for missing frame', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); { - const error = await page.$$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e); + const error = await page.$$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e); expect(error.message).toContain('Error: failed to find frame for selector'); } { const body = await page.$('body'); - const error = await body.$$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e); + const error = await body.$$eval('iframe >> internal:control=enter-frame >> canvas', e => 1).catch(e => e); expect(error.message).toContain('Error: failed to find frame for selector'); } }); @@ -139,16 +139,16 @@ it('should work for $ and $$ (handle)', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); const body = await page.$('body'); - const element = await body.$('iframe >> control=enter-frame >> button'); + const element = await body.$('iframe >> internal:control=enter-frame >> button'); expect(await element.textContent()).toBe('Hello iframe'); - const elements = await body.$$('iframe >> control=enter-frame >> span'); + const elements = await body.$$('iframe >> internal:control=enter-frame >> span'); expect(elements).toHaveLength(2); }); it('should work for $eval', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const value = await page.$eval('iframe >> control=enter-frame >> button', b => b.nodeName); + const value = await page.$eval('iframe >> internal:control=enter-frame >> button', b => b.nodeName); expect(value).toBe('BUTTON'); }); @@ -156,14 +156,14 @@ it('should work for $eval (handle)', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); const body = await page.$('body'); - const value = await body.$eval('iframe >> control=enter-frame >> button', b => b.nodeName); + const value = await body.$eval('iframe >> internal:control=enter-frame >> button', b => b.nodeName); expect(value).toBe('BUTTON'); }); it('should work for $$eval', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const value = await page.$$eval('iframe >> control=enter-frame >> span', ss => ss.map(s => s.textContent)); + const value = await page.$$eval('iframe >> internal:control=enter-frame >> span', ss => ss.map(s => s.textContent)); expect(value).toEqual(['1', '2']); }); @@ -171,30 +171,30 @@ it('should work for $$eval (handle)', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); const body = await page.$('body'); - const value = await body.$$eval('iframe >> control=enter-frame >> span', ss => ss.map(s => s.textContent)); + const value = await body.$$eval('iframe >> internal:control=enter-frame >> span', ss => ss.map(s => s.textContent)); expect(value).toEqual(['1', '2']); }); it('should not allow dangling enter-frame', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const button = page.locator('iframe >> control=enter-frame'); + const button = page.locator('iframe >> internal:control=enter-frame'); const error = await button.click().catch(e => e); expect(error.message).toContain('Selector cannot end with'); - expect(error.message).toContain('iframe >> control=enter-frame'); + expect(error.message).toContain('iframe >> internal:control=enter-frame'); }); it('should not allow leading enter-frame', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const error = await page.waitForSelector('control=enter-frame >> button').catch(e => e); + const error = await page.waitForSelector('internal:control=enter-frame >> button').catch(e => e); expect(error.message).toContain('Selector cannot start with'); }); it('should not allow capturing before enter-frame', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const button = page.locator('*css=iframe >> control=enter-frame >> div'); + const button = page.locator('*css=iframe >> internal:control=enter-frame >> div'); const error = await await button.click().catch(e => e); expect(error.message).toContain('Can not capture the selector before diving into the frame'); }); @@ -202,7 +202,7 @@ it('should not allow capturing before enter-frame', async ({ page, server }) => it('should capture after the enter-frame', async ({ page, server }) => { await routeIframe(page); await page.goto(server.EMPTY_PAGE); - const div = page.locator('iframe >> control=enter-frame >> *css=div >> button'); + const div = page.locator('iframe >> internal:control=enter-frame >> *css=div >> button'); expect(await div.innerHTML()).toContain('