From 3dc1920ce83b56a408d84e2d43e07ec8976d924d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 11 Aug 2022 14:10:12 -0700 Subject: [PATCH] feat(expect): toHaveText/toContainText work with text in shadow dom (#16433) --- .../src/server/injected/injectedScript.ts | 6 +++--- tests/page/expect-to-have-text.spec.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 323f875034..312be0b8d2 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -21,7 +21,7 @@ import { VueEngine } from './vueSelectorEngine'; import { RoleEngine } from './roleSelectorEngine'; import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser'; import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser'; -import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorUtils'; +import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText } from './selectorUtils'; import { SelectorEvaluatorImpl } from './selectorEvaluator'; import { isElementVisible, parentElementOrShadowHost } from './domUtils'; import type { CSSComplexSelectorList } from '../isomorphic/cssParser'; @@ -1091,7 +1091,7 @@ export class InjectedScript { } else if (expression === 'to.have.id') { received = element.id; } else if (expression === 'to.have.text') { - received = options.useInnerText ? (element as HTMLElement).innerText : element.textContent || ''; + received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new Map(), element).full; } else if (expression === 'to.have.title') { received = document.title; } else if (expression === 'to.have.url') { @@ -1124,7 +1124,7 @@ export class InjectedScript { // List of values. let received: string[] | undefined; if (expression === 'to.have.text.array' || expression === 'to.contain.text.array') - received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : e.textContent || ''); + received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full); else if (expression === 'to.have.class.array') received = elements.map(e => e.classList.toString()); diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 8e25ba91e2..c55e4150bc 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -107,6 +107,27 @@ test.describe('toHaveText with text', () => { const locator = page.locator('#node'); await expect(locator).toHaveText('Text content', { useInnerText: true }); }); + + test('in shadow dom', async ({ page }) => { + await page.setContent(` +
+ + `); + await expect(page.locator('span')).toHaveText('some text'); + await expect(page.locator('span')).toContainText('text'); + await expect(page.locator('div')).toHaveText('some text'); + await expect(page.locator('div')).toContainText('text'); + await expect(page.locator('span')).toHaveText('some text', { useInnerText: true }); + await expect(page.locator('span')).toContainText('text', { useInnerText: true }); + // Playwright intentionally does not perform innerText piercing on shadow dom. + await expect(page.locator('div')).not.toHaveText('some text', { useInnerText: true }); + await expect(page.locator('div')).not.toContainText('text', { useInnerText: true }); + }); }); test.describe('not.toHaveText', () => {