8000 feat: add traverseShadowRoots option to toMatch (#463) · argos-ci/jest-puppeteer@28c5235 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 28c5235

Browse files
authored
feat: add traverseShadowRoots option to toMatch (#463)
* feat: adding a new matcher for shadow DOM toMatch * fix: fixing incorrect page setup * fix: rewording export in toMatchInShadow * fix: moving toMatchInShadow into toMatch
1 parent 3f85633 commit 28c5235

File tree

8 files changed

+237
-100
lines changed

8 files changed

+237
-100
lines changed

packages/expect-puppeteer/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Expect a text or a string RegExp to be present in the page or element.
164164
- `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes.
165165
- `mutation` - to execute `pageFunction` on every DOM mutation.
166166
- `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.
167+
- `traverseShadowRoots`<[boolean]> Whether shadow roots should be traversed to find a match.
167168

168169
```js
169170
// Matching using text

packages/expect-puppeteer/src/matchers/notToMatch.js

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,63 @@ import { defaultOptions } from '../options'
33

44
async function notToMatch(instance, matcher, options) {
55
options = defaultOptions(options)
6+
const { traverseShadowRoots = false } = options
67

78
const { page, handle } = await getContext(instance, () => document.body)
89

910
try {
1011
await page.waitForFunction(
11-
(handle, matcher) => {
12+
(handle, matcher, traverseShadowRoots) => {
13+
function getShadowTextContent(node) {
14+
const walker = document.createTreeWalker(
15+
node,
16+
// eslint-disable-next-line no-bitwise
17+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
18+
null,
19+
false,
20+
)
21+
let result = ''
22+
let currentNode = walker.nextNode()
23+
while (currentNode) {
24+
if (currentNode.assignedSlot) {
25+
// Skip everything within this subtree, since it's assigned to a slot in the shadow DOM.
26+
const nodeWithAssignedSlot = currentNode
27+
while (
28+
currentNode === nodeWithAssignedSlot ||
29+
nodeWithAssignedSlot.contains(currentNode)
30+
) {
31+
currentNode = walker.nextNode()
32+
}
33+
// eslint-disable-next-line no-continue
34+
continue
35+
} else if (currentNode.nodeType === Node.TEXT_NODE) {
36+
result += currentNode.textContent
37+
} else if (currentNode.shadowRoot) {
38+
result += getShadowTextContent(currentNode.shadowRoot)
39+
} else if (typeof currentNode.assignedNodes === 'function') {
40+
const assignedNodes = currentNode.assignedNodes()
41+
// eslint-disable-next-line no-loop-func
42+
assignedNodes.forEach((node) => {
43+
result += getShadowTextContent(node)
44+
})
45+
}
46+
currentNode = walker.nextNode()
47+
}
48+
return result
49+
}
50+
1251
if (!handle) return false
13-
return handle.textContent.match(new RegExp(matcher)) === null
52+
53+
const textContent = traverseShadowRoots
54+
? getShadowTextContent(handle)
55+
: handle.textContent
56+
57+
return textContent.match(new RegExp(matcher)) === null
1458
},
1559
options,
1660
handle,
1761
matcher,
62+
traverseShadowRoots,
1863
)
1964
} catch (error) {
2065
throw enhanceError(error, `Text found "${matcher}"`)

packages/expect-puppeteer/src/matchers/notToMatch.test.js

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,51 @@ describe('not.toMatch', () => {
55
await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}`)
66
})
77

8-
describe.each(['Page', 'Frame'])('%s', (pageType) => {
9-
let page
10-
setupPage(pageType, ({ currentPage }) => {
11-
page = currentPage
12-
})
13-
it('should be ok if text is not in the page', async () => {
14-
await expect(page).not.toMatch('Nop!')
15-
})
16-
17-
it('should return an error if text is in the page', async () => {
18-
expect.assertions(3)
19-
20-
try {
21-
await expect(page).not.toMatch('home')
22-
} catch (error) {
23-
expect(error.message).toMatch('Text found "home"')
24-
expect(error.message).toMatch('waiting for function failed')
25-
}
26-
})
27-
})
8+
describe.each(['Page', 'Frame', 'ShadowPage', 'ShadowFrame'])(
9+
'%s',
10+
(pageType) => {
11+
let page
12+
setupPage(pageType, ({ currentPage }) => {
13+
page = currentPage
14+
})
2815

29-
describe('ElementHandle', () => {
30-
it('should be ok if text is in the page', async () => {
31-
const dialogBtn = await page.$('#dialog-btn')
32-
await expect(dialogBtn).not.toMatch('Nop')
33-
})
34-
35-
it('should return an error if text is not in the page', async () => {
36-
expect.assertions(3)
37-
const dialogBtn = await page.$('#dialog-btn')
38-
39-
try {
40-
await expect(dialogBtn).not.toMatch('Open dialog')
41-
} catch (error) {
42-
expect(error.message).toMatch('Text found "Open dialog"')
43-
expect(error.message).toMatch('waiting for function failed')
44-
}
45-
})
46-
})
16+
const options = ['ShadowPage', 'ShadowFrame'].includes(pageType)
17+
? { traverseShadowRoots: true }
18+
: {}
19+
20+
it('should be ok if text is not in the page', async () => {
21+
await expect(page).not.toMatch('Nop!', options)
22+
})
23+
24+
it('should return an error if text is in the page', async () => {
25+
expect.assertions(3)
26+
27+
try {
28+
await expect(page).not.toMatch('home', options)
29+
} catch (error) {
30+
expect(error.message).toMatch('Text found "home"')
31+
expect(error.message).toMatch('waiting for function failed')
32+
}
33+
})
34+
35+
describe('ElementHandle', () => {
36+
it('should be ok if text is in the page', async () => {
37+
const dialogBtn = await page.$('#dialog-btn')
38+
await expect(dialogBtn).not.toMatch('Nop', options)
39+
})
40+
41+
it('should return an error if text is not in the page', async () => {
42+
expect.assertions(3)
43+
const dialogBtn = await page.$('#dialog-btn')
44+
45+
try {
46+
await expect(dialogBtn).not.toMatch('Open dialog', options)
47+
} catch (error) {
48+
expect(error.message).toMatch('Text found "Open dialog"')
49+
expect(error.message).toMatch('waiting for function failed')
50+
}
51+
})
52+
})
53+
},
54+
)
4755
})

packages/expect-puppeteer/src/matchers/setupPage.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,29 @@ function waitForFrame(page) {
1212
return promise
1313
}
1414

15-
export const setupPage = (pageType, cb) => {
15+
async function goToPage(page, route, isFrame, cb) {
1616
let currentPage = page
17+
await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}/${route}`)
18+
if (isFrame) {
19+
currentPage = await waitForFrame(page)
20+
}
21+
cb({
22+
currentPage,
23+
})
24+
}
25+
26+
export const setupPage = (pageType, cb) => {
1727
beforeEach(async () => {
1828
if (pageType === `Page`) {
1929
cb({
20-
currentPage,
30+
currentPage: page,
2131
})
22-
return
32+
} else if (pageType === 'ShadowPage') {
33+
await goToPage(page, 'shadow.html', false, cb)
34+
} else if (pageType === 'ShadowFrame') {
35+
await goToPage(page, 'shadowFrame.html', true, cb)
36+
} else {
37+
await goToPage(page, 'frame.html', true, cb)
2338
}
24-
await page.goto(
25-
`http://localhost:${process.env.TEST_SERVER_PORT}/frame.html`,
26-
)
27-
currentPage = await waitForFrame(page)
28-
cb({
29-
currentPage,
30-
})
3139
})
3240
}

packages/expect-puppeteer/src/matchers/toMatch.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,78 @@ import { defaultOptions } from '../options'
33

44
async function toMatch(instance, matcher, options) {
55
options = defaultOptions(options)
6+
const { traverseShadowRoots = false } = options
67

78
const { page, handle } = await getContext(instance, () => document.body)
89

910
const { text, regexp } = expandSearchExpr(matcher)
1011

1112
try {
1213
await page.waitForFunction(
13-
(handle, text, regexp) => {
14+
(handle, text, regexp, traverseShadowRoots) => {
15+
function getShadowTextContent(node) {
16+
const walker = document.createTreeWalker(
17+
node,
18+
// eslint-disable-next-line no-bitwise
19+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
20+
null,
21+
false,
22+
)
23+
let result = ''
24+
let currentNode = walker.nextNode()
25+
while (currentNode) {
26+
if (currentNode.assignedSlot) {
27+
// Skip everything within this subtree, since it's assigned to a slot in the shadow DOM.
28+
const nodeWithAssignedSlot = currentNode
29+
while (
30+
currentNode === nodeWithAssignedSlot ||
31+
nodeWithAssignedSlot.contains(currentNode)
32+
) {
33+
currentNode = walker.nextNode()
34+
}
35+
// eslint-disable-next-line no-continue
36+
continue
37+
} else if (currentNode.nodeType === Node.TEXT_NODE) {
38+
result += currentNode.textContent
39+
} else if (currentNode.shadowRoot) {
40+
result += getShadowTextContent(currentNode.shadowRoot)
41+
} else if (typeof currentNode.assignedNodes === 'function') {
42+
const assignedNodes = currentNode.assignedNodes()
43+
// eslint-disable-next-line no-loop-func
44+
assignedNodes.forEach((node) => {
45+
result += getShadowTextContent(node)
46+
})
47+
}
48+
currentNode = walker.nextNode()
49+
}
50+
return result
51+
}
52+
1453
if (!handle) return false
54+
55+
const textContent = traverseShadowRoots
56+
? getShadowTextContent(handle)
57+
: handle.textContent
58+
1559
if (regexp !== null) {
1660
const [, pattern, flags] = regexp.match(/\/(.*)\/(.*)?/)
1761
return (
18-
handle.textContent
62+
textContent
1963
.replace(/\s+/g, ' ')
2064
.trim()
2165
.match(new RegExp(pattern, flags)) !== null
2266
)
2367
}
2468
if (text !== null) {
25-
return handle.textContent.replace(/\s+/g, ' ').trim().includes(text)
69+
return textContent.replace(/\s+/g, ' ').trim().includes(text)
2670
}
2771
return false
2872
},
2973
options,
3074
handle,
3175
text,
3276
regexp,
77+
traverseShadowRoots,
3378
)
3479
} catch (error) {
3580
throw enhanceError(error, `Text not found "${matcher}"`)

packages/expect-puppeteer/src/matchers/toMatch.test.js

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,60 @@ describe('toMatch', () => {
55
await page.goto(`http://localhost:${process.env.TEST_SERVER_PORT}`)
66
})
77

8-
describe.each(['Page', 'Frame'])('%s', (pageType) => {
9-
let page
10-
setupPage(pageType, ({ currentPage }) => {
11-
page = currentPage
12-
})
13-
it('should be ok if text is in the page', async () => {
14-
await expect(page).toMatch('This is home!')
15-
})
16-
17-
it('should support RegExp', async () => {
18-
await expect(page).toMatch(/THIS.is.home/i)
19-
})
20-
21-
it('should return an error if text is not in the page', async () => {
22-
expect.assertions(3)
23-
24-
try {
25-
await expect(page).toMatch('Nop')
26-
} catch (error) {
27-
expect(error.message).toMatch('Text not found "Nop"')
28-
expect(error.message).toMatch('waiting for function failed')
29-
}
30-
})
31-
})
8+
describe.each(['Page', 'Frame', 'ShadowPage', 'ShadowFrame'])(
9+
'%s',
10+
(pageType) => {
11+
let page
12+
setupPage(pageType, ({ currentPage }) => {
13+
page = currentPage
14+
})
3215

33-
describe('ElementHandle', () => {
34-
it('should be ok if text is in the page', async () => {
35-
const dialogBtn = await page.$('#dialog-btn')
36-
await expect(dialogBtn).toMatch('Open dialog')
37-
})
38-
39-
it('should support RegExp', async () => {
40-
const dialogBtn = await page.$('#dialog-btn')
41-
await expect(dialogBtn).toMatch(/OPEN/i)
42-
})
43-
44-
it('should return an error if text is not in the page', async () => {
45-
expect.assertions(3)
46-
const dialogBtn = await page.$('#dialog-btn')
47-
48-
try {
49-
await expect(dialogBtn).toMatch('This is home!')
50-
} catch (error) {
51-
expect(error.message).toMatch('Text not found "This is home!"')
52-
expect(error.message).toMatch('waiting for function failed')
53-
}
54-
})
55-
})
16+
const options = ['ShadowPage', 'ShadowFrame'].includes(pageType)
17+
? { traverseShadowRoots: true }
18+
: {}
19+
20+
it('should be ok if text is in the page', async () => {
21+
await expect(page).toMatch('This is home!', options)
22+
})
23+
24+
it('should support RegExp', async () => {
25+
await expect(page).toMatch(/THIS.is.home/i, options)
26+
})
27+
28+
it('should return an error if text is not in the page', async () => {
29+
expect.assertions(3)
30+
31+
try {
32+
await expect(page).toMatch('Nop', options)
33+
} catch (error) {
34+
expect(error.message).toMatch('Text not found "Nop"')
35+
expect(error.message).toMatch('waiting for function failed')
36+
}
37+
})
38+
39+
describe('ElementHandle', () => {
40+
it('should be ok if text is in the page', async () => {
41+
const dialogBtn = await page.$('#dialog-btn')
42+
await expect(dialogBtn).toMatch('Open dialog', options)
43+
})
44+
45+
it('should support RegExp', async () => {
46+
const dialogBtn = await page.$('#dialog-btn')
47+
await expect(dialogBtn).toMatch(/OPEN/i, options)
48+
})
49+
50+
it('should return an error if text is not in the page', async () => {
51+
expect.assertions(3)
52+
const dialogBtn = await page.$('#dialog-btn')
53+
54+
try {
55+
await expect(dialogBtn).toMatch('This is home!', options)
56+
} catch (error) {
57+
expect(error.message).toMatch('Text not found "This is home!"')
58+
expect(error.message).toMatch('waiting for function failed')
59+
}
60+
})
61+
})
62+
},
63+
)
5664
})

0 commit comments

Comments
 (0)
0