diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 45fe34a385065..62beb558aa534 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -275,6 +275,7 @@ export class InjectedScript { const result = this.querySelectorAll(selector, root); if (strict && result.length > 1) throw this.strictModeViolationError(selector, result); + this.checkDeprecatedSelectorUsage(selector, result); return result[0]; } @@ -1228,21 +1229,18 @@ export class InjectedScript { return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${trimStringWithEllipsis(text, 50)}`); } - strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error { + private _generateSelectors(elements: Element[]) { this._evaluator.begin(); beginAriaCaches(); beginDOMCaches(); try { // Firefox is slow to access DOM bindings in the utility world, making it very expensive to generate a lot of selectors. const maxElements = this._isUtilityWorld && this._browserName === 'firefox' ? 2 : 10; - const infos = matches.slice(0, maxElements).map(m => ({ + const infos = elements.slice(0, maxElements).map(m => ({ preview: this.previewNode(m), selector: this.generateSelectorSimple(m), })); - const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`); - if (infos.length < matches.length) - lines.push('\n ...'); - return this.createStacklessError(`strict mode violation: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} elements:${lines.join('')}\n`); + return infos.map((info, i) => `${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`); } finally { endDOMCaches(); endAriaCaches(); @@ -1250,6 +1248,25 @@ export class InjectedScript { } } + strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error { + const lines = this._generateSelectors(matches).map(line => `\n ` + line); + if (lines.length < matches.length) + lines.push('\n ...'); + return this.createStacklessError(`strict mode violation: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} elements:${lines.join('')}\n`); + } + + checkDeprecatedSelectorUsage(selector: ParsedSelector, matches: Element[]) { + if (!matches.length) + return; + const deperecated = selector.parts.find(part => part.name === '_react' || part.name === '_vue'); + if (!deperecated) + return; + const lines = this._generateSelectors(matches).map(line => `\n ` + line); + if (lines.length < matches.length) + lines.push('\n ...'); + throw this.createStacklessError(`"${deperecated.name}" selector is not supported: ${asLocator(this._sdkLanguage, stringifySelector(selector))} resolved to ${matches.length} element${matches.length === 1 ? '' : 's'}:${lines.join('')}\n`); + } + createStacklessError(message: string): Error { if (this._browserName === 'firefox') { const error = new Error('Error: ' + message); diff --git a/packages/playwright-core/src/server/frameSelectors.ts b/packages/playwright-core/src/server/frameSelectors.ts index 9118041532531..674ecfcb920fa 100644 --- a/packages/playwright-core/src/server/frameSelectors.ts +++ b/packages/playwright-core/src/server/frameSelectors.ts @@ -71,7 +71,9 @@ export class FrameSelectors { if (!resolved) throw new Error(`Failed to find frame for selector "${selector}"`); return await resolved.injected.evaluateHandle((injected, { info, scope }) => { - return injected.querySelectorAll(info.parsed, scope || document); + const elements = injected.querySelectorAll(info.parsed, scope || document); + injected.checkDeprecatedSelectorUsage(info.parsed, elements); + return elements; }, { info: resolved.info, scope: resolved.scope }); } @@ -82,7 +84,9 @@ export class FrameSelectors { throw new Error(`Failed to find frame for selector "${selector}"`); await options.__testHookBeforeQuery?.(); return await resolved.injected.evaluate((injected, { info }) => { - return injected.querySelectorAll(info.parsed, document).length; + const elements = injected.querySelectorAll(info.parsed, document); + injected.checkDeprecatedSelectorUsage(info.parsed, elements); + return elements.length; }, { info: resolved.info }); } @@ -92,7 +96,9 @@ export class FrameSelectors { if (!resolved) return []; const arrayHandle = await resolved.injected.evaluateHandle((injected, { info, scope }) => { - return injected.querySelectorAll(info.parsed, scope || document); + const elements = injected.querySelectorAll(info.parsed, scope || document); + injected.checkDeprecatedSelectorUsage(info.parsed, elements); + return elements; }, { info: resolved.info, scope: resolved.scope }); const properties = await arrayHandle.getProperties(); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index da1de2170d2e8..7fc9dd0527a6f 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -785,6 +785,7 @@ export class Frame extends SdkObject { } else if (element) { log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`; } + injected.checkDeprecatedSelectorUsage(info.parsed, elements); return { log, element, visible, attached: !!element }; }, { info: resolved.info, root: resolved.frame === this ? scope : undefined })); const { log, visible, attached } = await progress.race(result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached }))); @@ -1115,6 +1116,7 @@ export class Frame extends SdkObject { } else if (element) { log = ` locator resolved to ${injected.previewNode(element)}`; } + injected.checkDeprecatedSelectorUsage(info.parsed, elements); return { log, success: !!element, element }; }, { info: resolved.info, callId: progress.metadata.id })); const { log, success } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success }))); @@ -1444,6 +1446,8 @@ export class Frame extends SdkObject { throw injected.strictModeViolationError(info!.parsed, elements); else if (elements.length) log = ` locator resolved to ${injected.previewNode(elements[0])}`; + if (info) + injected.checkDeprecatedSelectorUsage(info.parsed, elements); return { log, ...await injected.expect(elements[0], options, elements) }; }, { info, options, callId: progress.metadata.id })); diff --git a/tests/page/selectors-react.spec.ts b/tests/page/selectors-react.spec.ts index c7bfc3f68ac11..e31e44fdca29c 100644 --- a/tests/page/selectors-react.spec.ts +++ b/tests/page/selectors-react.spec.ts @@ -15,7 +15,31 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect as baseExpect } from './pageTest'; +import type { Locator } from 'playwright-core'; + +const expect = baseExpect.extend({ + async toHaveCountError(locator: Locator, expected: number) { + try { + await expect(locator).toHaveCount(expected); + if (!expected) + return { pass: true, message: () => 'Locator has expected count of 0' }; + return { + pass: false, + message: () => `Querying locator ${locator.toString()} should throw, but it did not.`, + }; + } catch (e) { + const message = (e as Error).message; + try { + expect(message).toContain(`"_react" selector is not supported`); + expect(message).toContain(`resolved to ${expected} element`); + } catch (error) { + return { pass: false, message: () => (error as Error).message }; + } + return { pass: true, message: () => 'Error message is as expected' }; + } + } +}); const reacts = { 'react15': '/reading-list/react15.html', @@ -31,107 +55,106 @@ for (const [name, url] of Object.entries(reacts)) { }); it('should work with single-root elements @smoke', async ({ page }) => { - await expect(page.locator(`_react=BookList`)).toHaveCount(1); - await expect(page.locator(`_react=BookItem`)).toHaveCount(3); - await expect(page.locator(`_react=BookList >> _react=BookItem`)).toHaveCount(3); - await expect(page.locator(`_react=BookItem >> _react=BookList`)).toHaveCount(0); + await expect(page.locator(`_react=BookList`)).toHaveCountError(1); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(3); + await expect(page.locator(`_react=BookList >> _react=BookItem`)).toHaveCountError(3); + await expect(page.locator(`_react=BookItem >> _react=BookList`)).toHaveCountError(0); }); it('should work with multi-root elements (fragments)', async ({ page }) => { it.skip(name === 'react15', 'React 15 does not support fragments'); - await expect(page.locator(`_react=App`)).toHaveCount(15); - await expect(page.locator(`_react=AppHeader`)).toHaveCount(2); - await expect(page.locator(`_react=NewBook`)).toHaveCount(2); + await expect(page.locator(`_react=App`)).toHaveCountError(15); + await expect(page.locator(`_react=AppHeader`)).toHaveCountError(2); + await expect(page.locator(`_react=NewBook`)).toHaveCountError(2); }); it('should not crash when there is no match', async ({ page }) => { - await expect(page.locator(`_react=Apps`)).toHaveCount(0); - await expect(page.locator(`_react=BookLi`)).toHaveCount(0); + await expect(page.locator(`_react=Apps`)).toHaveCountError(0); + await expect(page.locator(`_react=BookLi`)).toHaveCountError(0); }); it('should compose', async ({ page }) => { - await expect(page.locator(`_react=NewBook >> _react=button`)).toHaveText('new book'); - expect(await page.$eval(`_react=NewBook >> _react=input`, el => el.tagName)).toBe('INPUT'); - await expect(page.locator(`_react=BookItem >> text=Gatsby`)).toHaveText('The Great Gatsby'); + await expect(page.locator(`_react=NewBook >> _react=button`).locator(':scope:has-text("new book")')).toHaveCountError(1); + await expect(page.locator(`_react=BookItem >> text=Gatsby`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1); }); it('should query by props combinations', async ({ page }) => { - await expect(page.locator(`_react=BookItem[name="The Great Gatsby"]`)).toHaveCount(1); - await expect(page.locator(`_react=BookItem[name="the great gatsby" i]`)).toHaveCount(1); - await expect(page.locator(`_react=li[key="The Great Gatsby"]`)).toHaveCount(1); - await expect(page.locator(`_react=ColorButton[nested.index = 0]`)).toHaveCount(1); - await expect(page.locator(`_react=ColorButton[nested.nonexisting.index = 0]`)).toHaveCount(0); - await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 0]`)).toHaveCount(0); - await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 1]`)).toHaveCount(0); - await expect(page.locator(`_react=ColorButton[nested.value = 4.1]`)).toHaveCount(1); - await expect(page.locator(`_react=ColorButton[enabled = false]`)).toHaveCount(4); - await expect(page.locator(`_react=ColorButton[enabled = true] `)).toHaveCount(5); - await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"]`)).toHaveCount(2); - await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"i][nested.index = 6]`)).toHaveCount(1); + await expect(page.locator(`_react=BookItem[name="The Great Gatsby"]`)).toHaveCountError(1); + await expect(page.locator(`_react=BookItem[name="the great gatsby" i]`)).toHaveCountError(1); + await expect(page.locator(`_react=li[key="The Great Gatsby"]`)).toHaveCountError(1); + await expect(page.locator(`_react=ColorButton[nested.index = 0]`)).toHaveCountError(1); + await expect(page.locator(`_react=ColorButton[nested.nonexisting.index = 0]`)).toHaveCountError(0); + await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 0]`)).toHaveCountError(0); + await expect(page.locator(`_react=ColorButton[nested.index.nonexisting = 1]`)).toHaveCountError(0); + await expect(page.locator(`_react=ColorButton[nested.value = 4.1]`)).toHaveCountError(1); + await expect(page.locator(`_react=ColorButton[enabled = false]`)).toHaveCountError(4); + await expect(page.locator(`_react=ColorButton[enabled = true] `)).toHaveCountError(5); + await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"]`)).toHaveCountError(2); + await expect(page.locator(`_react=ColorButton[enabled = true][color = "red"i][nested.index = 6]`)).toHaveCountError(1); }); it('should exact match by props', async ({ page }) => { - await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`)).toHaveText('The Great Gatsby'); - await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`)).toHaveCount(1); + await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1); + await expect(page.locator(`_react=BookItem[name = "The Great Gatsby"]`)).toHaveCountError(1); // case sensitive by default - await expect(page.locator(`_react=BookItem[name = "the great gatsby"]`)).toHaveCount(0); - await expect(page.locator(`_react=BookItem[name = "the great gatsby" s]`)).toHaveCount(0); - await expect(page.locator(`_react=BookItem[name = "the great gatsby" S]`)).toHaveCount(0); + await expect(page.locator(`_react=BookItem[name = "the great gatsby"]`)).toHaveCountError(0); + await expect(page.locator(`_react=BookItem[name = "the great gatsby" s]`)).toHaveCountError(0); + await expect(page.locator(`_react=BookItem[name = "the great gatsby" S]`)).toHaveCountError(0); // case insensitive with flag - await expect(page.locator(`_react=BookItem[name = "the great gatsby" i]`)).toHaveCount(1); - await expect(page.locator(`_react=BookItem[name = "the great gatsby" I]`)).toHaveCount(1); - await expect(page.locator(`_react=BookItem[name = " The Great Gatsby "]`)).toHaveCount(0); + await expect(page.locator(`_react=BookItem[name = "the great gatsby" i]`)).toHaveCountError(1); + await expect(page.locator(`_react=BookItem[name = "the great gatsby" I]`)).toHaveCountError(1); + await expect(page.locator(`_react=BookItem[name = " The Great Gatsby "]`)).toHaveCountError(0); }); it('should partially match by props', async ({ page }) => { // Check partial matching - await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`)).toHaveText('The Great Gatsby'); - await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`)).toHaveCount(1); - await expect(page.locator(`_react=[name *= "Gatsby"]`)).toHaveCount(1); + await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1); + await expect(page.locator(`_react=BookItem[name *= "Gatsby"]`)).toHaveCountError(1); + await expect(page.locator(`_react=[name *= "Gatsby"]`)).toHaveCountError(1); - await expect(page.locator(`_react=BookItem[name = "Gatsby"]`)).toHaveCount(0); + await expect(page.locator(`_react=BookItem[name = "Gatsby"]`)).toHaveCountError(0); }); it('should support all string operators', async ({ page }) => { - await expect(page.locator(`_react=ColorButton[color = "red"]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color |= "red"]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color $= "ed"]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color ^= "gr"]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color ~= "e"]`)).toHaveCount(0); - await expect(page.locator(`_react=BookItem[name ~= "gatsby" i]`)).toHaveCount(1); - await expect(page.locator(`_react=BookItem[name *= " gatsby" i]`)).toHaveCount(1); + await expect(page.locator(`_react=ColorButton[color = "red"]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color |= "red"]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color $= "ed"]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color ^= "gr"]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color ~= "e"]`)).toHaveCountError(0); + await expect(page.locator(`_react=BookItem[name ~= "gatsby" i]`)).toHaveCountError(1); + await expect(page.locator(`_react=BookItem[name *= " gatsby" i]`)).toHaveCountError(1); }); it('should support regex', async ({ page }) => { - await expect(page.locator(`_react=ColorButton[color = /red/]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color = /^red$/]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color = /RED/i]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color = /[pqr]ed/]`)).toHaveCount(3); - await expect(page.locator(`_react=ColorButton[color = /[pq]ed/]`)).toHaveCount(0); - await expect(page.locator(`_react=BookItem[name = /gat.by/i]`)).toHaveCount(1); + await expect(page.locator(`_react=ColorButton[color = /red/]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color = /^red$/]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color = /RED/i]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color = /[pqr]ed/]`)).toHaveCountError(3); + await expect(page.locator(`_react=ColorButton[color = /[pq]ed/]`)).toHaveCountError(0); + await expect(page.locator(`_react=BookItem[name = /gat.by/i]`)).toHaveCountError(1); }); it('should support truthy querying', async ({ page }) => { - await expect(page.locator(`_react=ColorButton[enabled]`)).toHaveCount(5); + await expect(page.locator(`_react=ColorButton[enabled]`)).toHaveCountError(5); }); it('should support nested react trees', async ({ page }) => { - await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(3); await page.evaluate(() => { // @ts-ignore mountNestedApp(); }); - await expect(page.locator(`_react=BookItem`)).toHaveCount(6); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(6); }); it('should work with react memo', async ({ page }) => { it.skip(name === 'react15' || name === 'react16', 'Class components dont support memo'); - await expect(page.locator(`_react=ButtonGrid`)).toHaveCount(9); + await expect(page.locator(`_react=ButtonGrid`)).toHaveCountError(9); }); it('should work with multiroot react', async ({ page }) => { await it.step('mount second root', async () => { - await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(3); await page.evaluate(() => { const anotherRoot = document.createElement('div'); anotherRoot.id = 'root2'; @@ -139,19 +162,19 @@ for (const [name, url] of Object.entries(reacts)) { // @ts-ignore window.mountApp(anotherRoot); }); - await expect(page.locator(`_react=BookItem`)).toHaveCount(6); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(6); }); await it.step('add a new book to second root', async () => { await page.locator('#root2 input').fill('newbook'); await page.locator('#root2 >> text=new book').click(); - await expect(page.locator('css=#root >> _react=BookItem')).toHaveCount(3); - await expect(page.locator('css=#root2 >> _react=BookItem')).toHaveCount(4); + await expect(page.locator('css=#root >> _react=BookItem')).toHaveCountError(3); + await expect(page.locator('css=#root2 >> _react=BookItem')).toHaveCountError(4); }); }); it('should work with multiroot react inside shadow DOM', async ({ page }) => { - await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(3); await page.evaluate(() => { const anotherRoot = document.createElement('div'); document.body.append(anotherRoot); @@ -159,11 +182,11 @@ for (const [name, url] of Object.entries(reacts)) { // @ts-ignore window.mountApp(shadowRoot); }); - await expect(page.locator(`_react=BookItem`)).toHaveCount(6); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(6); }); it('should work with multiroot react after unmount', async ({ page }) => { - await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(3); await page.evaluate(() => { const anotherRoot = document.createElement('div'); @@ -172,7 +195,7 @@ for (const [name, url] of Object.entries(reacts)) { const newRoot = window.mountApp(anotherRoot); newRoot.unmount(); }); - await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + await expect(page.locator(`_react=BookItem`)).toHaveCountError(3); }); }); } diff --git a/tests/page/selectors-vue.spec.ts b/tests/page/selectors-vue.spec.ts index 71b60d1cb3fea..feee266a68b51 100644 --- a/tests/page/selectors-vue.spec.ts +++ b/tests/page/selectors-vue.spec.ts @@ -15,7 +15,32 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; + +import { test as it, expect as baseExpect } from './pageTest'; +import type { Locator } from 'playwright-core'; + +const expect = baseExpect.extend({ + async toHaveCountError(locator: Locator, expected: number) { + try { + await expect(locator).toHaveCount(expected); + if (!expected) + return { pass: true, message: () => 'Locator has expected count of 0' }; + return { + pass: false, + message: () => `Querying locator ${locator.toString()} should throw, but it did not.`, + }; + } catch (e) { + const message = (e as Error).message; + try { + expect(message).toContain(`"_vue" selector is not supported`); + expect(message).toContain(`resolved to ${expected} element`); + } catch (error) { + return { pass: false, message: () => (error as Error).message }; + } + return { pass: true, message: () => 'Error message is as expected' }; + } + } +}); const vues = { 'vue2': '/reading-list/vue2.html', @@ -29,105 +54,102 @@ for (const [name, url] of Object.entries(vues)) { }); it('should work with single-root elements @smoke', async ({ page }) => { - await expect(page.locator(`_vue=book-list`)).toHaveCount(1); + await expect(page.locator(`_vue=book-list`)).toHaveCountError(1); // count() was not working, see: https://github.com/microsoft/playwright/issues/12887 - expect(await page.locator(`_vue=book-list`).count()).toBe(1); - await expect(page.locator(`_vue=book-list`)).toHaveCount(1); - await expect(page.locator(`_vue=book-item`)).toHaveCount(3); - expect(await page.locator(`_vue=book-item`).count()).toBe(3); - await expect(page.locator(`_vue=book-item`)).toHaveCount(3); - await expect(page.locator(`_vue=book-list >> _vue=book-item`)).toHaveCount(3); - expect(await page.locator(`_vue=book-list >> _vue=book-item`).count()).toBe(3); - await expect(page.locator(`_vue=book-item >> _vue=book-list`)).toHaveCount(0); + await expect(page.locator(`_vue=book-list`)).toHaveCountError(1); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(3); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(3); + await expect(page.locator(`_vue=book-list >> _vue=book-item`)).toHaveCountError(3); + await expect(page.locator(`_vue=book-item >> _vue=book-list`)).toHaveCountError(0); }); it('should work with multi-root elements (fragments)', async ({ page }) => { it.skip(name === 'vue2', 'vue2 does not support fragments'); - await expect(page.locator(`_vue=Root`)).toHaveCount(15); - await expect(page.locator(`_vue=app-header`)).toHaveCount(2); - await expect(page.locator(`_vue=new-book`)).toHaveCount(2); + await expect(page.locator(`_vue=Root`)).toHaveCountError(15); + await expect(page.locator(`_vue=app-header`)).toHaveCountError(2); + await expect(page.locator(`_vue=new-book`)).toHaveCountError(2); }); it('should not crash when there is no match', async ({ page }) => { - await expect(page.locator(`_vue=apps`)).toHaveCount(0); - await expect(page.locator(`_vue=book-li`)).toHaveCount(0); + await expect(page.locator(`_vue=apps`)).toHaveCountError(0); + await expect(page.locator(`_vue=book-li`)).toHaveCountError(0); }); it('should compose', async ({ page }) => { - await expect(page.locator(`_vue=book-item >> text=Gatsby`)).toHaveText('The Great Gatsby'); + await expect(page.locator(`_vue=book-item >> text=Gatsby`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1); }); it('should query by props combinations', async ({ page }) => { - await expect(page.locator(`_vue=book-item[name="The Great Gatsby"]`)).toHaveCount(1); - await expect(page.locator(`_vue=book-item[name="the great gatsby" i]`)).toHaveCount(1); - await expect(page.locator(`_vue=color-button[nested.index = 0]`)).toHaveCount(1); - await expect(page.locator(`_vue=color-button[nested.nonexisting.index = 0]`)).toHaveCount(0); - await expect(page.locator(`_vue=color-button[nested.index.nonexisting = 0]`)).toHaveCount(0); - await expect(page.locator(`_vue=color-button[nested.index.nonexisting = 1]`)).toHaveCount(0); - await expect(page.locator(`_vue=color-button[nested.value = 4.1]`)).toHaveCount(1); - await expect(page.locator(`_vue=color-button[enabled = false]`)).toHaveCount(4); - await expect(page.locator(`_vue=color-button[enabled = true] `)).toHaveCount(5); - await expect(page.locator(`_vue=color-button[enabled = true][color = "red"]`)).toHaveCount(2); - await expect(page.locator(`_vue=color-button[enabled = true][color = "red"i][nested.index = 6]`)).toHaveCount(1); + await expect(page.locator(`_vue=book-item[name="The Great Gatsby"]`)).toHaveCountError(1); + await expect(page.locator(`_vue=book-item[name="the great gatsby" i]`)).toHaveCountError(1); + await expect(page.locator(`_vue=color-button[nested.index = 0]`)).toHaveCountError(1); + await expect(page.locator(`_vue=color-button[nested.nonexisting.index = 0]`)).toHaveCountError(0); + await expect(page.locator(`_vue=color-button[nested.index.nonexisting = 0]`)).toHaveCountError(0); + await expect(page.locator(`_vue=color-button[nested.index.nonexisting = 1]`)).toHaveCountError(0); + await expect(page.locator(`_vue=color-button[nested.value = 4.1]`)).toHaveCountError(1); + await expect(page.locator(`_vue=color-button[enabled = false]`)).toHaveCountError(4); + await expect(page.locator(`_vue=color-button[enabled = true] `)).toHaveCountError(5); + await expect(page.locator(`_vue=color-button[enabled = true][color = "red"]`)).toHaveCountError(2); + await expect(page.locator(`_vue=color-button[enabled = true][color = "red"i][nested.index = 6]`)).toHaveCountError(1); }); it('should exact match by props', async ({ page }) => { - await expect(page.locator(`_vue=book-item[name = "The Great Gatsby"]`)).toHaveText('The Great Gatsby'); - await expect(page.locator(`_vue=book-item[name = "The Great Gatsby"]`)).toHaveCount(1); + await expect(page.locator(`_vue=book-item[name = "The Great Gatsby"]`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1); + await expect(page.locator(`_vue=book-item[name = "The Great Gatsby"]`)).toHaveCountError(1); // case sensitive by default - await expect(page.locator(`_vue=book-item[name = "the great gatsby"]`)).toHaveCount(0); - await expect(page.locator(`_vue=book-item[name = "the great gatsby" s]`)).toHaveCount(0); - await expect(page.locator(`_vue=book-item[name = "the great gatsby" S]`)).toHaveCount(0); + await expect(page.locator(`_vue=book-item[name = "the great gatsby"]`)).toHaveCountError(0); + await expect(page.locator(`_vue=book-item[name = "the great gatsby" s]`)).toHaveCountError(0); + await expect(page.locator(`_vue=book-item[name = "the great gatsby" S]`)).toHaveCountError(0); // case insensitive with flag - await expect(page.locator(`_vue=book-item[name = "the great gatsby" i]`)).toHaveCount(1); - await expect(page.locator(`_vue=book-item[name = "the great gatsby" I]`)).toHaveCount(1); - await expect(page.locator(`_vue=book-item[name = " The Great Gatsby "]`)).toHaveCount(0); + await expect(page.locator(`_vue=book-item[name = "the great gatsby" i]`)).toHaveCountError(1); + await expect(page.locator(`_vue=book-item[name = "the great gatsby" I]`)).toHaveCountError(1); + await expect(page.locator(`_vue=book-item[name = " The Great Gatsby "]`)).toHaveCountError(0); }); it('should partially match by props', async ({ page }) => { // Check partial matching - await expect(page.locator(`_vue=book-item[name *= "Gatsby"]`)).toHaveText('The Great Gatsby'); - await expect(page.locator(`_vue=book-item[name *= "Gatsby"]`)).toHaveCount(1); - await expect(page.locator(`_vue=[name *= "Gatsby"]`)).toHaveCount(1); + await expect(page.locator(`_vue=book-item[name *= "Gatsby"]`).locator(':scope:has-text("The Great Gatsby")')).toHaveCountError(1); + await expect(page.locator(`_vue=book-item[name *= "Gatsby"]`)).toHaveCountError(1); + await expect(page.locator(`_vue=[name *= "Gatsby"]`)).toHaveCountError(1); - await expect(page.locator(`_vue=book-item[name = "Gatsby"]`)).toHaveCount(0); + await expect(page.locator(`_vue=book-item[name = "Gatsby"]`)).toHaveCountError(0); }); it('should support all string operators', async ({ page }) => { - await expect(page.locator(`_vue=color-button[color = "red"]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color |= "red"]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color $= "ed"]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color ^= "gr"]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color ~= "e"]`)).toHaveCount(0); - await expect(page.locator(`_vue=book-item[name ~= "gatsby" i]`)).toHaveCount(1); - await expect(page.locator(`_vue=book-item[name *= " gatsby" i]`)).toHaveCount(1); + await expect(page.locator(`_vue=color-button[color = "red"]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color |= "red"]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color $= "ed"]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color ^= "gr"]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color ~= "e"]`)).toHaveCountError(0); + await expect(page.locator(`_vue=book-item[name ~= "gatsby" i]`)).toHaveCountError(1); + await expect(page.locator(`_vue=book-item[name *= " gatsby" i]`)).toHaveCountError(1); }); it('should support regex', async ({ page }) => { - await expect(page.locator(`_vue=color-button[color = /red/]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color = /^red$/]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color = /RED/i]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color = /[pqr]ed/]`)).toHaveCount(3); - await expect(page.locator(`_vue=color-button[color = /[pq]ed/]`)).toHaveCount(0); - await expect(page.locator(`_vue=book-item[name = /gat.by/i]`)).toHaveCount(1); + await expect(page.locator(`_vue=color-button[color = /red/]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color = /^red$/]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color = /RED/i]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color = /[pqr]ed/]`)).toHaveCountError(3); + await expect(page.locator(`_vue=color-button[color = /[pq]ed/]`)).toHaveCountError(0); + await expect(page.locator(`_vue=book-item[name = /gat.by/i]`)).toHaveCountError(1); }); it('should support truthy querying', async ({ page }) => { - await expect(page.locator(`_vue=color-button[enabled]`)).toHaveCount(5); + await expect(page.locator(`_vue=color-button[enabled]`)).toHaveCountError(5); }); it('should support nested vue trees', async ({ page }) => { - await expect(page.locator(`_vue=book-item`)).toHaveCount(3); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(3); await page.evaluate(() => { // @ts-ignore mountNestedApp(); }); - await expect(page.locator(`_vue=book-item`)).toHaveCount(6); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(6); }); it('should work with multiroot react', async ({ page }) => { await it.step('mount second root', async () => { - await expect(page.locator(`_vue=book-item`)).toHaveCount(3); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(3); await page.evaluate(() => { const anotherRoot = document.createElement('div'); anotherRoot.id = 'root2'; @@ -136,19 +158,19 @@ for (const [name, url] of Object.entries(vues)) { // @ts-ignore window.mountApp(anotherRoot.querySelector('div')); }); - await expect(page.locator(`_vue=book-item`)).toHaveCount(6); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(6); }); await it.step('add a new book to second root', async () => { await page.locator('#root2 input').fill('newbook'); await page.locator('#root2 >> text=new book').click(); - await expect(page.locator('css=#root >> _vue=book-item')).toHaveCount(3); - await expect(page.locator('css=#root2 >> _vue=book-item')).toHaveCount(4); + await expect(page.locator('css=#root >> _vue=book-item')).toHaveCountError(3); + await expect(page.locator('css=#root2 >> _vue=book-item')).toHaveCountError(4); }); }); it('should work with multiroot vue inside shadow DOM', async ({ page }) => { - await expect(page.locator(`_vue=book-item`)).toHaveCount(3); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(3); await page.evaluate(vueName => { const anotherRoot = document.createElement('div'); document.body.append(anotherRoot); @@ -164,7 +186,7 @@ for (const [name, url] of Object.entries(vues)) { window.mountApp(shadowRoot); } }, name); - await expect(page.locator(`_vue=book-item`)).toHaveCount(6); + await expect(page.locator(`_vue=book-item`)).toHaveCountError(6); }); }); }