├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
Hello h3
14 |
15 |
16 |
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 | [](https://www.npmjs.com/package/pptr-testing-library)
4 | [](https://travis-ci.org/patrickhulce/pptr-testing-library)
5 | [](https://coveralls.io/github/patrickhulce/pptr-testing-library?branch=master)
6 | [](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 |
--------------------------------------------------------------------------------