├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── api ├── miscellaneous.js ├── retrieval.js └── waits.js ├── external └── serialization-utils.js ├── index.js ├── package-lock.json ├── package.json └── tests ├── server.js ├── setup.js ├── todomvc-react.zip └── todomvc.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | tests/todomvc-react/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - "node" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gideon Pyzer / Huddle 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppeteer-extensions 2 | [![Build Status](https://travis-ci.org/HuddleEng/puppeteer-extensions.svg?branch=master)](https://travis-ci.org/HuddleEng/puppeteer-extensions) 3 | 4 | This library exposes a number of convenience functions to extend Puppeteer's API, in order to make writing tests easier. 5 | The idea is that many of these functions (or similar ones) will eventually make their way into Puppeteer's own API, but 6 | this allows us to experiment with new ways of improving UI testing. 7 | 8 | ## Usage 9 | - `page` Puppeteer page instance 10 | - `timeout` [Optional] Timeout for waits in milliseconds (default: 5000 ms) 11 | 12 | ```javascript 13 | const extensions = require('puppeteer-extensions')(page); 14 | ``` 15 | 16 | ```javascript 17 | (async() { 18 | const listItem = '.todo-list li'; 19 | ... 20 | await extensions.waitForNthSelectorAttributeValue(listItem, 1, 'class', 'completed'); 21 | })(); 22 | 23 | ``` 24 | 25 | 26 | ## API 27 | The API is split into categories to better organise the extension functions. This currently includes: 28 | 29 | - [Waits](#waits) 30 | - [Retrieval](#retrieval) 31 | - [Miscellaneous](#miscellaneous) 32 | 33 | 34 | **resetRequests()** 35 | 36 | Resets the requests cache used by the `waits` API. This should be called when you are going to navigate to another page, 37 | in order to track the new requests correctly. 38 | 39 | ## Waits 40 | **waitForResource(resource, timeout=defaultTimeout)** 41 | - `resource` \ The URL of the resource (or a substring of it) 42 | - `timeout` \ Timeout for the check 43 | 44 | Wait for a resource request to be responded to 45 | 46 | 47 | **waitForLoadedWebFontCountToBe(count, timeout=defaultTimeout)** 48 | - `count` \ The number of web fonts to expect 49 | - `timeout` \ Timeout for the check 50 | 51 | Wait for a specific number of web fonts to be loaded and ready on the page 52 | 53 | 54 | **waitForFunction(fn, options, ...args)** 55 | - `fn` \ The function to execute on the page 56 | - `options` \ Optional waiting parameters 57 | - `args` \<...args> Arguments to be passed into the function 58 | 59 | Wait for function to execute on the page (see [waitForFunction](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforfunctionpagefunction-options-args)) 60 | 61 | 62 | **waitUntilExistsAndVisible(selector)** 63 | - `selector` \ The selector for the element on the page 64 | 65 | Wait until an element exists on the page and is visible (i.e. not transparent) (see [waitForSelector](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforselectorselector-options)) 66 | 67 | 68 | **waitWhileExistsAndVisible(selector)** 69 | - `selector` \ The selector for the element on the page 70 | 71 | Wait while an element still exists on the page and is visible (i.e. not transparent) (see [waitForSelector](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforselectorselector-options)) 72 | 73 | 74 | **waitUntilSelectorHasVisibleContent(selector)** 75 | - `selector` \ The selector for the element on the page 76 | 77 | Wait until the selector has visible content (i.e. the element takes up some width and height on the page) (i.e. not transparent) 78 | 79 | 80 | **waitWhileSelectorHasVisibleContent(selector)** 81 | - `selector` \ The selector for the element on the page 82 | 83 | Wait while the selector has visible content (i.e. the element takes up some width and height on the page) (i.e. not transparent) 84 | 85 | 86 | **waitForNthSelectorAttribute(selector, nth, attributeName)** 87 | - `selector` \ The selector for the element on the page 88 | - `nth` \ The nth element found by the selector 89 | - `attributeName` \ The attribute name to look for 90 | 91 | Wait for the nth element found from the selector has a particular attribute 92 | 93 | 94 | **waitForSelectorAttribute(selector, attributeName)** 95 | - `selector` \ The selector for the element on the page 96 | - `attributeName` \ The attribute name to look for 97 | 98 | Wait for the element found from the selector has a particular attribute 99 | 100 | 101 | **waitForNthSelectorAttributeValue(selector, nth, attributeName, attributeValue)** 102 | - `selector` \ The selector for the element on the page 103 | - `nth` \ The nth element found by the selector 104 | - `attributeName` \ The attribute name to look for 105 | - `attributeValue` \ The attribute value to match the attributeName 106 | 107 | Wait for the nth element found from the selector has a particular attribute value pair 108 | 109 | 110 | **waitForSelectorAttributeValue(selector, attributeName, attributeValue)** 111 | - `selector` \ The selector for the element on the page 112 | - `attributeName` \ The attribute name to look for 113 | - `attributeValue` \ The attribute value to match the attributeName 114 | 115 | Wait for the element found from the selector has a particular attribute value pair 116 | 117 | 118 | **waitForElementCount(selector, expectedCount)** 119 | - `selector` \ The selector for the element on the page 120 | - `expectedCount` \ The number of elements to expect 121 | 122 | Wait for the element count to be a particular value 123 | 124 | 125 | **waitForDocumentTitle(title)** 126 | - `title` \ The expected title of the document 127 | 128 | Wait for the document title to be a particular string 129 | 130 | 131 | **waitForUrl(regex)** 132 | - `regex` \ The regular expression to match the URL on 133 | 134 | Wait for the current window location to match a particular regular expression 135 | 136 | 137 | **waitFor(milliseconds)** 138 | - `milliseconds` \ The number of milliseconds to wait for 139 | 140 | Wait for a given number of milliseconds 141 | 142 | 143 | ## Retrieval 144 | 145 | **getValue(selector)** 146 | - `selector` \ The selector for the element to get the value for 147 | - **returns** \ The value property value for the element 148 | 149 | Get the value property value for a particular element 150 | 151 | 152 | **getText(selector)** 153 | - `selector` \ The selector for the element to get the text for 154 | - **returns** \ The text property value for the element 155 | 156 | Get the text property value for a particular element 157 | 158 | 159 | **getPropertyValue(selector, property)** 160 | - `selector` \ The selector for the element to get the property value for 161 | - `property` \ The property to look for 162 | - **returns** \ The property value for the element 163 | 164 | Get the value of a particular property for a particular element 165 | 166 | 167 | **isElementFocused(selector)** 168 | - `selector` \ The selector of the element to check for focus state 169 | - **returns** \ Whether the element is focused or not 170 | 171 | Check if element is focused 172 | 173 | ## Miscellaneous 174 | 175 | **turnOffAnimations()** 176 | 177 | Turn off CSS animations on the page to help avoid flaky visual comparisons 178 | 179 | 180 | **fastForwardTime(milliseconds)** 181 | - `milliseconds` \ The number of milliseconds to fast forward 182 | 183 | Fast forward the current time by a given number of milliseconds 184 | 185 | 186 | **evaluate(fn, ...args)** 187 | - `fn` \ The function to execute on the page 188 | - `args` \<...args> Arguments to be passed into the function 189 | 190 | Runs a function on the page -------------------------------------------------------------------------------- /api/miscellaneous.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This file represents the miscellaneous API. It exposes functions that don't fit under a particular category. 4 | * 5 | **/ 6 | 7 | const serializeFunctionWithArgs = require('../external/serialization-utils'); 8 | 9 | module.exports = puppeteerPage => ({ 10 | /** 11 | * Turn off CSS animations on the page to help avoid flaky visual comparisons 12 | */ 13 | async turnOffAnimations () { 14 | return puppeteerPage.evaluate(() => { 15 | function disableAnimations() { 16 | const {jQuery} = window; 17 | if (jQuery) { 18 | jQuery.fx.off = true; 19 | } 20 | 21 | const css = document.createElement('style'); 22 | css.type = 'text/css'; 23 | css.innerHTML = '* { -webkit-transition: none !important; transition: none !important; -webkit-animation: none !important; animation: none !important; }'; 24 | document.body.appendChild( css ); 25 | } 26 | 27 | if (document.readyState !== 'loading') { 28 | disableAnimations(); 29 | } else { 30 | window.addEventListener('load', disableAnimations, false); 31 | } 32 | }) 33 | }, 34 | /** 35 | * Fast forward the current time by a given number of milliseconds 36 | * @param {number} milliseconds - The number of milliseconds to fast forward 37 | */ 38 | async fastForwardTime(milliseconds) { 39 | return puppeteerPage.evaluate(milliseconds => { 40 | window.__oldDate = Date; 41 | 42 | function hackyDate() { 43 | return new window.__oldDate((new window.__oldDate()).getTime() + milliseconds); 44 | } 45 | 46 | hackyDate.now = () => { 47 | return hackyDate().getTime(); 48 | }; 49 | 50 | window.Date = hackyDate; 51 | }, milliseconds); 52 | }, 53 | /** 54 | * Run a function on the page 55 | * @param {function} fn - The function to execute on the page 56 | * @param {...args} args - Arguments to be passed into the function 57 | */ 58 | async evaluate(fn, ...args) { 59 | const fnStr = serializeFunctionWithArgs(fn, ...args); 60 | return puppeteerPage.evaluate(fnStr); 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /api/retrieval.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This file represents the retrieval API. It exposes convenience functions for getting properties/values/state from the UI. 4 | * 5 | **/ 6 | 7 | module.exports = puppeteerPage => ({ 8 | /** 9 | * Get the value property value for a particular element 10 | * @param {string} selector - The selector for the element to get the value for 11 | * @returns {string} value - The value property value for the element 12 | */ 13 | async getValue(selector) { 14 | return puppeteerPage.evaluate(selector => { 15 | return document.querySelector(selector).value; 16 | }, selector); 17 | }, 18 | /** 19 | * Get the text property value for a particular element 20 | * @param {string} selector - The selector for the element to get the text for 21 | * @returns {string} value - The text property value for the element 22 | */ 23 | async getText(selector) { 24 | return puppeteerPage.evaluate(selector => { 25 | return document.querySelector(selector).textContent; 26 | }, selector); 27 | }, 28 | /** 29 | * Get the value of a particular property for a particular element 30 | * @param {string} selector - The selector for the element to get the property value for 31 | * @param {string} property - The property to look for 32 | * @returns {string} value - The property value for the element 33 | */ 34 | async getPropertyValue(selector, property) { 35 | try { 36 | return puppeteerPage.evaluate((selector, property) => { 37 | const element = document.querySelector(selector); 38 | return element[property]; 39 | }, selector, property); 40 | } catch(e) { 41 | throw Error(`Unable able to get ${property} from ${selector}.`, e); 42 | } 43 | }, 44 | /** 45 | * Check if element is focused 46 | * @param {string} selector - The selector of the element to check for focus state 47 | * @returns {boolean} Whether the element is focused or not 48 | */ 49 | async isElementFocused (selector) { 50 | return puppeteerPage.evaluate(selector => { 51 | const element = document.querySelector(selector); 52 | return element === document.activeElement; 53 | }, selector); 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /api/waits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This file represents the waits API. It exposes useful polling functions for particular resources or selectors 4 | * 5 | **/ 6 | 7 | const serializeFunctionWithArgs = require('../external/serialization-utils'); 8 | 9 | const pollFor = ({checkFn, interval, timeout, timeoutMsg}) => { 10 | return new Promise((resolve, reject) => { 11 | const startTime = new Date().getTime(); 12 | const timer = setInterval(async () => { 13 | if ((new Date().getTime() - startTime) < timeout) { 14 | if (await checkFn()) { 15 | clearInterval(timer); 16 | resolve(); 17 | } 18 | } else { 19 | clearInterval(timer); 20 | reject(timeoutMsg); 21 | } 22 | }, interval); 23 | }); 24 | }; 25 | 26 | const isSuccessfulResponse = request => { 27 | const response = request.response && request.response(); 28 | return response && (response.status() === 200 || response.status() === 304); 29 | }; 30 | 31 | module.exports = (puppeteerPage, requests, defaultTimeout) => ({ 32 | /** 33 | * Wait for a resource request to be responded to 34 | * @param {string} resource - The URL of the resource (or a substring of it) 35 | * @param {number] [timeout=defaultTimeout] - Timeout for the check 36 | */ 37 | waitForResource (resource, timeout = defaultTimeout) { 38 | return new Promise((resolve, reject) => { 39 | 40 | const resourceRequestHasResponded = () => { 41 | const resourceRequest = requests && requests.find(r => r.url && r.url().indexOf(resource) !== -1); 42 | return isSuccessfulResponse(resourceRequest); 43 | }; 44 | 45 | if (resourceRequestHasResponded()) { 46 | resolve(); 47 | } else { 48 | pollFor({ 49 | checkFn: () => { 50 | return resourceRequestHasResponded(); 51 | }, 52 | internal: 100, 53 | timeout: timeout, 54 | timeoutMsg: 'Timeout waiting for resource match.' 55 | }).then(resolve).catch(reject) 56 | } 57 | }); 58 | }, 59 | /** 60 | * Wait for a specific number of web fonts to be loaded and ready on the page 61 | * @param {number} count - The number of web fonts to expect 62 | * @param {number] [timeout=defaultTimeout] - Timeout for the check 63 | */ 64 | async waitForLoadedWebFontCountToBe(count, timeout = defaultTimeout) { 65 | let hasInjectedWebFontsAllLoadedFunction = false; 66 | 67 | async function checkWebFontIsLoaded() { 68 | const fontResponses = requests.filter(r => { 69 | if (r.resourceType() === 'font') { 70 | return isSuccessfulResponse(r); 71 | } 72 | 73 | return false; 74 | }); 75 | 76 | if (fontResponses.length === count) { 77 | if (hasInjectedWebFontsAllLoadedFunction) { 78 | return puppeteerPage.evaluate(() => { 79 | return !!window.__webFontsAllLoaded; 80 | }); 81 | } else { 82 | await puppeteerPage.evaluate(() => { 83 | (async function() { 84 | window.__webFontsAllLoaded = await document.fonts.ready; 85 | })(); 86 | }); 87 | 88 | hasInjectedWebFontsAllLoadedFunction = true; 89 | return false; 90 | } 91 | } 92 | return false; 93 | } 94 | 95 | return pollFor({ 96 | checkFn: checkWebFontIsLoaded, 97 | internal: 100, 98 | timeout: timeout, 99 | timeoutMsg: `Timeout waiting for ${count} web font responses` 100 | }); 101 | }, 102 | /** 103 | * Wait for function to execute on the page 104 | * @param {function} fn - The function to execute on the page 105 | * @param {object} options - Optional waiting parameters 106 | * @param {...args} args - Arguments to be passed into the function 107 | * @see https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforfunctionpagefunction-options-args 108 | */ 109 | async waitForFunction(fn, options, ...args) { 110 | const fnStr = serializeFunctionWithArgs(fn, ...args); 111 | return puppeteerPage.waitForFunction(fnStr, options); 112 | }, 113 | /** 114 | * Wait until an element exists on the page and is visible (i.e. not transparent) 115 | * @param {string} selector - The selector for the element on the page 116 | * @see https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforselectorselector-options 117 | */ 118 | async waitUntilExistsAndVisible(selector) { 119 | return puppeteerPage.waitForSelector(selector, { visible: true }); 120 | }, 121 | /** 122 | * Wait while an element still exists on the page and is visible (i.e. not transparent) 123 | * @param {string} selector - The selector for the element on the page 124 | * @see https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforselectorselector-options 125 | */ 126 | async waitWhileExistsAndVisible(selector) { 127 | return puppeteerPage.waitForSelector(selector, { hidden: true }); 128 | }, 129 | /** 130 | * Wait until the selector has visible content (i.e. the element takes up some width and height on the page) 131 | * @param {string} selector - The selector for the element on the page 132 | */ 133 | async waitUntilSelectorHasVisibleContent(selector) { 134 | return puppeteerPage.waitForFunction(selector => { 135 | const elem = document.querySelector(selector); 136 | const isVisible = elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length; 137 | return !!isVisible; 138 | }, {timeout: defaultTimeout}, selector); 139 | }, 140 | /** 141 | * Wait while the selector has visible content (i.e. the element takes up some width and height on the page) 142 | * @param {string} selector - The selector for the element on the page 143 | */ 144 | async waitWhileSelectorHasVisibleContent(selector) { 145 | return puppeteerPage.waitForFunction(selector => { 146 | const elem = document.querySelector(selector); 147 | const isVisible = elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length; 148 | return !isVisible; 149 | }, {timeout: defaultTimeout}, selector); 150 | }, 151 | /** 152 | * Wait for the nth element found from the selector has a particular attribute 153 | * @param {string} selector - The selector for the element on the page 154 | * @param {number} nth - The nth element found by the selector 155 | * @param {string} attributeName - The attribute name to look for 156 | */ 157 | async waitForNthSelectorAttribute(selector, nth, attributeName) { 158 | return puppeteerPage.waitForFunction((selector, nth, attributeName) => { 159 | const element = document.querySelectorAll(selector)[nth - 1]; 160 | return typeof element.attributes[attributeName] !== 'undefined'; 161 | }, {timeout: defaultTimeout}, selector, nth, attributeName); 162 | }, 163 | /** 164 | * Wait for the element found from the selector has a particular attribute 165 | * @param {string} selector - The selector for the element on the page 166 | * @param {string} attributeName - The attribute name to look for 167 | */ 168 | async waitForSelectorAttribute (selector, attributeName) { 169 | return this.waitForNthSelectorAttribute(selector, 1, attributeName); 170 | }, 171 | /** 172 | * Wait for the nth element found from the selector has a particular attribute value pair 173 | * @param {string} selector - The selector for the element on the page 174 | * @param {number} nth - The nth element found by the selector 175 | * @param {string} attributeName - The attribute name to look for 176 | * @param {string} attributeValue - The attribute value to match the attributeName 177 | */ 178 | async waitForNthSelectorAttributeValue (selector, nth, attributeName, attributeValue) { 179 | return puppeteerPage.waitForFunction((selector, nth, attributeName, attributeValue) => { 180 | const element = document.querySelectorAll(selector)[nth - 1]; 181 | return element.attributes[attributeName] && element.attributes[attributeName].value === attributeValue; 182 | }, {timeout: defaultTimeout}, selector, nth, attributeName, attributeValue); 183 | }, 184 | /** 185 | * Wait for the element found from the selector has a particular attribute value pair 186 | * @param {string} selector - The selector for the element on the page 187 | * @param {string} attributeName - The attribute name to look for 188 | * @param {string} attributeValue - The attribute value to match the attributeName 189 | */ 190 | async waitForSelectorAttributeValue (selector, attributeName, attributeValue) { 191 | return this.waitForNthSelectorAttributeValue(selector, 1, attributeName, attributeValue); 192 | }, 193 | /** 194 | * Wait for the element count to be a particular value 195 | * @param {string} selector - The selector for the element on the page 196 | * @param {number} expectedCount - The number of elements to expect 197 | */ 198 | async waitForElementCount(selector, expectedCount) { 199 | return puppeteerPage.waitForFunction((selector, expectedCount) => { 200 | return document.querySelectorAll(selector).length === expectedCount; 201 | }, { timeout: defaultTimeout}, selector, expectedCount); 202 | }, 203 | /** 204 | * Wait for the document title to be a particular string 205 | * @param {string} title - The expected title of the document 206 | */ 207 | async waitForDocumentTitle(title) { 208 | return puppeteerPage.waitForFunction(title => { 209 | const actualTitle = document.title; 210 | return actualTitle === title; 211 | }, {timeout: defaultTimeout}, title); 212 | }, 213 | /** 214 | * Wait for the current window location to match a particular regular expression 215 | * @param {RegExp} regex - The regular expression to match the URL on 216 | */ 217 | async waitForUrl(regex) { 218 | return this.waitForFunction(regex => { 219 | return regex.test(window.location.href); 220 | }, { timeout: defaultTimeout}, regex); 221 | }, 222 | /** 223 | * Wait for a given number of milliseconds 224 | * @param {number} milliseconds - The number of milliseconds to wait for 225 | */ 226 | async waitFor(milliseconds) { 227 | return new Promise(resolve => { 228 | setTimeout(() => { 229 | resolve(); 230 | }, milliseconds); 231 | }); 232 | }, 233 | }); -------------------------------------------------------------------------------- /external/serialization-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This file contains functions taken, and sometimes modified, from the PhantomJS repository, under BSD-3-Clause licence 4 | * https://github.com/ariya/phantomjs/blob/master/LICENSE.BSD 5 | * 6 | * The following copyright notice(s) apply: 7 | * 8 | * Copyright (C) 2011 Ariya Hidayat 9 | * Copyright (C) 2011 Ivan De Marino 10 | * Copyright (C) 2011 James Roe 11 | * Copyright (C) 2011 execjosh, http://execjosh.blogspot.com 12 | * Copyright (C) 2012 James M. Greene 13 | **/ 14 | 15 | // Source: https://github.com/ariya/phantomjs/blob/master/src/modules/webpage.js#L205 16 | const detectType = value => { 17 | let s = typeof value; 18 | if (s === 'object') { 19 | if (value) { 20 | if (value instanceof Array) { 21 | s = 'array'; 22 | } else if (value instanceof RegExp) { 23 | s = 'regexp'; 24 | } else if (value instanceof Date) { 25 | s = 'date'; 26 | } 27 | } else { 28 | s = 'null'; 29 | } 30 | } 31 | return s; 32 | }; 33 | 34 | // Source: https://github.com/ariya/phantomjs/blob/master/src/modules/webpage.js#L167 35 | const quoteString = str => { 36 | let c, i, l = str.length, o = '"'; 37 | for (i = 0; i < l; i += 1) { 38 | c = str.charAt(i); 39 | if (c >= ' ') { 40 | if (c === '\\' || c === '"') { 41 | o += '\\'; 42 | } 43 | o += c; 44 | } else { 45 | switch (c) { 46 | case '\b': 47 | o += '\\b'; 48 | break; 49 | case '\f': 50 | o += '\\f'; 51 | break; 52 | case '\n': 53 | o += '\\n'; 54 | break; 55 | case '\r': 56 | o += '\\r'; 57 | break; 58 | case '\t': 59 | o += '\\t'; 60 | break; 61 | default: 62 | c = c.charCodeAt(); 63 | o += '\\u00' + Math.floor(c / 16).toString(16) + 64 | (c % 16).toString(16); 65 | } 66 | } 67 | } 68 | return o + '"'; 69 | }; 70 | 71 | // Source: from https://github.com/ariya/phantomjs/blob/master/src/modules/webpage.js#L354-L388 72 | module.exports = function serializeFunctionWithArgs(fn, ...args) { 73 | if (!(fn instanceof Function || typeof fn === 'string' || fn instanceof String)) { 74 | throw Error('Wrong use of evaluate'); 75 | } 76 | 77 | let str = '(function() { return (' + fn.toString() + ')('; 78 | 79 | args.forEach(arg => { 80 | let argType = detectType(arg); 81 | 82 | switch (argType) { 83 | case 'object': //< for type "object" 84 | case 'array': //< for type "array" 85 | str += JSON.stringify(arg) + ','; 86 | break; 87 | case 'date': //< for type "date" 88 | str += 'new Date(' + JSON.stringify(arg) + '),'; 89 | break; 90 | case 'string': //< for type "string" 91 | str += quoteString(arg) + ','; 92 | break; 93 | default: // for types: "null", "number", "function", "regexp", "undefined" 94 | str += arg + ','; 95 | break; 96 | } 97 | }); 98 | 99 | return str.replace(/,$/, '') + '); })()'; 100 | }; 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const waits = require('./api/waits'); 2 | const retrieval = require('./api/retrieval'); 3 | const miscellaneous = require('./api/miscellaneous'); 4 | const DEFAULT_TIMEOUT_MS = 5000; 5 | let resourceRequests = []; 6 | 7 | module.exports = (puppeteerInstance, timeout = DEFAULT_TIMEOUT_MS) => { 8 | puppeteerInstance.on('request', request => { 9 | resourceRequests.push(request); 10 | }); 11 | 12 | const resetRequests = () => { 13 | resourceRequests = []; 14 | }; 15 | 16 | return Object.assign({resetRequests}, 17 | waits(puppeteerInstance, resourceRequests, timeout), 18 | retrieval(puppeteerInstance), 19 | miscellaneous(puppeteerInstance), 20 | ); 21 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-extensions", 3 | "version": "1.0.3", 4 | "description": "Convenience functions for the Puppeteer", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest /tests", 8 | "postinstall": "node tests/setup.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/HuddleEng/puppeteer-extensions.git" 13 | }, 14 | "author": "Gideon Pyzer / Huddle", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/HuddleEng/puppeteer-extensions/issues" 18 | }, 19 | "homepage": "https://github.com/HuddleEng/puppeteer-extensions#readme", 20 | "devDependencies": { 21 | "colors": "^1.1.2", 22 | "express": "^4.16.2", 23 | "extract-zip": "^1.6.6", 24 | "jest": "^26.6.3", 25 | "puppeteer": "^1.15.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | 4 | const app = express(); 5 | 6 | app.use(express.static(path.join(__dirname, 'todomvc-react'))); 7 | 8 | app.get('/', (req, res) => { 9 | res.sendFile('index.html'); 10 | }); 11 | 12 | module.exports = { 13 | start: (port) => { 14 | return new Promise((resolve) => { 15 | const server = app.listen(port, () => { 16 | console.log(`Test server started on port ${port}`); 17 | resolve(server); 18 | }); 19 | }); 20 | }, 21 | stop: server => { 22 | server.close(); 23 | console.log('Test server stopped'); 24 | } 25 | }; -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | const {promisify} = require('util'); 2 | const extract = promisify(require('extract-zip')); 3 | const path = require('path'); 4 | const colors = require('colors'); 5 | const file = 'todomvc-react.zip'; 6 | const source = path.join(__dirname, file); 7 | const CONSOLE_PREFIX = 'Puppeteer Extensions: '; 8 | 9 | (async() => { 10 | console.log(`${CONSOLE_PREFIX} Running post-setup script...`.green); 11 | const extractErrors = await extract(source, { dir: __dirname }); 12 | 13 | if (!extractErrors) { 14 | console.log(`${CONSOLE_PREFIX} Post-setup script complete`.green); 15 | } else { 16 | throw Error(`${CONSOLE_PREFIX} Unable to extract ${file}`.red); 17 | } 18 | })(); -------------------------------------------------------------------------------- /tests/todomvc-react.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuddleEng/puppeteer-extensions/e5dff7c160166536b1a7db10de6722792dfa4d3e/tests/todomvc-react.zip -------------------------------------------------------------------------------- /tests/todomvc.test.js: -------------------------------------------------------------------------------- 1 | const input = "header input"; 2 | const listItem = ".todo-list li"; 3 | const firstItem = listItem + ":nth-of-type(1)"; 4 | const firstItemInput = firstItem + " > input"; 5 | const firstItemToggle = firstItem + " .toggle"; 6 | const firstItemRemoveButton = firstItem + " button"; 7 | const secondItem = listItem + ":nth-of-type(2)"; 8 | const secondItemInput = secondItem + " > input"; 9 | const todoCount = ".todo-count"; 10 | 11 | const puppeteer = require("puppeteer"); 12 | const server = require("./server"); 13 | 14 | let serverInstance; 15 | let browser; 16 | let extensions; 17 | let page; 18 | 19 | // Notice how you have to add all this boilerplate to every test file. This is what Muppeteer solves. 20 | beforeAll(async () => { 21 | serverInstance = await server.start(3000); 22 | browser = await puppeteer.launch(); 23 | page = await browser.newPage(); 24 | extensions = require("../index")(page); 25 | await page.goto("http://localhost:3000"); 26 | }); 27 | 28 | afterAll(async () => { 29 | await browser.close(); 30 | server.stop(serverInstance); 31 | }); 32 | 33 | // Note this test is not super realistic but just a way to test all API functions 34 | describe("Add a todo item", () => { 35 | it("input field is focused", async () => { 36 | await page.waitForSelector(input); 37 | await page.focus(input); 38 | expect(await extensions.isElementFocused(input)).toBe(true); 39 | }); 40 | it("typing text and hitting enter key adds new item", async () => { 41 | await page.waitForSelector(input); 42 | await page.type(input, "My first item"); 43 | await page.keyboard.press("Enter"); 44 | await page.waitForSelector(firstItem); 45 | expect(await extensions.getText(firstItem)).toBe("My first item"); 46 | expect(await extensions.getValue(firstItemInput)).toBe("My first item"); 47 | }); 48 | it("clicking checkbox marks item as complete", async () => { 49 | await page.waitForSelector(firstItemToggle); 50 | await page.click(firstItemToggle); 51 | await extensions.waitForNthSelectorAttributeValue( 52 | listItem, 53 | 1, 54 | "class", 55 | "completed" 56 | ); 57 | expect(await extensions.getPropertyValue(firstItem, "className")).toBe( 58 | "completed" 59 | ); 60 | }); 61 | it("typing more text and hitting enter adds a second item", async () => { 62 | await page.type(input, "My second item"); 63 | await page.keyboard.press("Enter"); 64 | await page.waitForSelector(secondItem); 65 | expect(await extensions.getText(secondItem)).toBe("My second item"); 66 | expect(await extensions.getValue(secondItemInput)).toBe("My second item"); 67 | }); 68 | it("hovering over first item shows x button", async () => { 69 | await page.hover(firstItem); 70 | }); 71 | it("clicking on first item x button removes it from the list", async () => { 72 | await page.click(firstItemRemoveButton); 73 | await extensions.waitForElementCount(listItem, 1); 74 | expect(await extensions.getText(todoCount)).toBe("1 item left"); 75 | }); 76 | }); 77 | --------------------------------------------------------------------------------