├── .npmignore ├── rollup.input.js ├── .gitignore ├── jest.config.js ├── rollup.config.js ├── .travis.yml ├── tsconfig.json ├── test ├── fixtures │ └── page.html ├── __snapshots__ │ └── extend.test.ts.snap ├── index.test.ts └── extend.test.ts ├── LICENSE ├── lib ├── extend.ts ├── typedefs.ts └── index.ts ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | .nyc_output/ 3 | .vscode/ 4 | test/ 5 | 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /rollup.input.js: -------------------------------------------------------------------------------- 1 | export * from 'dom-testing-library/dist/queries' 2 | export {getNodeText} from 'dom-testing-library/dist/get-node-text' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .nyc_output/ 4 | dist/ 5 | npm-debug.log 6 | 7 | dom-testing-library.js 8 | extend.js 9 | extend.d.ts 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['**/*.ts', '!**/*.d.ts'], 3 | transform: { 4 | '\\.ts$': 'ts-jest', 5 | }, 6 | moduleFileExtensions: ['ts', 'js', 'json'], 7 | testMatch: ['**/*.test.ts'], 8 | } 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | input: path.join(__dirname, 'rollup.input.js'), 5 | output: { 6 | file: 'dom-testing-library.js', 7 | format: 'iife', 8 | name: '__dom_testing_library__', 9 | }, 10 | plugins: [require('rollup-plugin-node-resolve')(), require('rollup-plugin-commonjs')()], 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | cache: yarn 4 | notifications: 5 | email: false 6 | node_js: 7 | - v10 8 | - v8 9 | before_install: 10 | - npm install -g yarn coveralls nyc @patrickhulce/scripts 11 | script: 12 | - yarn rebuild 13 | - yarn test:lint 14 | - yarn test:unit --coverage --runInBand --verbose 15 | after_success: 16 | - cat coverage/lcov.info | coveralls || echo 'Failed to upload to coveralls...' 17 | - hulk npm-publish --yes 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "target": "es2015", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "declaration": true, 10 | 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "strictNullChecks": true, 15 | "preserveWatchOutput": true, 16 | }, 17 | "include": [ 18 | "lib/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hello h1

6 |

Hello h2

7 | Image A 8 | 9 | 10 | 11 | 12 |
13 |

Hello h3

14 |
15 | 16 | 17 | 18 |
Layout table
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/__snapshots__/extend.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`lib/extend.ts should handle the LabelText methods 1`] = `""`; 4 | 5 | exports[`lib/extend.ts should handle the get* method failures 1`] = ` 6 | "Evaluation failed: Error: Unable to find an element with the title: missing. 7 | 8 |
11 | 12 | 13 |

14 | Hello h3 15 |

16 | 17 | 18 |
19 | at getElementError :X:X) 20 | at getAllByTitle :X:X) 21 | at firstResultOrNull :X:X) 22 | at Object.getByTitle :X:X) 23 | at anonymous :X:X)" 24 | `; 25 | 26 | exports[`lib/extend.ts should handle the get* methods 1`] = `""`; 27 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as puppeteer from 'puppeteer' 3 | import {getDocument, queries, getQueriesForElement} from '../lib' 4 | 5 | describe('lib/index.ts', () => { 6 | let browser: puppeteer.Browser 7 | let page: puppeteer.Page 8 | 9 | it('should launch puppeteer', async () => { 10 | browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}) 11 | page = await browser.newPage() 12 | await page.goto(`file://${path.join(__dirname, 'fixtures/page.html')}`) 13 | }) 14 | 15 | it('should export the utilities', async () => { 16 | const document = await getDocument(page) 17 | const element = await queries.getByText(document, 'Hello h1') 18 | expect(await queries.getNodeText(element)).toEqual('Hello h1') 19 | }) 20 | 21 | it('should bind getQueriesForElement', async () => { 22 | const {getByText} = getQueriesForElement(await getDocument(page)) 23 | const element = await getByText('Hello h1') 24 | expect(await queries.getNodeText(element)).toEqual('Hello h1') 25 | }) 26 | 27 | afterAll(async () => { 28 | await browser.close() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Patrick Hulce (https://patrickhulce.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/extend.ts: -------------------------------------------------------------------------------- 1 | import {getDocument, getQueriesForElement} from '.' 2 | import {ElementHandle} from '../node_modules/@types/puppeteer' 3 | import {IScopedQueryUtils} from './typedefs' 4 | 5 | // tslint:disable-next-line 6 | let Page, ElementHandle 7 | 8 | function requireOrUndefined(path: string): any { 9 | try { 10 | return require(path) 11 | } catch (err) {} 12 | } 13 | 14 | try { 15 | Page = require('puppeteer/lib/Page.js') // tslint:disable-line 16 | if (Page.Page) Page = Page.Page 17 | 18 | ElementHandle = requireOrUndefined('puppeteer/lib/ElementHandle.js') // tslint:disable-line 19 | if (!ElementHandle) { 20 | ElementHandle = require('puppeteer/lib/ExecutionContext.js').ElementHandle // tslint:disable-line 21 | } 22 | 23 | Page.prototype.getDocument = getDocument 24 | getQueriesForElement(ElementHandle.prototype, function(this: ElementHandle): ElementHandle { 25 | return this 26 | }) 27 | 28 | ElementHandle.prototype.getQueriesForElement = function(this: ElementHandle): ElementHandle { 29 | return getQueriesForElement(this) 30 | } 31 | } catch (err) { 32 | // tslint:disable-next-line 33 | console.error('Could not augment puppeteer functions, do you have a conflicting version?') 34 | throw err 35 | } 36 | 37 | /* tslint:disable */ 38 | declare module 'puppeteer' { 39 | interface Page { 40 | getDocument(): Promise 41 | } 42 | 43 | interface ElementHandle extends IScopedQueryUtils {} 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pptr-testing-library", 3 | "version": "0.0.0-development", 4 | "description": "puppeteer + dom-testing-library", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "test": "npm run test:lint && npm run test:unit", 8 | "test:unit": "jest", 9 | "test:lint": "lint -t typescript './lib/**/*.ts'", 10 | "semantic-release": "semantic-release", 11 | "clean": "rm -fR dist/", 12 | "rebuild": "npm run clean && npm run build", 13 | "prepublishOnly": "npm run rebuild && generate-export-aliases", 14 | "build": "npm run build:ts && npm run build:rollup", 15 | "build:ts": "tsc", 16 | "build:rollup": "rollup -c rollup.config.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/patrickhulce/pptr-testing-library.git" 21 | }, 22 | "author": "Patrick Hulce ", 23 | "license": "MIT", 24 | "homepage": "https://github.com/patrickhulce/pptr-testing-library#readme", 25 | "bugs": { 26 | "url": "https://github.com/patrickhulce/pptr-testing-library/issues" 27 | }, 28 | "keywords": [ 29 | "puppeteer", 30 | "dom-testing-library", 31 | "testing", 32 | "utility" 33 | ], 34 | "config": { 35 | "tslint": { 36 | "rules": { 37 | "no-unsafe-any": false 38 | } 39 | }, 40 | "exportAliases": { 41 | "extend": "./dist/extend" 42 | } 43 | }, 44 | "dependencies": { 45 | "dom-testing-library": "^3.11.0", 46 | "wait-for-expect": "^0.4.0" 47 | }, 48 | "devDependencies": { 49 | "@patrickhulce/lint": "^2.1.3", 50 | "@types/jest": "^23.1.1", 51 | "@types/puppeteer": "^1.10.0", 52 | "generate-export-aliases": "^1.1.0", 53 | "jest": "^23.1.0", 54 | "puppeteer": "^1.10.0", 55 | "rollup": "^0.61.1", 56 | "rollup-plugin-commonjs": "^9.1.3", 57 | "rollup-plugin-node-resolve": "^3.3.0", 58 | "ts-jest": "^22.4.6", 59 | "tslint": "^5.10.0", 60 | "typescript": "^2.9.2" 61 | }, 62 | "peerDependencies": { 63 | "puppeteer": "^1.5.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/extend.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as puppeteer from 'puppeteer' 3 | import '../lib/extend' 4 | 5 | describe('lib/extend.ts', () => { 6 | let browser: puppeteer.Browser 7 | let page: puppeteer.Page 8 | let document: puppeteer.ElementHandle 9 | 10 | it('should launch puppeteer', async () => { 11 | browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}) 12 | page = await browser.newPage() 13 | await page.goto(`file://${path.join(__dirname, 'fixtures/page.html')}`) 14 | }) 15 | 16 | it('should extend puppeteer ElementHandle', async () => { 17 | document = await page.getDocument() 18 | expect(typeof document.queryAllByAltText).toBe('function') 19 | }) 20 | 21 | it('should handle the query* methods', async () => { 22 | const element = await document.queryByText('Hello h1') 23 | expect(element).toBeTruthy() 24 | /* istanbul ignore next */ 25 | expect(await page.evaluate(el => el.textContent, element)).toEqual('Hello h1') 26 | }) 27 | 28 | it('should use the new v3 methods', async () => { 29 | const element = await document.queryByRole('presentation') 30 | expect(element).toBeTruthy() 31 | /* istanbul ignore next */ 32 | expect(await page.evaluate(el => el.textContent, element)).toContain('Layout table') 33 | }) 34 | 35 | it('should handle regex matching', async () => { 36 | const element = await document.queryByText(/HeLlO/i) 37 | expect(element).toBeTruthy() 38 | /* istanbul ignore next */ 39 | expect(await page.evaluate(el => el.textContent, element)).toEqual('Hello h1') 40 | }) 41 | 42 | it('should handle the get* methods', async () => { 43 | const element = await document.getByTestId('testid-text-input') 44 | /* istanbul ignore next */ 45 | expect(await page.evaluate(el => el.outerHTML, element)).toMatchSnapshot() 46 | }) 47 | 48 | it('should handle the get* method failures', async () => { 49 | // Use the scoped element so the pretty HTML snapshot is smaller 50 | const scope = await document.$('#scoped') 51 | 52 | try { 53 | await scope.getByTitle('missing') 54 | fail() 55 | } catch (err) { 56 | err.message = err.message.replace(/\(.*?:\d+:\d+/g, ':X:X') 57 | expect(err.message).toMatchSnapshot() 58 | } 59 | }) 60 | 61 | it('should handle the LabelText methods', async () => { 62 | const element = await document.getByLabelText('Label A') 63 | /* istanbul ignore next */ 64 | expect(await page.evaluate(el => el.outerHTML, element)).toMatchSnapshot() 65 | }) 66 | 67 | it('should handle the queryAll* methods', async () => { 68 | const elements = await document.queryAllByText(/Hello/) 69 | expect(elements).toHaveLength(3) 70 | 71 | const text = await Promise.all([ 72 | page.evaluate(el => el.textContent, elements[0]), 73 | page.evaluate(el => el.textContent, elements[1]), 74 | page.evaluate(el => el.textContent, elements[2]), 75 | ]) 76 | 77 | expect(text).toEqual(['Hello h1', 'Hello h2', 'Hello h3']) 78 | }) 79 | 80 | it('should scope results to element', async () => { 81 | const scope = await document.$('#scoped') 82 | const element = await scope.queryByText(/Hello/) 83 | /* istanbul ignore next */ 84 | expect(await page.evaluate(el => el.textContent, element)).toEqual('Hello h3') 85 | }) 86 | 87 | it('should get text content', async () => { 88 | const $h3 = await document.$('#scoped h3') 89 | expect(await $h3.getNodeText()).toEqual('Hello h3') 90 | }) 91 | 92 | it('should work with destructuring', async () => { 93 | const {queryByText} = (await document.$('#scoped')).getQueriesForElement() 94 | expect(await queryByText('Hello h1')).toBeFalsy() 95 | expect(await queryByText('Hello h3')).toBeTruthy() 96 | }) 97 | 98 | afterAll(async () => { 99 | await browser.close() 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /lib/typedefs.ts: -------------------------------------------------------------------------------- 1 | import {Matcher, MatcherOptions, SelectorMatcherOptions} from 'dom-testing-library/typings' // tslint:disable-line no-submodule-imports 2 | import {ElementHandle} from 'puppeteer' 3 | 4 | type Element = ElementHandle 5 | 6 | interface IQueryMethods { 7 | queryByPlaceholderText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 8 | queryAllByPlaceholderText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 9 | getByPlaceholderText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 10 | getAllByPlaceholderText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 11 | 12 | queryByText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 13 | queryAllByText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 14 | getByText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 15 | getAllByText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 16 | 17 | queryByLabelText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 18 | queryAllByLabelText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 19 | getByLabelText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 20 | getAllByLabelText(el: Element, m: Matcher, opts?: SelectorMatcherOptions): Promise 21 | 22 | queryByAltText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 23 | queryAllByAltText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 24 | getByAltText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 25 | getAllByAltText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 26 | 27 | queryByTestId(el: Element, m: Matcher, opts?: MatcherOptions): Promise 28 | queryAllByTestId(el: Element, m: Matcher, opts?: MatcherOptions): Promise 29 | getByTestId(el: Element, m: Matcher, opts?: MatcherOptions): Promise 30 | getAllByTestId(el: Element, m: Matcher, opts?: MatcherOptions): Promise 31 | 32 | queryByTitle(el: Element, m: Matcher, opts?: MatcherOptions): Promise 33 | queryAllByTitle(el: Element, m: Matcher, opts?: MatcherOptions): Promise 34 | getByTitle(el: Element, m: Matcher, opts?: MatcherOptions): Promise 35 | getAllByTitle(el: Element, m: Matcher, opts?: MatcherOptions): Promise 36 | 37 | queryByRole(el: Element, m: Matcher, opts?: MatcherOptions): Promise 38 | queryAllByRole(el: Element, m: Matcher, opts?: MatcherOptions): Promise 39 | getByRole(el: Element, m: Matcher, opts?: MatcherOptions): Promise 40 | getAllByRole(el: Element, m: Matcher, opts?: MatcherOptions): Promise 41 | 42 | queryBySelectText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 43 | queryAllBySelectText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 44 | getBySelectText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 45 | getAllBySelectText(el: Element, m: Matcher, opts?: MatcherOptions): Promise 46 | 47 | queryByValue(el: Element, m: Matcher, opts?: MatcherOptions): Promise 48 | queryAllByValue(el: Element, m: Matcher, opts?: MatcherOptions): Promise 49 | getByValue(el: Element, m: Matcher, opts?: MatcherOptions): Promise 50 | getAllByValue(el: Element, m: Matcher, opts?: MatcherOptions): Promise 51 | } 52 | 53 | type IScopedQueryMethods = { 54 | [K in keyof IQueryMethods]: (m: Matcher, opts?: MatcherOptions) => ReturnType 55 | } 56 | 57 | export interface IScopedQueryUtils extends IScopedQueryMethods { 58 | getQueriesForElement(): IQueryUtils & IScopedQueryUtils 59 | getNodeText(): Promise 60 | } 61 | 62 | export interface IQueryUtils extends IQueryMethods { 63 | getQueriesForElement(): IQueryUtils & IScopedQueryUtils 64 | getNodeText(el: Element): Promise 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pptr-testing-library 2 | 3 | [![NPM Package](https://badge.fury.io/js/pptr-testing-library.svg)](https://www.npmjs.com/package/pptr-testing-library) 4 | [![Build Status](https://travis-ci.org/patrickhulce/pptr-testing-library.svg?branch=master)](https://travis-ci.org/patrickhulce/pptr-testing-library) 5 | [![Coverage Status](https://coveralls.io/repos/github/patrickhulce/pptr-testing-library/badge.svg?branch=master)](https://coveralls.io/github/patrickhulce/pptr-testing-library?branch=master) 6 | [![Dependencies](https://david-dm.org/patrickhulce/pptr-testing-library.svg)](https://david-dm.org/patrickhulce/pptr-testing-library) 7 | 8 | [puppeteer](https://github.com/GoogleChrome/puppeteer) + [dom-testing-library](https://github.com/kentcdodds/dom-testing-library) = 💖 9 | 10 | All your favorite user-centric querying functions from react-testing-library/dom-testing-library available from Puppeteer! 11 | 12 | ## Install 13 | 14 | `npm install --save-dev pptr-testing-library` 15 | 16 | ## Use 17 | 18 | ```js 19 | const puppeteer = require('puppeteer') 20 | const {getDocument, queries, wait} = require('pptr-testing-library') 21 | 22 | const {getByTestId, getByLabelText} = queries 23 | 24 | const browser = await puppeteer.launch() 25 | const page = await browser.newPage() 26 | 27 | // Grab ElementHandle for document 28 | const $document = await getDocument(page) 29 | // Your favorite query methods are available 30 | const $form = await getByTestId($document, 'my-form') 31 | // returned elements are ElementHandles too! 32 | const $email = await getByLabelText($form, 'Email') 33 | // interact with puppeteer like usual 34 | await $email.type('pptr@example.com') 35 | // waiting works too! 36 | await wait(() => getByText('Loading...')) 37 | ``` 38 | 39 | A little too un-puppeteer for you? We've got prototype-mucking covered too :) 40 | 41 | ```js 42 | const puppeteer = require('puppeteer') 43 | require('pptr-testing-library/extend') 44 | 45 | const browser = await puppeteer.launch() 46 | const page = await browser.newPage() 47 | 48 | // getDocument is added to prototype of Page 49 | const $document = await page.getDocument() 50 | // query methods are added directly to prototype of ElementHandle 51 | const $form = await $document.getByTestId('my-form') 52 | // destructing works if you explicitly call getQueriesForElement 53 | const {getByText} = $form.getQueriesForElement() 54 | // ... 55 | ``` 56 | 57 | ## API 58 | 59 | Unique methods, not part of `dom-testing-library` 60 | 61 | - `getDocument(page: puppeteer.Page): ElementHandle` - get an ElementHandle for the document 62 | 63 | --- 64 | 65 | [dom-testing-libary API](https://github.com/kentcdodds/dom-testing-library#usage). All `get*`/`query*` methods are supported. 66 | 67 | - `getQueriesForElement(handle: ElementHandle): ElementHandle & QueryUtils` - extend the input object with the query API and return it 68 | - `wait(conditionFn: () => {}): Promise<{}>` - wait for the condition to not throw 69 | - `getNodeText(handle: ElementHandle): Promise` - get the text content of the element 70 | - `queries: QueryUtils` - the query subset of `dom-testing-library` exports 71 | - `queryByPlaceholderText` 72 | - `queryAllByPlaceholderText` 73 | - `getByPlaceholderText` 74 | - `getAllByPlaceholderText` 75 | - `queryByText` 76 | - `queryAllByText` 77 | - `getByText` 78 | - `getAllByText` 79 | - `queryByLabelText` 80 | - `queryAllByLabelText` 81 | - `getByLabelText` 82 | - `getAllByLabelText` 83 | - `queryByAltText` 84 | - `queryAllByAltText` 85 | - `getByAltText` 86 | - `getAllByAltText` 87 | - `queryByTestId` 88 | - `queryAllByTestId` 89 | - `getByTestId` 90 | - `getAllByTestId` 91 | - `queryByTitle` 92 | - `queryAllByTitle` 93 | - `getByTitle` 94 | - `getAllByTitle` 95 | 96 | ## Known Limitations 97 | 98 | - `waitForElement` method is not exposed. Puppeteer has its own set of wait utilities that somewhat conflict with the style used in `dom-testing-library`. See [#3](https://github.com/patrickhulce/pptr-testing-library/issues/3). 99 | - `fireEvent` method is not exposed, use puppeteer's built-ins instead. 100 | - `expect` assertion extensions are not available. 101 | 102 | ## Special Thanks 103 | 104 | [dom-testing-library](https://github.com/kentcdodds/dom-testing-library) of course! 105 | 106 | ## Related Puppeteer Test Utilities 107 | 108 | - [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) 109 | - Yours! Name TBD, PR welcome ;) 110 | 111 | ## LICENSE 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync} from 'fs' 2 | import * as path from 'path' 3 | import {ElementHandle, EvaluateFn, JSHandle, Page} from 'puppeteer' 4 | import waitForExpect from 'wait-for-expect' 5 | import {IQueryUtils, IScopedQueryUtils} from './typedefs' 6 | 7 | const domLibraryAsString = readFileSync( 8 | path.join(__dirname, '../dom-testing-library.js'), 9 | 'utf8', 10 | ).replace(/process.env/g, '{}') 11 | 12 | /* istanbul ignore next */ 13 | function mapArgument(argument: any, index: number): any { 14 | return index === 0 && typeof argument === 'object' && argument.regex 15 | ? new RegExp(argument.regex, argument.flags) 16 | : argument 17 | } 18 | 19 | const delegateFnBodyToExecuteInPage = ` 20 | ${domLibraryAsString}; 21 | 22 | const mappedArgs = args.map(${mapArgument.toString()}); 23 | return __dom_testing_library__[fnName](container, ...mappedArgs); 24 | ` 25 | 26 | type DOMReturnType = ElementHandle | ElementHandle[] | null 27 | 28 | type ContextFn = (...args: any[]) => ElementHandle 29 | 30 | async function createElementHandleArray(handle: JSHandle): Promise { 31 | const lengthHandle = await handle.getProperty('length') 32 | const length = await lengthHandle.jsonValue() 33 | 34 | const elements: ElementHandle[] = [] 35 | for (let i = 0; i < length; i++) { 36 | const jsElement = await handle.getProperty(i.toString()) 37 | const element = await createElementHandle(jsElement) 38 | if (element) elements.push(element) 39 | } 40 | 41 | return elements 42 | } 43 | 44 | async function createElementHandle(handle: JSHandle): Promise { 45 | const element = handle.asElement() 46 | if (element) return element 47 | await handle.dispose() 48 | return null // tslint:disable-line 49 | } 50 | 51 | async function covertToElementHandle(handle: JSHandle, asArray: boolean): Promise { 52 | return asArray ? createElementHandleArray(handle) : createElementHandle(handle) 53 | } 54 | 55 | function processNodeText(handles: IHandleSet): Promise { 56 | return handles.containerHandle 57 | .executionContext() 58 | .evaluate(handles.evaluateFn, handles.containerHandle, 'getNodeText') 59 | } 60 | 61 | async function processQuery(handles: IHandleSet): Promise { 62 | const {containerHandle, evaluateFn, fnName, argsToForward} = handles 63 | 64 | const handle = await containerHandle 65 | .executionContext() 66 | .evaluateHandle(evaluateFn, containerHandle, fnName, ...argsToForward) 67 | return covertToElementHandle(handle, fnName.includes('All')) 68 | } 69 | 70 | interface IHandleSet { 71 | containerHandle: ElementHandle 72 | evaluateFn: EvaluateFn 73 | fnName: string 74 | argsToForward: any[] 75 | } 76 | 77 | function createDelegateFor( 78 | fnName: keyof IQueryUtils, 79 | contextFn?: ContextFn, 80 | processHandleFn?: (handles: IHandleSet) => Promise, 81 | ): (...args: any[]) => Promise { 82 | // @ts-ignore 83 | processHandleFn = processHandleFn || processQuery 84 | 85 | const convertRegExp = (regex: RegExp) => ({regex: regex.source, flags: regex.flags}) 86 | 87 | return async function(...args: any[]): Promise { 88 | // @ts-ignore 89 | const containerHandle: ElementHandle = contextFn ? contextFn.apply(this, args) : this 90 | // @ts-ignore 91 | const evaluateFn: EvaluateFn = new Function( 92 | 'container, fnName, ...args', 93 | delegateFnBodyToExecuteInPage, 94 | ) 95 | 96 | // Convert RegExp to a special format since they don't serialize well 97 | let argsToForward = args.map(arg => (arg instanceof RegExp ? convertRegExp(arg) : arg)) 98 | // Remove the container from the argsToForward since it's always the first argument 99 | if (containerHandle === args[0]) { 100 | argsToForward = args.slice(1) 101 | } 102 | 103 | return processHandleFn!({fnName, containerHandle, evaluateFn, argsToForward}) 104 | } 105 | } 106 | 107 | export async function getDocument(_page?: Page): Promise { 108 | // @ts-ignore 109 | const page: Page = _page || this 110 | const documentHandle = await page.mainFrame().evaluateHandle('document') 111 | const document = documentHandle.asElement() 112 | if (!document) throw new Error('Could not find document') 113 | return document 114 | } 115 | 116 | export function wait( 117 | callback: () => any = () => undefined, 118 | {timeout = 4500, interval = 50} = {}, // tslint:disable-line 119 | ): Promise<{}> { 120 | return waitForExpect(callback, timeout, interval) 121 | } 122 | 123 | export function getQueriesForElement( 124 | object: T, 125 | contextFn?: ContextFn, 126 | ): T & IQueryUtils & IScopedQueryUtils { 127 | const o = object as any 128 | if (!contextFn) contextFn = () => o 129 | o.getQueriesForElement = () => getQueriesForElement(o, () => o) 130 | 131 | o.queryByPlaceholderText = createDelegateFor('queryByPlaceholderText', contextFn) 132 | o.queryAllByPlaceholderText = createDelegateFor('queryAllByPlaceholderText', contextFn) 133 | o.getByPlaceholderText = createDelegateFor('getByPlaceholderText', contextFn) 134 | o.getAllByPlaceholderText = createDelegateFor('getAllByPlaceholderText', contextFn) 135 | 136 | o.queryByText = createDelegateFor('queryByText', contextFn) 137 | o.queryAllByText = createDelegateFor('queryAllByText', contextFn) 138 | o.getByText = createDelegateFor('getByText', contextFn) 139 | o.getAllByText = createDelegateFor('getAllByText', contextFn) 140 | 141 | o.queryByLabelText = createDelegateFor('queryByLabelText', contextFn) 142 | o.queryAllByLabelText = createDelegateFor('queryAllByLabelText', contextFn) 143 | o.getByLabelText = createDelegateFor('getByLabelText', contextFn) 144 | o.getAllByLabelText = createDelegateFor('getAllByLabelText', contextFn) 145 | 146 | o.queryByAltText = createDelegateFor('queryByAltText', contextFn) 147 | o.queryAllByAltText = createDelegateFor('queryAllByAltText', contextFn) 148 | o.getByAltText = createDelegateFor('getByAltText', contextFn) 149 | o.getAllByAltText = createDelegateFor('getAllByAltText', contextFn) 150 | 151 | o.queryByTestId = createDelegateFor('queryByTestId', contextFn) 152 | o.queryAllByTestId = createDelegateFor('queryAllByTestId', contextFn) 153 | o.getByTestId = createDelegateFor('getByTestId', contextFn) 154 | o.getAllByTestId = createDelegateFor('getAllByTestId', contextFn) 155 | 156 | o.queryByTitle = createDelegateFor('queryByTitle', contextFn) 157 | o.queryAllByTitle = createDelegateFor('queryAllByTitle', contextFn) 158 | o.getByTitle = createDelegateFor('getByTitle', contextFn) 159 | o.getAllByTitle = createDelegateFor('getAllByTitle', contextFn) 160 | 161 | o.queryByRole = createDelegateFor('queryByRole', contextFn) 162 | o.queryAllByRole = createDelegateFor('queryAllByRole', contextFn) 163 | o.getByRole = createDelegateFor('getByRole', contextFn) 164 | o.getAllByRole = createDelegateFor('getAllByRole', contextFn) 165 | 166 | o.queryBySelectText = createDelegateFor('queryBySelectText', contextFn) 167 | o.queryAllBySelectText = createDelegateFor('queryAllBySelectText', contextFn) 168 | o.getBySelectText = createDelegateFor('getBySelectText', contextFn) 169 | o.getAllBySelectText = createDelegateFor('getAllBySelectText', contextFn) 170 | 171 | o.queryByValue = createDelegateFor('queryByValue', contextFn) 172 | o.queryAllByValue = createDelegateFor('queryAllByValue', contextFn) 173 | o.getByValue = createDelegateFor('getByValue', contextFn) 174 | o.getAllByValue = createDelegateFor('getAllByValue', contextFn) 175 | 176 | o.getNodeText = createDelegateFor('getNodeText', contextFn, processNodeText) 177 | 178 | return o 179 | } 180 | 181 | export const within = getQueriesForElement 182 | 183 | // @ts-ignore 184 | export const queries: IQueryUtils = {} 185 | getQueriesForElement(queries, el => el) 186 | --------------------------------------------------------------------------------