From b25ece6b00165efcc0d3867dbb2463a951cb18ff Mon Sep 17 00:00:00 2001 From: KJ Kim Date: Wed, 12 Jan 2022 14:10:31 -0800 Subject: [PATCH 1/4] feat: adding a new matcher for shadow DOM toMatch --- packages/expect-puppeteer/src/index.js | 3 + .../src/matchers/setupPage.js | 28 ++++--- .../src/matchers/toMatchInShadow.js | 78 +++++++++++++++++++ .../src/matchers/toMatchInShadow.test.js | 60 ++++++++++++++ server/public/shadow.html | 21 +++++ server/public/shadowFrame.html | 1 + 6 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 packages/expect-puppeteer/src/matchers/toMatchInShadow.js create mode 100644 packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js create mode 100644 server/public/shadow.html create mode 100644 server/public/shadowFrame.html diff --git a/packages/expect-puppeteer/src/index.js b/packages/expect-puppeteer/src/index.js index 56be8ac9..883390cf 100644 --- a/packages/expect-puppeteer/src/index.js +++ b/packages/expect-puppeteer/src/index.js @@ -8,6 +8,7 @@ import toFill from './matchers/toFill' import toFillForm from './matchers/toFillForm' import toMatch from './matchers/toMatch' import toMatchElement from './matchers/toMatchElement' +import toMatchInShadow from './matchers/toMatchInShadow' import toSelect from './matchers/toSelect' import toUploadFile from './matchers/toUploadFile' @@ -20,6 +21,7 @@ const pageMatchers = { toFillForm, toMatch, toMatchElement, + toMatchInShadow, toSelect, toUploadFile, not: { @@ -34,6 +36,7 @@ const elementHandleMatchers = { toFillForm, toMatch, toMatchElement, + toMatchInShadow, toSelect, toUploadFile, not: { diff --git a/packages/expect-puppeteer/src/matchers/setupPage.js b/packages/expect-puppeteer/src/matchers/setupPage.js index 9ea6ffb7..2953ca0a 100644 --- a/packages/expect-puppeteer/src/matchers/setupPage.js +++ b/packages/expect-puppeteer/src/matchers/setupPage.js @@ -12,21 +12,29 @@ function waitForFrame(page) { return promise } -export const setupPage = (pageType, cb) => { +async function goToPage(page, route, isFrame, cb) { let currentPage = page + await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}/${route}`) + if (isFrame) { + currentPage = await waitForFrame(page) + } + cb({ + currentPage, + }) +} + +export const setupPage = (pageType, cb) => { beforeEach(async () => { if (pageType === `Page`) { cb({ - currentPage, + page, }) - return + } else if (pageType === 'ShadowPage') { + await goToPage(page, 'shadow.html', false, cb) + } else if (pageType === 'ShadowFrame') { + await goToPage(page, 'shadowFrame.html', true, cb) + } else { + await goToPage(page, 'frame.html', true, cb) } - await page.goto( - `http://localhost:${process.env.TEST_SERVER_PORT}/frame.html`, - ) - currentPage = await waitForFrame(page) - cb({ - currentPage, - }) }) } diff --git a/packages/expect-puppeteer/src/matchers/toMatchInShadow.js b/packages/expect-puppeteer/src/matchers/toMatchInShadow.js new file mode 100644 index 00000000..d2f53e55 --- /dev/null +++ b/packages/expect-puppeteer/src/matchers/toMatchInShadow.js @@ -0,0 +1,78 @@ +import { getContext, enhanceError, expandSearchExpr } from '../utils' +import { defaultOptions } from '../options' + +async function toMatch(instance, matcher, options) { + options = defaultOptions(options) + + const { page, handle } = await getContext(instance, () => document.body) + + const { text, regexp } = expandSearchExpr(matcher) + + try { + await page.waitForFunction( + (handle, text, regexp) => { + function getShadowTextContent(node) { + const walker = document.createTreeWalker( + node, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, + null, + false, + ) + let result = '' + let currentNode = walker.nextNode() + while (currentNode) { + if (currentNode.assignedSlot) { + // Skip everything within this subtree, since it's assigned to a slot in the shadow DOM. + const nodeWithAssignedSlot = currentNode + while ( + currentNode === nodeWithAssignedSlot || + nodeWithAssignedSlot.contains(currentNode) + ) { + currentNode = walker.nextNode() + } + // eslint-disable-next-line no-continue + continue + } else if (currentNode.nodeType === Node.TEXT_NODE) { + result += currentNode.textContent + } else if (currentNode.shadowRoot) { + result += getShadowTextContent(currentNode.shadowRoot) + } else if (typeof currentNode.assignedNodes === 'function') { + const assignedNodes = currentNode.assignedNodes() + // eslint-disable-next-line no-loop-func + assignedNodes.forEach((node) => { + result += getShadowTextContent(node) + }) + } + currentNode = walker.nextNode() + } + return result + } + + if (!handle) return false + const textContent = getShadowTextContent(handle) + if (regexp !== null) { + const [, pattern, flags] = regexp.match(/\/(.*)\/(.*)?/) + return ( + textContent + .replace(/\s+/g, ' ') + .trim() + .match(new RegExp(pattern, flags)) !== null + ) + } + if (text !== null) { + return textContent.replace(/\s+/g, ' ').trim().includes(text) + } + return false + }, + options, + handle, + text, + regexp, + ) + } catch (error) { + throw enhanceError(error, `Text not found "${matcher}"`) + } +} + +export default toMatch diff --git a/packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js b/packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js new file mode 100644 index 00000000..6c620ade --- /dev/null +++ b/packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js @@ -0,0 +1,60 @@ +import { setupPage } from './setupPage' + +describe('toMatchInShadow', () => { + beforeEach(async () => { + await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}`) + }) + + describe.each(['ShadowPage', 'ShadowFrame'])('%s', (pageType) => { + let page + setupPage(pageType, ({ currentPage }) => { + page = currentPage + }) + it('should be ok if text is in the page in shadow DOM', async () => { + await expect(page).toMatchInShadow('This is home!') + }) + + it('should be ok if text is in the page in a slot', async () => { + await expect(page).toMatchInShadow('Light DOM content (slotted)') + }) + + it('should support RegExp', async () => { + await expect(page).toMatchInShadow(/THIS.is.home/i) + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(3) + + try { + await expect(page).toMatchInShadow('Nop') + } catch (error) { + expect(error.message).toMatch('Text not found "Nop"') + expect(error.message).toMatch('waiting for function failed') + } + }) + }) + + describe('ElementHandle', () => { + it('should be ok if text is in the page', async () => { + const dialogBtn = await page.$('#dialog-btn') + await expect(dialogBtn).toMatchInShadow('Open dialog') + }) + + it('should support RegExp', async () => { + const dialogBtn = await page.$('#dialog-btn') + await expect(dialogBtn).toMatchInShadow(/OPEN/i) + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(3) + const dialogBtn = await page.$('#dialog-btn') + + try { + await expect(dialogBtn).toMatchInShadow('This is home!') + } catch (error) { + expect(error.message).toMatch('Text not found "This is home!"') + expect(error.message).toMatch('waiting for function failed') + } + }) + }) +}) diff --git a/server/public/shadow.html b/server/public/shadow.html new file mode 100644 index 00000000..d6f546c9 --- /dev/null +++ b/server/public/shadow.html @@ -0,0 +1,21 @@ + + + + + Test App + + + + +

Light DOM content (slotted)

+
+ + + diff --git a/server/public/shadowFrame.html b/server/public/shadowFrame.html new file mode 100644 index 00000000..dea0f840 --- /dev/null +++ b/server/public/shadowFrame.html @@ -0,0 +1 @@ + From 938f4d67898154628d3e8eac37debcff5d58d333 Mon Sep 17 00:00:00 2001 From: KJ Kim Date: Wed, 12 Jan 2022 17:31:49 -0800 Subject: [PATCH 2/4] fix: fixing incorrect page setup --- packages/expect-puppeteer/src/matchers/setupPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/expect-puppeteer/src/matchers/setupPage.js b/packages/expect-puppeteer/src/matchers/setupPage.js index 2953ca0a..c0e52b1a 100644 --- a/packages/expect-puppeteer/src/matchers/setupPage.js +++ b/packages/expect-puppeteer/src/matchers/setupPage.js @@ -27,7 +27,7 @@ export const setupPage = (pageType, cb) => { beforeEach(async () => { if (pageType === `Page`) { cb({ - page, + currentPage: page, }) } else if (pageType === 'ShadowPage') { await goToPage(page, 'shadow.html', false, cb) From ed363a781c9cd55cdbc3917eddc24162afa56e27 Mon Sep 17 00:00:00 2001 From: KJ Kim Date: Wed, 12 Jan 2022 21:09:24 -0800 Subject: [PATCH 3/4] fix: rewording export in toMatchInShadow --- packages/expect-puppeteer/src/matchers/toMatchInShadow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/expect-puppeteer/src/matchers/toMatchInShadow.js b/packages/expect-puppeteer/src/matchers/toMatchInShadow.js index d2f53e55..a1b47c91 100644 --- a/packages/expect-puppeteer/src/matchers/toMatchInShadow.js +++ b/packages/expect-puppeteer/src/matchers/toMatchInShadow.js @@ -1,7 +1,7 @@ import { getContext, enhanceError, expandSearchExpr } from '../utils' import { defaultOptions } from '../options' -async function toMatch(instance, matcher, options) { +async function toMatchInShadow(instance, matcher, options) { options = defaultOptions(options) const { page, handle } = await getContext(instance, () => document.body) @@ -75,4 +75,4 @@ async function toMatch(instance, matcher, options) { } } -export default toMatch +export default toMatchInShadow From 396a7bca2359b4d96c1a7aa00326911d3f6b91d7 Mon Sep 17 00:00:00 2001 From: KJ Kim Date: Wed, 12 Jan 2022 23:41:28 -0800 Subject: [PATCH 4/4] fix: moving toMatchInShadow into toMatch --- packages/expect-puppeteer/README.md | 1 + packages/expect-puppeteer/src/index.js | 3 - .../src/matchers/notToMatch.js | 49 ++++++++- .../src/matchers/notToMatch.test.js | 84 ++++++++------- .../expect-puppeteer/src/matchers/toMatch.js | 51 ++++++++- .../src/matchers/toMatch.test.js | 102 ++++++++++-------- .../src/matchers/toMatchInShadow.js | 78 -------------- .../src/matchers/toMatchInShadow.test.js | 60 ----------- 8 files changed, 197 insertions(+), 231 deletions(-) delete mode 100644 packages/expect-puppeteer/src/matchers/toMatchInShadow.js delete mode 100644 packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js diff --git a/packages/expect-puppeteer/README.md b/packages/expect-puppeteer/README.md index 8a7a073c..0c133bf0 100644 --- a/packages/expect-puppeteer/README.md +++ b/packages/expect-puppeteer/README.md @@ -164,6 +164,7 @@ Expect a text or a string RegExp to be present in the page or element. - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes. - `mutation` - to execute `pageFunction` on every DOM mutation. - `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. + - `traverseShadowRoots`<[boolean]> Whether shadow roots should be traversed to find a match. ```js // Matching using text diff --git a/packages/expect-puppeteer/src/index.js b/packages/expect-puppeteer/src/index.js index 883390cf..56be8ac9 100644 --- a/packages/expect-puppeteer/src/index.js +++ b/packages/expect-puppeteer/src/index.js @@ -8,7 +8,6 @@ import toFill from './matchers/toFill' import toFillForm from './matchers/toFillForm' import toMatch from './matchers/toMatch' import toMatchElement from './matchers/toMatchElement' -import toMatchInShadow from './matchers/toMatchInShadow' import toSelect from './matchers/toSelect' import toUploadFile from './matchers/toUploadFile' @@ -21,7 +20,6 @@ const pageMatchers = { toFillForm, toMatch, toMatchElement, - toMatchInShadow, toSelect, toUploadFile, not: { @@ -36,7 +34,6 @@ const elementHandleMatchers = { toFillForm, toMatch, toMatchElement, - toMatchInShadow, toSelect, toUploadFile, not: { diff --git a/packages/expect-puppeteer/src/matchers/notToMatch.js b/packages/expect-puppeteer/src/matchers/notToMatch.js index bbe45e2c..b6f3a6f5 100644 --- a/packages/expect-puppeteer/src/matchers/notToMatch.js +++ b/packages/expect-puppeteer/src/matchers/notToMatch.js @@ -3,18 +3,63 @@ import { defaultOptions } from '../options' async function notToMatch(instance, matcher, options) { options = defaultOptions(options) + const { traverseShadowRoots = false } = options const { page, handle } = await getContext(instance, () => document.body) try { await page.waitForFunction( - (handle, matcher) => { + (handle, matcher, traverseShadowRoots) => { + function getShadowTextContent(node) { + const walker = document.createTreeWalker( + node, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, + null, + false, + ) + let result = '' + let currentNode = walker.nextNode() + while (currentNode) { + if (currentNode.assignedSlot) { + // Skip everything within this subtree, since it's assigned to a slot in the shadow DOM. + const nodeWithAssignedSlot = currentNode + while ( + currentNode === nodeWithAssignedSlot || + nodeWithAssignedSlot.contains(currentNode) + ) { + currentNode = walker.nextNode() + } + // eslint-disable-next-line no-continue + continue + } else if (currentNode.nodeType === Node.TEXT_NODE) { + result += currentNode.textContent + } else if (currentNode.shadowRoot) { + result += getShadowTextContent(currentNode.shadowRoot) + } else if (typeof currentNode.assignedNodes === 'function') { + const assignedNodes = currentNode.assignedNodes() + // eslint-disable-next-line no-loop-func + assignedNodes.forEach((node) => { + result += getShadowTextContent(node) + }) + } + currentNode = walker.nextNode() + } + return result + } + if (!handle) return false - return handle.textContent.match(new RegExp(matcher)) === null + + const textContent = traverseShadowRoots + ? getShadowTextContent(handle) + : handle.textContent + + return textContent.match(new RegExp(matcher)) === null }, options, handle, matcher, + traverseShadowRoots, ) } catch (error) { throw enhanceError(error, `Text found "${matcher}"`) diff --git a/packages/expect-puppeteer/src/matchers/notToMatch.test.js b/packages/expect-puppeteer/src/matchers/notToMatch.test.js index fb1ee401..6fdb3076 100644 --- a/packages/expect-puppeteer/src/matchers/notToMatch.test.js +++ b/packages/expect-puppeteer/src/matchers/notToMatch.test.js @@ -5,43 +5,51 @@ describe('not.toMatch', () => { await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}`) }) - describe.each(['Page', 'Frame'])('%s', (pageType) => { - let page - setupPage(pageType, ({ currentPage }) => { - page = currentPage - }) - it('should be ok if text is not in the page', async () => { - await expect(page).not.toMatch('Nop!') - }) - - it('should return an error if text is in the page', async () => { - expect.assertions(3) - - try { - await expect(page).not.toMatch('home') - } catch (error) { - expect(error.message).toMatch('Text found "home"') - expect(error.message).toMatch('waiting for function failed') - } - }) - }) + describe.each(['Page', 'Frame', 'ShadowPage', 'ShadowFrame'])( + '%s', + (pageType) => { + let page + setupPage(pageType, ({ currentPage }) => { + page = currentPage + }) - describe('ElementHandle', () => { - it('should be ok if text is in the page', async () => { - const dialogBtn = await page.$('#dialog-btn') - await expect(dialogBtn).not.toMatch('Nop') - }) - - it('should return an error if text is not in the page', async () => { - expect.assertions(3) - const dialogBtn = await page.$('#dialog-btn') - - try { - await expect(dialogBtn).not.toMatch('Open dialog') - } catch (error) { - expect(error.message).toMatch('Text found "Open dialog"') - expect(error.message).toMatch('waiting for function failed') - } - }) - }) + const options = ['ShadowPage', 'ShadowFrame'].includes(pageType) + ? { traverseShadowRoots: true } + : {} + + it('should be ok if text is not in the page', async () => { + await expect(page).not.toMatch('Nop!', options) + }) + + it('should return an error if text is in the page', async () => { + expect.assertions(3) + + try { + await expect(page).not.toMatch('home', options) + } catch (error) { + expect(error.message).toMatch('Text found "home"') + expect(error.message).toMatch('waiting for function failed') + } + }) + + describe('ElementHandle', () => { + it('should be ok if text is in the page', async () => { + const dialogBtn = await page.$('#dialog-btn') + await expect(dialogBtn).not.toMatch('Nop', options) + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(3) + const dialogBtn = await page.$('#dialog-btn') + + try { + await expect(dialogBtn).not.toMatch('Open dialog', options) + } catch (error) { + expect(error.message).toMatch('Text found "Open dialog"') + expect(error.message).toMatch('waiting for function failed') + } + }) + }) + }, + ) }) diff --git a/packages/expect-puppeteer/src/matchers/toMatch.js b/packages/expect-puppeteer/src/matchers/toMatch.js index c3f58d6b..f95a0219 100644 --- a/packages/expect-puppeteer/src/matchers/toMatch.js +++ b/packages/expect-puppeteer/src/matchers/toMatch.js @@ -3,6 +3,7 @@ import { defaultOptions } from '../options' async function toMatch(instance, matcher, options) { options = defaultOptions(options) + const { traverseShadowRoots = false } = options const { page, handle } = await getContext(instance, () => document.body) @@ -10,19 +11,62 @@ async function toMatch(instance, matcher, options) { try { await page.waitForFunction( - (handle, text, regexp) => { + (handle, text, regexp, traverseShadowRoots) => { + function getShadowTextContent(node) { + const walker = document.createTreeWalker( + node, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, + null, + false, + ) + let result = '' + let currentNode = walker.nextNode() + while (currentNode) { + if (currentNode.assignedSlot) { + // Skip everything within this subtree, since it's assigned to a slot in the shadow DOM. + const nodeWithAssignedSlot = currentNode + while ( + currentNode === nodeWithAssignedSlot || + nodeWithAssignedSlot.contains(currentNode) + ) { + currentNode = walker.nextNode() + } + // eslint-disable-next-line no-continue + continue + } else if (currentNode.nodeType === Node.TEXT_NODE) { + result += currentNode.textContent + } else if (currentNode.shadowRoot) { + result += getShadowTextContent(currentNode.shadowRoot) + } else if (typeof currentNode.assignedNodes === 'function') { + const assignedNodes = currentNode.assignedNodes() + // eslint-disable-next-line no-loop-func + assignedNodes.forEach((node) => { + result += getShadowTextContent(node) + }) + } + currentNode = walker.nextNode() + } + return result + } + if (!handle) return false + + const textContent = traverseShadowRoots + ? getShadowTextContent(handle) + : handle.textContent + if (regexp !== null) { const [, pattern, flags] = regexp.match(/\/(.*)\/(.*)?/) return ( - handle.textContent + textContent .replace(/\s+/g, ' ') .trim() .match(new RegExp(pattern, flags)) !== null ) } if (text !== null) { - return handle.textContent.replace(/\s+/g, ' ').trim().includes(text) + return textContent.replace(/\s+/g, ' ').trim().includes(text) } return false }, @@ -30,6 +74,7 @@ async function toMatch(instance, matcher, options) { handle, text, regexp, + traverseShadowRoots, ) } catch (error) { throw enhanceError(error, `Text not found "${matcher}"`) diff --git a/packages/expect-puppeteer/src/matchers/toMatch.test.js b/packages/expect-puppeteer/src/matchers/toMatch.test.js index 069f2e1a..bcf65e41 100644 --- a/packages/expect-puppeteer/src/matchers/toMatch.test.js +++ b/packages/expect-puppeteer/src/matchers/toMatch.test.js @@ -5,52 +5,60 @@ describe('toMatch', () => { await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}`) }) - describe.each(['Page', 'Frame'])('%s', (pageType) => { - let page - setupPage(pageType, ({ currentPage }) => { - page = currentPage - }) - it('should be ok if text is in the page', async () => { - await expect(page).toMatch('This is home!') - }) - - it('should support RegExp', async () => { - await expect(page).toMatch(/THIS.is.home/i) - }) - - it('should return an error if text is not in the page', async () => { - expect.assertions(3) - - try { - await expect(page).toMatch('Nop') - } catch (error) { - expect(error.message).toMatch('Text not found "Nop"') - expect(error.message).toMatch('waiting for function failed') - } - }) - }) + describe.each(['Page', 'Frame', 'ShadowPage', 'ShadowFrame'])( + '%s', + (pageType) => { + let page + setupPage(pageType, ({ currentPage }) => { + page = currentPage + }) - describe('ElementHandle', () => { - it('should be ok if text is in the page', async () => { - const dialogBtn = await page.$('#dialog-btn') - await expect(dialogBtn).toMatch('Open dialog') - }) - - it('should support RegExp', async () => { - const dialogBtn = await page.$('#dialog-btn') - await expect(dialogBtn).toMatch(/OPEN/i) - }) - - it('should return an error if text is not in the page', async () => { - expect.assertions(3) - const dialogBtn = await page.$('#dialog-btn') - - try { - await expect(dialogBtn).toMatch('This is home!') - } catch (error) { - expect(error.message).toMatch('Text not found "This is home!"') - expect(error.message).toMatch('waiting for function failed') - } - }) - }) + const options = ['ShadowPage', 'ShadowFrame'].includes(pageType) + ? { traverseShadowRoots: true } + : {} + + it('should be ok if text is in the page', async () => { + await expect(page).toMatch('This is home!', options) + }) + + it('should support RegExp', async () => { + await expect(page).toMatch(/THIS.is.home/i, options) + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(3) + + try { + await expect(page).toMatch('Nop', options) + } catch (error) { + expect(error.message).toMatch('Text not found "Nop"') + expect(error.message).toMatch('waiting for function failed') + } + }) + + describe('ElementHandle', () => { + it('should be ok if text is in the page', async () => { + const dialogBtn = await page.$('#dialog-btn') + await expect(dialogBtn).toMatch('Open dialog', options) + }) + + it('should support RegExp', async () => { + const dialogBtn = await page.$('#dialog-btn') + await expect(dialogBtn).toMatch(/OPEN/i, options) + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(3) + const dialogBtn = await page.$('#dialog-btn') + + try { + await expect(dialogBtn).toMatch('This is home!', options) + } catch (error) { + expect(error.message).toMatch('Text not found "This is home!"') + expect(error.message).toMatch('waiting for function failed') + } + }) + }) + }, + ) }) diff --git a/packages/expect-puppeteer/src/matchers/toMatchInShadow.js b/packages/expect-puppeteer/src/matchers/toMatchInShadow.js deleted file mode 100644 index a1b47c91..00000000 --- a/packages/expect-puppeteer/src/matchers/toMatchInShadow.js +++ /dev/null @@ -1,78 +0,0 @@ -import { getContext, enhanceError, expandSearchExpr } from '../utils' -import { defaultOptions } from '../options' - -async function toMatchInShadow(instance, matcher, options) { - options = defaultOptions(options) - - const { page, handle } = await getContext(instance, () => document.body) - - const { text, regexp } = expandSearchExpr(matcher) - - try { - await page.waitForFunction( - (handle, text, regexp) => { - function getShadowTextContent(node) { - const walker = document.createTreeWalker( - node, - // eslint-disable-next-line no-bitwise - NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, - null, - false, - ) - let result = '' - let currentNode = walker.nextNode() - while (currentNode) { - if (currentNode.assignedSlot) { - // Skip everything within this subtree, since it's assigned to a slot in the shadow DOM. - const nodeWithAssignedSlot = currentNode - while ( - currentNode === nodeWithAssignedSlot || - nodeWithAssignedSlot.contains(currentNode) - ) { - currentNode = walker.nextNode() - } - // eslint-disable-next-line no-continue - continue - } else if (currentNode.nodeType === Node.TEXT_NODE) { - result += currentNode.textContent - } else if (currentNode.shadowRoot) { - result += getShadowTextContent(currentNode.shadowRoot) - } else if (typeof currentNode.assignedNodes === 'function') { - const assignedNodes = currentNode.assignedNodes() - // eslint-disable-next-line no-loop-func - assignedNodes.forEach((node) => { - result += getShadowTextContent(node) - }) - } - currentNode = walker.nextNode() - } - return result - } - - if (!handle) return false - const textContent = getShadowTextContent(handle) - if (regexp !== null) { - const [, pattern, flags] = regexp.match(/\/(.*)\/(.*)?/) - return ( - textContent - .replace(/\s+/g, ' ') - .trim() - .match(new RegExp(pattern, flags)) !== null - ) - } - if (text !== null) { - return textContent.replace(/\s+/g, ' ').trim().includes(text) - } - return false - }, - options, - handle, - text, - regexp, - ) - } catch (error) { - throw enhanceError(error, `Text not found "${matcher}"`) - } -} - -export default toMatchInShadow diff --git a/packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js b/packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js deleted file mode 100644 index 6c620ade..00000000 --- a/packages/expect-puppeteer/src/matchers/toMatchInShadow.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { setupPage } from './setupPage' - -describe('toMatchInShadow', () => { - beforeEach(async () => { - await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}`) - }) - - describe.each(['ShadowPage', 'ShadowFrame'])('%s', (pageType) => { - let page - setupPage(pageType, ({ currentPage }) => { - page = currentPage - }) - it('should be ok if text is in the page in shadow DOM', async () => { - await expect(page).toMatchInShadow('This is home!') - }) - - it('should be ok if text is in the page in a slot', async () => { - await expect(page).toMatchInShadow('Light DOM content (slotted)') - }) - - it('should support RegExp', async () => { - await expect(page).toMatchInShadow(/THIS.is.home/i) - }) - - it('should return an error if text is not in the page', async () => { - expect.assertions(3) - - try { - await expect(page).toMatchInShadow('Nop') - } catch (error) { - expect(error.message).toMatch('Text not found "Nop"') - expect(error.message).toMatch('waiting for function failed') - } - }) - }) - - describe('ElementHandle', () => { - it('should be ok if text is in the page', async () => { - const dialogBtn = await page.$('#dialog-btn') - await expect(dialogBtn).toMatchInShadow('Open dialog') - }) - - it('should support RegExp', async () => { - const dialogBtn = await page.$('#dialog-btn') - await expect(dialogBtn).toMatchInShadow(/OPEN/i) - }) - - it('should return an error if text is not in the page', async () => { - expect.assertions(3) - const dialogBtn = await page.$('#dialog-btn') - - try { - await expect(dialogBtn).toMatchInShadow('This is home!') - } catch (error) { - expect(error.message).toMatch('Text not found "This is home!"') - expect(error.message).toMatch('waiting for function failed') - } - }) - }) -})