Skip to content

Commit

Permalink
fix: moving toMatchInShadow into toMatch
Browse files Browse the repository at this point in the history
  • Loading branch information
k-j-kim committed Jan 13, 2022
1 parent ed363a7 commit 936f327
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 231 deletions.
1 change: 1 addition & 0 deletions packages/expect-puppeteer/README.md
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions packages/expect-puppeteer/src/index.js
Expand Up @@ -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'

Expand All @@ -21,7 +20,6 @@ const pageMatchers = {
toFillForm,
toMatch,
toMatchElement,
toMatchInShadow,
toSelect,
toUploadFile,
not: {
Expand All @@ -36,7 +34,6 @@ const elementHandleMatchers = {
toFillForm,
toMatch,
toMatchElement,
toMatchInShadow,
toSelect,
toUploadFile,
not: {
Expand Down
49 changes: 47 additions & 2 deletions packages/expect-puppeteer/src/matchers/notToMatch.js
Expand Up @@ -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}"`)
Expand Down
84 changes: 46 additions & 38 deletions packages/expect-puppeteer/src/matchers/notToMatch.test.js
Expand Up @@ -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')
}
})
})
},
)
})
51 changes: 48 additions & 3 deletions packages/expect-puppeteer/src/matchers/toMatch.js
Expand Up @@ -3,33 +3,78 @@ 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)

const { text, regexp } = expandSearchExpr(matcher)

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
},
options,
handle,
text,
regexp,
traverseShadowRoots,
)
} catch (error) {
throw enhanceError(error, `Text not found "${matcher}"`)
Expand Down
102 changes: 55 additions & 47 deletions packages/expect-puppeteer/src/matchers/toMatch.test.js
Expand Up @@ -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')
}
})
})
},
)
})

0 comments on commit 936f327

Please sign in to comment.