From 4d37d5bfd99d0ff0b9137b68c1c1262cfa5b389b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 16 Mar 2018 11:19:15 +0100 Subject: [PATCH] feat: add element handle support - Add toMatchElement method - Make all matchers wait before an element is present - All matchers accept an ElementHandle or a Page --- package.json | 2 +- packages/expect-puppeteer/README.md | 92 +++++++++++++++---- packages/expect-puppeteer/src/index.js | 62 +++++++++---- .../src/matchers/notToMatch.js | 15 ++- .../src/matchers/notToMatch.test.js | 38 ++++++-- .../src/matchers/notToMatchElement.js | 38 ++++++++ .../src/matchers/notToMatchElement.test.js | 48 ++++++++++ .../expect-puppeteer/src/matchers/toClick.js | 25 +---- .../src/matchers/toClick.test.js | 67 ++++++++++---- .../expect-puppeteer/src/matchers/toFill.js | 13 +-- .../src/matchers/toFill.test.js | 50 +++++++--- .../src/matchers/toFillForm.js | 24 ++--- .../src/matchers/toFillForm.test.js | 74 ++++++++++----- .../expect-puppeteer/src/matchers/toMatch.js | 15 ++- .../src/matchers/toMatch.test.js | 38 ++++++-- .../src/matchers/toMatchElement.js | 30 ++++++ .../src/matchers/toMatchElement.test.js | 62 +++++++++++++ .../expect-puppeteer/src/matchers/toSelect.js | 86 ++++++++++++++--- .../src/matchers/toSelect.test.js | 77 +++++++++++----- .../src/matchers/toUploadFile.js | 14 +-- .../src/matchers/toUploadFile.test.js | 54 ++++++++--- packages/expect-puppeteer/src/utils.js | 39 ++++++++ server/public/index.html | 6 ++ 23 files changed, 758 insertions(+), 211 deletions(-) create mode 100644 packages/expect-puppeteer/src/matchers/notToMatchElement.js create mode 100644 packages/expect-puppeteer/src/matchers/notToMatchElement.test.js create mode 100644 packages/expect-puppeteer/src/matchers/toMatchElement.js create mode 100644 packages/expect-puppeteer/src/matchers/toMatchElement.test.js create mode 100644 packages/expect-puppeteer/src/utils.js diff --git a/package.json b/package.json index c1ee608e..94383439 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "format": "prettier --write \"packages/**/*.{js,json,md}\" \"*.{js,json,md}\"", "lint": "eslint .", "release": "lerna publish && conventional-github-releaser -p angular", - "test": "jest" + "test": "jest --runInBand" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/packages/expect-puppeteer/README.md b/packages/expect-puppeteer/README.md index 58c01cfa..675f94f7 100644 --- a/packages/expect-puppeteer/README.md +++ b/packages/expect-puppeteer/README.md @@ -35,32 +35,45 @@ To use with Jest, just modify your configuration: } ``` +## Why do I need it + +Writing integration test is very hard especially in Single Page Application. Data are loaded asynchronously and it is difficult to know exactly when it will be displayed in the page. + +Puppeteer API is great, all this methods are built with it but it is low level and not designed to test an application. This API is designed for integration testing and will wait element before running each action. + ## API ##### Table of Contents -* [toClick](#expectpagetoclickselector-options) -* [toDisplayDialog](#expectpagetodisplaydialogblock) -* [toFill](#expectpagetofillselector-value-options) -* [toFillForm](#expectpagetofillformselector-values-options) -* [toMatch](#expectpagetomatchtext) -* [toSelect](#expectpagetoselectselector-valueortext) -* [toUploadFile](#expectpagetouploadfileselector-filepath) +* [toClick](#toClick) +* [toDisplayDialog](#toDisplayDialog) +* [toFill](#toFill) +* [toFillForm](#toFillForm) +* [toMatch](#toMatch) +* [toMatchElement](#toMatchElement) +* [toSelect](#toSelect) +* [toUploadFile](#toUploadFile) -### expect(page).toClick(selector[, options]) +### expect(instance).toClick(selector[, options]) +* `instance` <[Page]|[ElementHandle]> Context * `selector` <[string]> A [selector] to click on * `options` <[Object]> Optional parameters - * text <[string]> A text to match + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. + * `text` <[string]> A text or a RegExp to match in element `textContent`. ```js await expect(page).toClick('button', { text: 'Home' }) ``` -### expect(page).toDisplayDialog(block) +### expect(page).toDisplayDialog(block) +* `page` <[Page]> Context * `block` <[function]> A [function] that should trigger a dialog ```js @@ -69,22 +82,30 @@ await expect(page).toDisplayDialog(async () => { }) ``` -### expect(page).toFill(selector, value[, options]) +### expect(instance).toFill(selector, value[, options]) +* `instance` <[Page]|[ElementHandle]> Context * `selector` <[string]> A [selector] to match field * `value` <[string]> Value to fill * `options` <[Object]> Optional parameters + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. ```js await expect(page).toFill('input[name="firstName"]', 'James') ``` -### expect(page).toFillForm(selector, values[, options]) +### expect(instance).toFillForm(selector, values[, options]) +* `instance` <[Page]|[ElementHandle]> Context * `selector` <[string]> A [selector] to match form * `values` <[Object]> Values to fill * `options` <[Object]> Optional parameters + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. ```js @@ -94,29 +115,66 @@ await expect(page).toFillForm('form[name="myForm"]', { }) ``` -### expect(page).toMatch(text) +### expect(instance).toMatch(matcher[, options]) -* `text` <[string]> A text to match in page +* `instance` <[Page]|[ElementHandle]> Context +* `matcher` <[string]> A text or a RegExp to match in page * `options` <[Object]> Optional parameters + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. ```js +// Matching using text await expect(page).toMatch('Lorem ipsum') +// Matching using RegExp +await expect(page).toMatch('lo.*') ``` -### expect(page).toSelect(selector, valueOrText) +### expect(instance).toMatchElement(selector[, options]) +* `instance` <[Page]|[ElementHandle]> Context +* `selector` <[string]> A [selector] to match element +* `options` <[Object]> Optional parameters + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. + * `text` <[string]> A text or a RegExp to match in element `textContent`. + +```js +// Select a row containing a text +const row = await expect(page).toMatchElement('tr', { text: 'My row' }) +// Click on the third column link +await expect(row).toClick('td:nth-child(2) a') +``` + +### expect(instance).toSelect(selector, valueOrText[, options]) + +* `instance` <[Page]|[ElementHandle]> Context * `selector` <[string]> A [selector] to match select [element] * `valueOrText` <[string]> Value or text matching option +* `options` <[Object]> Optional parameters + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. ```js await expect(page).toSelect('select[name="choices"]', 'Choice 1') ``` -### expect(page).toUploadFile(selector, filePath) +### expect(instance).toUploadFile(selector, filePath[, options]) +* `instance` <[Page]|[ElementHandle]> Context * `selector` <[string]> A [selector] to match input [element] * `filePath` <[string]> A file path +* `options` <[Object]> Optional parameters + * `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: + * `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 `500`. ```js import path from 'path' @@ -148,3 +206,5 @@ MIT [element]: https://developer.mozilla.org/en-US/docs/Web/API/element 'Element' [map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map 'Map' [selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors 'selector' +[page]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page 'Page' +[element-handle]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-elementhandle 'ElementHandle' diff --git a/packages/expect-puppeteer/src/index.js b/packages/expect-puppeteer/src/index.js index 58623e5c..5776f6ac 100644 --- a/packages/expect-puppeteer/src/index.js +++ b/packages/expect-puppeteer/src/index.js @@ -1,23 +1,42 @@ /* eslint-disable no-use-before-define, no-restricted-syntax, no-await-in-loop */ -import toMatch from './matchers/toMatch' +import { getPuppeteerType } from './utils' +import notToMatch from './matchers/notToMatch' +import notToMatchElement from './matchers/notToMatchElement' import toClick from './matchers/toClick' -import toSelect from './matchers/toSelect' -import toUploadFile from './matchers/toUploadFile' +import toDisplayDialog from './matchers/toDisplayDialog' import toFill from './matchers/toFill' import toFillForm from './matchers/toFillForm' -import toDisplayDialog from './matchers/toDisplayDialog' -import notToMatch from './matchers/notToMatch' +import toMatch from './matchers/toMatch' +import toMatchElement from './matchers/toMatchElement' +import toSelect from './matchers/toSelect' +import toUploadFile from './matchers/toUploadFile' -const matchers = { - toMatch, +const pageMatchers = { toClick, + toDisplayDialog, + toFill, + toFillForm, + toMatch, + toMatchElement, toSelect, toUploadFile, + not: { + toMatch: notToMatch, + toMatchElement: notToMatchElement, + }, +} + +const elementHandleMatchers = { + toClick, toFill, toFillForm, - toDisplayDialog, + toMatch, + toMatchElement, + toSelect, + toUploadFile, not: { toMatch: notToMatch, + toMatchElement: notToMatchElement, }, } @@ -36,31 +55,40 @@ function createMatcher(matcher, page) { } } -function expectPage(page) { +function internalExpect(type, matchers) { const expectation = { not: {}, } Object.keys(matchers).forEach(key => { if (key === 'not') return - expectation[key] = createMatcher(matchers[key], page) + expectation[key] = createMatcher(matchers[key], type) }) Object.keys(matchers.not).forEach(key => { - expectation.not[key] = createMatcher(matchers.not[key], page) + expectation.not[key] = createMatcher(matchers.not[key], type) }) return expectation } +function expectPuppeteer(actual) { + const type = getPuppeteerType(actual) + switch (type) { + case 'Page': + return internalExpect(actual, pageMatchers) + case 'ElementHandle': + return internalExpect(actual, elementHandleMatchers) + default: + throw new Error(`${actual} is not supported`) + } +} + if (typeof global.expect !== 'undefined') { - const isPuppeteerPage = object => - Boolean(object && object.$ && object.$$ && object.close && object.click) const originalExpect = global.expect global.expect = (actual, ...args) => { - if (isPuppeteerPage(actual)) { - return expectPage(actual) - } + const type = getPuppeteerType(actual) + if (type) return expectPuppeteer(actual) return originalExpect(actual, ...args) } Object.keys(originalExpect).forEach(prop => { @@ -68,4 +96,4 @@ if (typeof global.expect !== 'undefined') { }) } -module.exports = expectPage +module.exports = expectPuppeteer diff --git a/packages/expect-puppeteer/src/matchers/notToMatch.js b/packages/expect-puppeteer/src/matchers/notToMatch.js index 77b609ce..e5c29c08 100644 --- a/packages/expect-puppeteer/src/matchers/notToMatch.js +++ b/packages/expect-puppeteer/src/matchers/notToMatch.js @@ -1,8 +1,19 @@ -async function notToMatch(page, matcher, options = { timeout: 500 }) { +import { defaultOptions, getContext } from '../utils' + +async function notToMatch(instance, matcher, options) { + options = defaultOptions(options) + + const { page, handle } = await getContext(instance, () => document.body) + try { await page.waitForFunction( - `document.body && document.body.textContent.match(new RegExp('${matcher}')) === null`, + (handle, matcher) => { + if (!handle) return false + return handle.textContent.match(new RegExp(matcher)) === null + }, options, + handle, + matcher, ) } catch (error) { throw new 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 0d915fb3..1a5ae690 100644 --- a/packages/expect-puppeteer/src/matchers/notToMatch.test.js +++ b/packages/expect-puppeteer/src/matchers/notToMatch.test.js @@ -3,17 +3,37 @@ describe('not.toMatch', () => { await page.goto('http://localhost:4444') }) - it('should be ok if text is not in the page', async () => { - await expect(page).not.toMatch('Nop!') + describe('Page', () => { + 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(2) + + try { + await expect(page).not.toMatch('home') + } catch (error) { + expect(error.message).toMatch('Text found "home"') + } + }) }) - it('should return an error if text is in the page', async () => { - expect.assertions(2) + 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(2) + const dialogBtn = await page.$('#dialog-btn') - try { - await expect(page).not.toMatch('home') - } catch (error) { - expect(error.message).toMatch('Text found "home"') - } + try { + await expect(dialogBtn).not.toMatch('Open dialog') + } catch (error) { + expect(error.message).toMatch('Text found "Open dialog"') + } + }) }) }) diff --git a/packages/expect-puppeteer/src/matchers/notToMatchElement.js b/packages/expect-puppeteer/src/matchers/notToMatchElement.js new file mode 100644 index 00000000..989b5f38 --- /dev/null +++ b/packages/expect-puppeteer/src/matchers/notToMatchElement.js @@ -0,0 +1,38 @@ +import { defaultOptions, getContext } from '../utils' + +async function notToMatchElement( + instance, + selector, + { text, ...options } = {}, +) { + options = defaultOptions(options) + + const { page, handle } = await getContext(instance, () => document) + + try { + await page.waitForFunction( + (handle, selector, text) => { + const elements = handle.querySelectorAll(selector) + if (text !== undefined) { + return [...elements].every( + ({ textContent }) => !textContent.match(text), + ) + } + + return elements.length === 0 + }, + options, + handle, + selector, + text, + ) + } catch (error) { + throw new Error( + `Element ${selector}${ + text !== undefined ? ` (text: "${text}") ` : ' ' + }found`, + ) + } +} + +export default notToMatchElement diff --git a/packages/expect-puppeteer/src/matchers/notToMatchElement.test.js b/packages/expect-puppeteer/src/matchers/notToMatchElement.test.js new file mode 100644 index 00000000..bf973a94 --- /dev/null +++ b/packages/expect-puppeteer/src/matchers/notToMatchElement.test.js @@ -0,0 +1,48 @@ +describe('not.toMatchElement', () => { + beforeEach(async () => { + await page.goto('http://localhost:4444') + }) + + describe('Page', () => { + it('should not match using selector', async () => { + await expect(page).not.toMatchElement('wtf') + }) + + it('should match using text', async () => { + await expect(page).not.toMatchElement('a', { text: 'Nothing here' }) + }) + + it('should return an error if element is not found', async () => { + expect.assertions(2) + + try { + await expect(page).not.toMatchElement('a', { text: 'Page 2' }) + } catch (error) { + expect(error.message).toMatch('Element a (text: "Page 2") found') + } + }) + }) + + describe('ElementHandle', () => { + it('should not match using selector', async () => { + const main = await page.$('main') + await expect(main).not.toMatchElement('main') + }) + + it('should match using text', async () => { + const main = await page.$('main') + await expect(main).not.toMatchElement('div', { text: 'Nothing here' }) + }) + + it('should return an error if element is not found', async () => { + const main = await page.$('main') + expect.assertions(2) + + try { + await expect(main).not.toMatchElement('div', { text: 'main' }) + } catch (error) { + expect(error.message).toMatch('Element div (text: "main") found') + } + }) + }) +}) diff --git a/packages/expect-puppeteer/src/matchers/toClick.js b/packages/expect-puppeteer/src/matchers/toClick.js index 85675154..f1168d4a 100644 --- a/packages/expect-puppeteer/src/matchers/toClick.js +++ b/packages/expect-puppeteer/src/matchers/toClick.js @@ -1,25 +1,8 @@ -async function toClick(page, selector, { text } = {}) { - await page.$$eval( - selector, - (elements, selector, text) => { - const element = - text !== undefined - ? [...elements].find(({ textContent }) => textContent.match(text)) - : elements[0] +import toMatchElement from './toMatchElement' - if (!element) { - throw new Error( - `Element ${selector} ${ - text !== undefined ? `(text: "${text}")` : '' - } not found`, - ) - } - - element.click() - }, - selector, - text, - ) +async function toClick(instance, selector, options) { + const element = await toMatchElement(instance, selector, options) + await element.click(options) } export default toClick diff --git a/packages/expect-puppeteer/src/matchers/toClick.test.js b/packages/expect-puppeteer/src/matchers/toClick.test.js index fa47f49b..38875e99 100644 --- a/packages/expect-puppeteer/src/matchers/toClick.test.js +++ b/packages/expect-puppeteer/src/matchers/toClick.test.js @@ -1,27 +1,60 @@ -describe('toMatch', () => { +describe('toClick', () => { beforeEach(async () => { await page.goto('http://localhost:4444') }) - it('should click using selector', async () => { - await expect(page).toClick('a[href="/page2.html"]') - const pathname = await page.evaluate(() => document.location.pathname) - expect(pathname).toBe('/page2.html') - }) + describe('Page', () => { + it('should click using selector', async () => { + await expect(page).toClick('a[href="/page2.html"]') + await page.waitForSelector('html') + const pathname = await page.evaluate(() => document.location.pathname) + expect(pathname).toBe('/page2.html') + }) + + it('should click using text', async () => { + await expect(page).toClick('a', { text: 'Page 2' }) + await page.waitForSelector('html') + const pathname = await page.evaluate(() => document.location.pathname) + expect(pathname).toBe('/page2.html') + }) + + it('should return an error if element is not found', async () => { + expect.assertions(2) - it('should click using text', async () => { - await expect(page).toClick('a', { text: 'Page 2' }) - const pathname = await page.evaluate(() => document.location.pathname) - expect(pathname).toBe('/page2.html') + try { + await expect(page).toClick('a', { text: 'Nop' }) + } catch (error) { + expect(error.message).toMatch('Element a (text: "Nop") not found') + } + }) }) - it('should return an error if element is not found', async () => { - expect.assertions(2) + describe('ElementHandle', () => { + it('should click using selector', async () => { + const body = await page.$('body') + await expect(body).toClick('a[href="/page2.html"]') + await page.waitForSelector('html') + const pathname = await page.evaluate(() => document.location.pathname) + expect(pathname).toBe('/page2.html') + }) + + it('should click using text', async () => { + const body = await page.$('body') + await expect(body).toClick('a', { text: 'Page 2' }) + await page.waitForSelector('html') + const pathname = await page.evaluate(() => document.location.pathname) + expect(pathname).toBe('/page2.html') + }) + + it('should return an error if element is not found', async () => { + const body = await page.$('body') + expect.assertions(2) - try { - await expect(page).toClick('a', { text: 'Nop' }) - } catch (error) { - expect(error.message).toMatch('Error: Element a (text: "Nop") not found') - } + try { + await expect(body).toClick('a', { text: 'Nop' }) + } catch (error) { + expect(error.message).toMatch('Element a (text: "Nop") not found') + } + }) }) }) diff --git a/packages/expect-puppeteer/src/matchers/toFill.js b/packages/expect-puppeteer/src/matchers/toFill.js index 80898498..30c7574b 100644 --- a/packages/expect-puppeteer/src/matchers/toFill.js +++ b/packages/expect-puppeteer/src/matchers/toFill.js @@ -1,12 +1,9 @@ -async function toFill(page, selector, value, options = { timeout: 500 }) { - try { - await page.waitFor(selector, options) - } catch (error) { - throw new Error(`Unable to find "${selector}" field`) - } +import toMatchElement from './toMatchElement' - await page.click(selector, { clickCount: 3 }) - await page.keyboard.type(value) +async function toFill(instance, selector, value, options) { + const element = await toMatchElement(instance, selector, options) + await element.click({ clickCount: 3 }) + await element.type(value) } export default toFill diff --git a/packages/expect-puppeteer/src/matchers/toFill.test.js b/packages/expect-puppeteer/src/matchers/toFill.test.js index c8611331..e8cfb2c2 100644 --- a/packages/expect-puppeteer/src/matchers/toFill.test.js +++ b/packages/expect-puppeteer/src/matchers/toFill.test.js @@ -3,21 +3,45 @@ describe('toFill', () => { await page.goto('http://localhost:4444') }) - it('should fill input', async () => { - await expect(page).toFill('[name="firstName"]', 'James') - const value = await page.evaluate( - () => document.querySelector('[name="firstName"]').value, - ) - expect(value).toBe('James') + describe('Page', () => { + it('should fill input', async () => { + await expect(page).toFill('[name="firstName"]', 'James') + const value = await page.evaluate( + () => document.querySelector('[name="firstName"]').value, + ) + expect(value).toBe('James') + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(2) + + try { + await expect(page).toFill('[name="notFound"]', 'James') + } catch (error) { + expect(error.message).toMatch('Element [name="notFound"] not found') + } + }) }) - it('should return an error if text is not in the page', async () => { - expect.assertions(2) + describe('ElementHandle', () => { + it('should fill input', async () => { + const body = await page.$('body') + await expect(body).toFill('[name="firstName"]', 'James') + const value = await page.evaluate( + () => document.querySelector('[name="firstName"]').value, + ) + expect(value).toBe('James') + }) + + it('should return an error if text is not in the page', async () => { + const body = await page.$('body') + expect.assertions(2) - try { - await expect(page).toFill('[name="notFound"]', 'James') - } catch (error) { - expect(error.message).toMatch('Unable to find "[name="notFound"]" field') - } + try { + await expect(body).toFill('[name="notFound"]', 'James') + } catch (error) { + expect(error.message).toMatch('Element [name="notFound"] not found') + } + }) }) }) diff --git a/packages/expect-puppeteer/src/matchers/toFillForm.js b/packages/expect-puppeteer/src/matchers/toFillForm.js index cc72aaeb..1b706663 100644 --- a/packages/expect-puppeteer/src/matchers/toFillForm.js +++ b/packages/expect-puppeteer/src/matchers/toFillForm.js @@ -1,25 +1,15 @@ +import { defaultOptions } from '../utils' import toFill from './toFill' +import toMatchElement from './toMatchElement' /* eslint-disable no-restricted-syntax, no-await-in-loop */ -async function toFillForm( - page, - formSelector, - values, - options = { timeout: 500 }, -) { - try { - await page.waitFor(formSelector, options) - } catch (error) { - throw new Error(`Unable to find "${formSelector}" form`) - } +async function toFillForm(instance, selector, values, options) { + options = defaultOptions(options) + + const form = await toMatchElement(instance, selector, options) for (const name of Object.keys(values)) { - await toFill( - page, - `${formSelector} [name="${name}"]`, - values[name], - options, - ) + await toFill(form, `[name="${name}"]`, values[name], options) } } diff --git a/packages/expect-puppeteer/src/matchers/toFillForm.test.js b/packages/expect-puppeteer/src/matchers/toFillForm.test.js index bd2312cc..b9801e3f 100644 --- a/packages/expect-puppeteer/src/matchers/toFillForm.test.js +++ b/packages/expect-puppeteer/src/matchers/toFillForm.test.js @@ -3,33 +3,65 @@ describe('toFillForm', () => { await page.goto('http://localhost:4444') }) - it('should fill input', async () => { - await expect(page).toFillForm('form', { - firstName: 'James', - lastName: 'Bond', + describe('Page', () => { + it('should fill input', async () => { + await expect(page).toFillForm('form', { + firstName: 'James', + lastName: 'Bond', + }) + const values = await page.evaluate(() => ({ + firstName: document.querySelector('[name="firstName"]').value, + lastName: document.querySelector('[name="lastName"]').value, + })) + expect(values).toEqual({ + firstName: 'James', + lastName: 'Bond', + }) }) - const values = await page.evaluate(() => ({ - firstName: document.querySelector('[name="firstName"]').value, - lastName: document.querySelector('[name="lastName"]').value, - })) - expect(values).toEqual({ - firstName: 'James', - lastName: 'Bond', + + it('should return an error if text is not in the page', async () => { + expect.assertions(2) + + try { + await expect(page).toFillForm('form[name="notFound"]', { + firstName: 'James', + lastName: 'Bond', + }) + } catch (error) { + expect(error.message).toMatch('Element form[name="notFound"] not found') + } }) }) - it('should return an error if text is not in the page', async () => { - expect.assertions(2) - - try { - await expect(page).toFillForm('form[name="notFound"]', { + describe('ElementHandle', () => { + it('should fill input', async () => { + const body = await page.$('body') + await expect(body).toFillForm('form', { + firstName: 'James', + lastName: 'Bond', + }) + const values = await page.evaluate(() => ({ + firstName: document.querySelector('[name="firstName"]').value, + lastName: document.querySelector('[name="lastName"]').value, + })) + expect(values).toEqual({ firstName: 'James', lastName: 'Bond', }) - } catch (error) { - expect(error.message).toMatch( - 'Unable to find "form[name="notFound"]" form', - ) - } + }) + + it('should return an error if text is not in the page', async () => { + const body = await page.$('body') + expect.assertions(2) + + try { + await expect(body).toFillForm('form[name="notFound"]', { + firstName: 'James', + lastName: 'Bond', + }) + } catch (error) { + expect(error.message).toMatch('Element form[name="notFound"] not found') + } + }) }) }) diff --git a/packages/expect-puppeteer/src/matchers/toMatch.js b/packages/expect-puppeteer/src/matchers/toMatch.js index 29fb44ed..372b6307 100644 --- a/packages/expect-puppeteer/src/matchers/toMatch.js +++ b/packages/expect-puppeteer/src/matchers/toMatch.js @@ -1,8 +1,19 @@ -async function toMatch(page, matcher, options = { timeout: 500 }) { +import { defaultOptions, getContext } from '../utils' + +async function toMatch(instance, matcher, options) { + options = defaultOptions(options) + + const { page, handle } = await getContext(instance, () => document.body) + try { await page.waitForFunction( - `document.body && document.body.textContent.match(new RegExp('${matcher}')) !== null`, + (handle, matcher) => { + if (!handle) return false + return handle.textContent.match(new RegExp(matcher)) !== null + }, options, + handle, + matcher, ) } catch (error) { throw new 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 f08067d7..54440874 100644 --- a/packages/expect-puppeteer/src/matchers/toMatch.test.js +++ b/packages/expect-puppeteer/src/matchers/toMatch.test.js @@ -3,17 +3,37 @@ describe('toMatch', () => { await page.goto('http://localhost:4444') }) - it('should be ok if text is in the page', async () => { - await expect(page).toMatch('This is home!') + describe('Page', () => { + it('should be ok if text is in the page', async () => { + await expect(page).toMatch('This is home!') + }) + + it('should return an error if text is not in the page', async () => { + expect.assertions(2) + + try { + await expect(page).toMatch('Nop') + } catch (error) { + expect(error.message).toMatch('Text not found "Nop"') + } + }) }) - it('should return an error if text is not in the page', async () => { - expect.assertions(2) + 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 return an error if text is not in the page', async () => { + expect.assertions(2) + const dialogBtn = await page.$('#dialog-btn') - try { - await expect(page).toMatch('Nop') - } catch (error) { - expect(error.message).toMatch('Text not found "Nop"') - } + try { + await expect(dialogBtn).toMatch('This is home!') + } catch (error) { + expect(error.message).toMatch('Text not found "This is home!"') + } + }) }) }) diff --git a/packages/expect-puppeteer/src/matchers/toMatchElement.js b/packages/expect-puppeteer/src/matchers/toMatchElement.js new file mode 100644 index 00000000..26a00386 --- /dev/null +++ b/packages/expect-puppeteer/src/matchers/toMatchElement.js @@ -0,0 +1,30 @@ +import { defaultOptions, getContext } from '../utils' + +async function toMatchElement(instance, selector, { text, ...options } = {}) { + options = defaultOptions(options) + + const { page, handle } = await getContext(instance, () => document) + + const getElement = (handle, selector, text) => { + const elements = handle.querySelectorAll(selector) + if (text !== undefined) { + return [...elements].find(({ textContent }) => textContent.match(text)) + } + return elements[0] + } + + try { + await page.waitForFunction(getElement, options, handle, selector, text) + } catch (error) { + throw new Error( + `Element ${selector}${ + text !== undefined ? ` (text: "${text}") ` : ' ' + }not found`, + ) + } + + const jsHandle = await page.evaluateHandle(getElement, handle, selector, text) + return jsHandle.asElement() +} + +export default toMatchElement diff --git a/packages/expect-puppeteer/src/matchers/toMatchElement.test.js b/packages/expect-puppeteer/src/matchers/toMatchElement.test.js new file mode 100644 index 00000000..0bab317e --- /dev/null +++ b/packages/expect-puppeteer/src/matchers/toMatchElement.test.js @@ -0,0 +1,62 @@ +describe('toMatchElement', () => { + beforeEach(async () => { + await page.goto('http://localhost:4444') + }) + + describe('Page', () => { + it('should match using selector', async () => { + const element = await expect(page).toMatchElement('a[href="/page2.html"]') + const textContentProperty = await element.getProperty('textContent') + const textContent = await textContentProperty.jsonValue() + expect(textContent).toBe('Page 2') + }) + + it('should match using text', async () => { + const element = await expect(page).toMatchElement('a', { text: 'Page 2' }) + const textContentProperty = await element.getProperty('textContent') + const textContent = await textContentProperty.jsonValue() + expect(textContent).toBe('Page 2') + }) + + it('should return an error if element is not found', async () => { + expect.assertions(2) + + try { + await expect(page).toMatchElement('a', { text: 'Nop' }) + } catch (error) { + expect(error.message).toMatch('Element a (text: "Nop") not found') + } + }) + }) + + describe('ElementHandle', () => { + it('should match using selector', async () => { + const main = await page.$('main') + const element = await expect(main).toMatchElement('#in-the-main') + const textContentProperty = await element.getProperty('textContent') + const textContent = await textContentProperty.jsonValue() + expect(textContent).toMatch('A div in the main') + }) + + it('should match using text', async () => { + const main = await page.$('main') + const element = await expect(main).toMatchElement('*', { + text: 'in the main', + }) + const textContentProperty = await element.getProperty('textContent') + const textContent = await textContentProperty.jsonValue() + expect(textContent).toMatch('A div in the main') + }) + + it('should return an error if element is not found', async () => { + const main = await page.$('main') + expect.assertions(2) + + try { + await expect(main).toMatchElement('a', { text: 'Page 2' }) + } catch (error) { + expect(error.message).toMatch('Element a (text: "Page 2") not found') + } + }) + }) +}) diff --git a/packages/expect-puppeteer/src/matchers/toSelect.js b/packages/expect-puppeteer/src/matchers/toSelect.js index 2dadd1a3..0f272fff 100644 --- a/packages/expect-puppeteer/src/matchers/toSelect.js +++ b/packages/expect-puppeteer/src/matchers/toSelect.js @@ -1,20 +1,78 @@ -async function toSelect(page, selector, valueOrText) { - const foundValue = await page.$$eval( - `${selector} option`, - (options, valueOrText, selector) => { - const option = options.find( - option => - option.value === valueOrText || option.textContent === valueOrText, - ) - if (!option) { - throw new Error(`Option not found "${selector}" ("${valueOrText}")`) +/* eslint-disable no-restricted-syntax */ +import toMatchElement from './toMatchElement' + +function select(page, element, value) { + return page.evaluate( + (element, value) => { + if (element.nodeName.toLowerCase() !== 'select') + throw new Error('Element is not a +
+ The main content of the page: it rocks!! +
+ A div in the main +
+