├── .eslintignore ├── .travis.yml ├── test ├── .eslintrc ├── page-model.js ├── test.js └── package.json ├── .gitignore ├── src ├── .babelrc └── index.js ├── .editorconfig ├── Gulpfile.js ├── LICENSE ├── package.json ├── README.md ├── .eslintrc └── lib └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | test/*.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/thumbnails 3 | test/test-screen.png 4 | *.iml 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /test/page-model.js: -------------------------------------------------------------------------------- 1 | import {Selector} from 'testcafe'; 2 | 3 | export default class Page { 4 | constructor() { 5 | this.Header = Selector('div > header > h1'); 6 | } 7 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import Page from './page-model'; 2 | 3 | fixture `e2e tests` 4 | .page `http://www.dobosz.at` 5 | ; 6 | 7 | const page = new Page(); 8 | 9 | test('it should render header element', async t => { 10 | await t 11 | .expect(page.Header.exists).ok(); 12 | }); -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "compact": false, 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "forceAllTransforms": true, 8 | "targets": { 9 | "node": true, 10 | }, 11 | }, 12 | ] 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-transform-runtime", 16 | "add-module-exports" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [{.eslintrc,*.json,.travis.yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-tests", 3 | "version": "1.5.0", 4 | "description": "e2e tests", 5 | "main": "test.js", 6 | "scripts": { 7 | "test": "npm install && testcafe \"puppeteer:no_sandbox:emulate=Galaxy S5\" test.js -s ./" 8 | }, 9 | "author": "jacek@dobosz.at", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "testcafe": "^0.20.2 || ^1.0.0", 13 | "testcafe-browser-provider-puppeteer": "../" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var babel = require('gulp-babel'); 3 | var mocha = require('gulp-mocha'); 4 | var del = require('del'); 5 | var nodeVersion = require('node-version'); 6 | 7 | 8 | var clean = function () { 9 | return del('lib'); 10 | }; 11 | 12 | var lint = function () { 13 | // TODO: eslint supports node version 4 or higher. 14 | // Remove this condition once we get rid of node 0.10 support. 15 | if (nodeVersion.major === '0') 16 | return null; 17 | 18 | var eslint = require('gulp-eslint'); 19 | 20 | return gulp 21 | .src([ 22 | 'src/**/*.js', 23 | 'test/**/*.js', 24 | 'Gulpfile.js' 25 | ]) 26 | .pipe(eslint()) 27 | .pipe(eslint.format()) 28 | .pipe(eslint.failAfterError()); 29 | }; 30 | 31 | var build = function () { 32 | return gulp 33 | .src('src/**/*.js') 34 | .pipe(babel()) 35 | .pipe(gulp.dest('lib')); 36 | }; 37 | 38 | gulp.task('clean', clean); 39 | gulp.task('lint', lint); 40 | gulp.task('build', gulp.series(lint, clean, build)); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 jdobosz 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testcafe-browser-provider-puppeteer", 3 | "version": "1.5.2", 4 | "description": "puppeteer TestCafe browser provider plugin.", 5 | "repository": "https://github.com/jdobosz/testcafe-browser-provider-puppeteer", 6 | "homepage": "https://github.com/jdobosz/testcafe-browser-provider-puppeteer", 7 | "author": { 8 | "name": "jdobosz", 9 | "email": "jacek@dobosz.at" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Lukasz Szmit", 14 | "email": "lukasz.szmit@workday.com" 15 | } 16 | ], 17 | "main": "lib/index", 18 | "files": [ 19 | "lib" 20 | ], 21 | "scripts": { 22 | "test": "cd test && npm test", 23 | "build": "gulp build" 24 | }, 25 | "keywords": [ 26 | "testcafe", 27 | "browser provider", 28 | "plugin" 29 | ], 30 | "license": "MIT", 31 | "dependencies": { 32 | "@babel/runtime": "^7.0.0", 33 | "puppeteer": "^1.20.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "^7.0.0", 37 | "@babel/core": "^7.0.0", 38 | "@babel/plugin-transform-runtime": "^7.0.0", 39 | "@babel/preset-env": "^7.0.0", 40 | "babel-eslint": "^10.0.0", 41 | "babel-plugin-add-module-exports": "^1.0.0", 42 | "del": "^3.0.0", 43 | "gulp": "^4.0.0", 44 | "gulp-babel": "^8.0.0", 45 | "gulp-eslint": "^5.0.0", 46 | "gulp-mocha": "^6.0.0", 47 | "node-version": "^1.1.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testcafe-browser-provider-puppeteer 2 | [![Build Status](https://travis-ci.org/jdobosz/testcafe-browser-provider-puppeteer.svg)](https://travis-ci.org/jdobosz/testcafe-browser-provider-puppeteer) 3 | [![npm version](https://badge.fury.io/js/testcafe-browser-provider-puppeteer.svg)](https://badge.fury.io/js/testcafe-browser-provider-puppeteer) 4 | 5 | This is the [puppeteer](https://github.com/GoogleChrome/puppeteer)/chromium browser provider plugin for [TestCafe](http://devexpress.github.io/testcafe). 6 | It allows to run tastcafe e2e tests headless in CI pipeline without any external dependency like xvfb, since everything what is needed is installed via npm. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm install --save-dev testcafe-browser-provider-puppeteer 12 | ``` 13 | 14 | ## Usage 15 | 16 | 17 | When you run tests from the command line, use the provider name when specifying browsers: 18 | 19 | ``` 20 | testcafe puppeteer 'path/to/test/file.js' 21 | ``` 22 | 23 | 24 | When you use API, pass the provider name to the `browsers()` method: 25 | 26 | ```js 27 | testCafe 28 | .createRunner() 29 | .src('path/to/test/file.js') 30 | .browsers('puppeteer') 31 | .run(); 32 | ``` 33 | 34 | ## Device Emulation 35 | 36 | If you want to emulate another device you can run `pupeteer:emulate=`. The supported devices are listed in the [Puppeteer DeviceDescriptors](https://github.com/puppeteer/puppeteer/blob/master/lib/DeviceDescriptors.js). 37 | 38 | ## Troubleshooting 39 | 40 | On same older linux distributions, fails chromium due to sandbox issues - see [this](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-fails-due-to-sandbox-issues). 41 | 42 | You can try in such case running the plugin without sandbox restriction 43 | 44 | ``` 45 | testcafe puppeteer:no_sandbox 'path/to/test/file.js' 46 | 47 | ``` 48 | 49 | In order to speedup CI you can provide custom executable of chromium browser instead to download it all the time: 50 | 51 | ``` 52 | runner 53 | .browsers(['puppeteer:no_sandbox?/usr/bin/chromium-browser']) 54 | 55 | 56 | runner 57 | .browsers(['puppeteer:?/usr/bin/chromium-browser']) 58 | 59 | ``` 60 | 61 | ## Author 62 | Jacek Dobosz 63 | 64 | ## Contributors 65 | Lukasz Szmit 66 | 67 | Pedro Scaff 68 | 69 | Bhavdeep Dhanjal 70 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | export default { 4 | // Multiple browsers support 5 | isMultiBrowser: true, 6 | 7 | browser: null, 8 | 9 | openedPages: {}, 10 | 11 | // Required - must be implemented 12 | // Browser control 13 | async openBrowser (id, pageUrl, browserName) { 14 | const browserArgs = browserName.split(':'); 15 | if (!this.browser) { 16 | const launchArgs = { 17 | timeout: 10000 18 | }; 19 | 20 | const noSandboxArgs = ['--no-sandbox', '--disable-setuid-sandbox']; 21 | 22 | if (browserArgs.indexOf('no_sandbox') !== -1) launchArgs.args = noSandboxArgs; 23 | else if (browserName.indexOf('?') !== -1) { 24 | const userArgs = browserName.split('?'); 25 | const params = userArgs[0]; 26 | 27 | if (params === 'no_sandbox') launchArgs.args = noSandboxArgs; 28 | 29 | const executablePath = userArgs[1]; 30 | 31 | if (executablePath.length > 0) 32 | launchArgs.executablePath = executablePath; 33 | } 34 | this.browser = await puppeteer.launch(launchArgs); 35 | } 36 | 37 | const page = await this.browser.newPage(); 38 | 39 | const emulationArg = browserArgs.find(v => /^emulate/.test(v)); 40 | 41 | if (Boolean(emulationArg)) { 42 | const [, emulationDevice] = emulationArg.split('='); 43 | const device = puppeteer.devices[emulationDevice]; 44 | 45 | if (!device) throw new Error('Emulation device is not supported'); 46 | 47 | await page.emulate(device); 48 | } 49 | 50 | await page.goto(pageUrl); 51 | this.openedPages[id] = page; 52 | }, 53 | 54 | async closeBrowser (id) { 55 | delete this.openedPages[id]; 56 | await this.browser.close(); 57 | }, 58 | 59 | async isValidBrowserName () { 60 | return true; 61 | }, 62 | 63 | // Extra methods 64 | async resizeWindow (id, width, height) { 65 | await this.openedPages[id].setViewport({ width, height }); 66 | }, 67 | 68 | async takeScreenshot (id, screenshotPath) { 69 | await this.openedPages[id].screenshot({ path: screenshotPath }); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "rules": { 5 | "no-alert": "error", 6 | "no-array-constructor": "error", 7 | "no-caller": "error", 8 | "no-catch-shadow": "error", 9 | "no-console": "error", 10 | "no-eval": "error", 11 | "no-extend-native": "error", 12 | "no-extra-bind": "error", 13 | "no-implied-eval": "error", 14 | "no-iterator": "error", 15 | "no-label-var": "error", 16 | "no-labels": "error", 17 | "no-lone-blocks": "error", 18 | "no-loop-func": "error", 19 | "no-multi-str": "error", 20 | "no-native-reassign": "error", 21 | "no-new": "error", 22 | "no-new-func": "error", 23 | "no-new-object": "error", 24 | "no-new-wrappers": "error", 25 | "no-octal-escape": "error", 26 | "no-proto": "error", 27 | "no-return-assign": "error", 28 | "no-script-url": "error", 29 | "no-sequences": "error", 30 | "no-shadow": "error", 31 | "no-shadow-restricted-names": "error", 32 | "no-spaced-func": "error", 33 | "no-undef-init": "error", 34 | "no-unused-expressions": "error", 35 | "no-with": "error", 36 | "camelcase": "error", 37 | "comma-spacing": "error", 38 | "consistent-return": "error", 39 | "eqeqeq": "error", 40 | "semi": "error", 41 | "semi-spacing": ["error", {"before": false, "after": true}], 42 | "space-infix-ops": "error", 43 | "space-unary-ops": ["error", { "words": true, "nonwords": false }], 44 | "yoda": ["error", "never"], 45 | "brace-style": ["error", "stroustrup", { "allowSingleLine": false }], 46 | "eol-last": "error", 47 | "indent": ["error", 4, { "SwitchCase": 1 }], 48 | "key-spacing": ["error", { "align": "value" }], 49 | "max-nested-callbacks": ["error", 3], 50 | "new-parens": "error", 51 | "newline-after-var": ["error", "always"], 52 | "no-lonely-if": "error", 53 | "no-multiple-empty-lines": ["error", { "max": 2 }], 54 | "no-nested-ternary": "error", 55 | "no-underscore-dangle": "off", 56 | "no-unneeded-ternary": "error", 57 | "object-curly-spacing": ["error", "always"], 58 | "operator-assignment": ["error", "always"], 59 | "quotes": ["error", "single", "avoid-escape"], 60 | "keyword-spacing" : "error", 61 | "space-before-blocks": ["error", "always"], 62 | "prefer-const": "error", 63 | "no-path-concat": "error", 64 | "no-undefined": "error", 65 | "strict": "off", 66 | "curly": ["error", "multi-or-nest"], 67 | "dot-notation": "off", 68 | "no-else-return": "error", 69 | "one-var": ["error", "never"], 70 | "no-multi-spaces": ["error", { 71 | "exceptions": { 72 | "VariableDeclarator": true, 73 | "AssignmentExpression": true 74 | } 75 | }], 76 | "radix": "error", 77 | "no-extra-parens": "error", 78 | "new-cap": ["error", { "capIsNew": false }], 79 | "space-before-function-paren": ["error", "always"], 80 | "no-use-before-define" : ["error", "nofunc"], 81 | "handle-callback-err": "off" 82 | }, 83 | "env": { 84 | "node": true 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = void 0; 9 | 10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 11 | 12 | var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); 13 | 14 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 15 | 16 | var _puppeteer = _interopRequireDefault(require("puppeteer")); 17 | 18 | var _default = { 19 | // Multiple browsers support 20 | isMultiBrowser: true, 21 | browser: null, 22 | openedPages: {}, 23 | // Required - must be implemented 24 | // Browser control 25 | openBrowser: function () { 26 | var _openBrowser = (0, _asyncToGenerator2.default)( 27 | /*#__PURE__*/ 28 | _regenerator.default.mark(function _callee(id, pageUrl, browserName) { 29 | var browserArgs, launchArgs, noSandboxArgs, userArgs, params, executablePath, page, emulationArg, _emulationArg$split, _emulationArg$split2, emulationDevice, device; 30 | 31 | return _regenerator.default.wrap(function _callee$(_context) { 32 | while (1) { 33 | switch (_context.prev = _context.next) { 34 | case 0: 35 | browserArgs = browserName.split(':'); 36 | 37 | if (this.browser) { 38 | _context.next = 8; 39 | break; 40 | } 41 | 42 | launchArgs = { 43 | timeout: 10000 44 | }; 45 | noSandboxArgs = ['--no-sandbox', '--disable-setuid-sandbox']; 46 | if (browserArgs.indexOf('no_sandbox') !== -1) launchArgs.args = noSandboxArgs;else if (browserName.indexOf('?') !== -1) { 47 | userArgs = browserName.split('?'); 48 | params = userArgs[0]; 49 | if (params === 'no_sandbox') launchArgs.args = noSandboxArgs; 50 | executablePath = userArgs[1]; 51 | if (executablePath.length > 0) launchArgs.executablePath = executablePath; 52 | } 53 | _context.next = 7; 54 | return _puppeteer.default.launch(launchArgs); 55 | 56 | case 7: 57 | this.browser = _context.sent; 58 | 59 | case 8: 60 | _context.next = 10; 61 | return this.browser.newPage(); 62 | 63 | case 10: 64 | page = _context.sent; 65 | emulationArg = browserArgs.find(function (v) { 66 | return /^emulate/.test(v); 67 | }); 68 | 69 | if (!Boolean(emulationArg)) { 70 | _context.next = 19; 71 | break; 72 | } 73 | 74 | _emulationArg$split = emulationArg.split('='), _emulationArg$split2 = (0, _slicedToArray2.default)(_emulationArg$split, 2), emulationDevice = _emulationArg$split2[1]; 75 | device = _puppeteer.default.devices[emulationDevice]; 76 | 77 | if (device) { 78 | _context.next = 17; 79 | break; 80 | } 81 | 82 | throw new Error('Emulation device is not supported'); 83 | 84 | case 17: 85 | _context.next = 19; 86 | return page.emulate(device); 87 | 88 | case 19: 89 | _context.next = 21; 90 | return page.goto(pageUrl); 91 | 92 | case 21: 93 | this.openedPages[id] = page; 94 | 95 | case 22: 96 | case "end": 97 | return _context.stop(); 98 | } 99 | } 100 | }, _callee, this); 101 | })); 102 | 103 | function openBrowser(_x, _x2, _x3) { 104 | return _openBrowser.apply(this, arguments); 105 | } 106 | 107 | return openBrowser; 108 | }(), 109 | closeBrowser: function () { 110 | var _closeBrowser = (0, _asyncToGenerator2.default)( 111 | /*#__PURE__*/ 112 | _regenerator.default.mark(function _callee2(id) { 113 | return _regenerator.default.wrap(function _callee2$(_context2) { 114 | while (1) { 115 | switch (_context2.prev = _context2.next) { 116 | case 0: 117 | delete this.openedPages[id]; 118 | _context2.next = 3; 119 | return this.browser.close(); 120 | 121 | case 3: 122 | case "end": 123 | return _context2.stop(); 124 | } 125 | } 126 | }, _callee2, this); 127 | })); 128 | 129 | function closeBrowser(_x4) { 130 | return _closeBrowser.apply(this, arguments); 131 | } 132 | 133 | return closeBrowser; 134 | }(), 135 | isValidBrowserName: function () { 136 | var _isValidBrowserName = (0, _asyncToGenerator2.default)( 137 | /*#__PURE__*/ 138 | _regenerator.default.mark(function _callee3() { 139 | return _regenerator.default.wrap(function _callee3$(_context3) { 140 | while (1) { 141 | switch (_context3.prev = _context3.next) { 142 | case 0: 143 | return _context3.abrupt("return", true); 144 | 145 | case 1: 146 | case "end": 147 | return _context3.stop(); 148 | } 149 | } 150 | }, _callee3); 151 | })); 152 | 153 | function isValidBrowserName() { 154 | return _isValidBrowserName.apply(this, arguments); 155 | } 156 | 157 | return isValidBrowserName; 158 | }(), 159 | // Extra methods 160 | resizeWindow: function () { 161 | var _resizeWindow = (0, _asyncToGenerator2.default)( 162 | /*#__PURE__*/ 163 | _regenerator.default.mark(function _callee4(id, width, height) { 164 | return _regenerator.default.wrap(function _callee4$(_context4) { 165 | while (1) { 166 | switch (_context4.prev = _context4.next) { 167 | case 0: 168 | _context4.next = 2; 169 | return this.openedPages[id].setViewport({ 170 | width: width, 171 | height: height 172 | }); 173 | 174 | case 2: 175 | case "end": 176 | return _context4.stop(); 177 | } 178 | } 179 | }, _callee4, this); 180 | })); 181 | 182 | function resizeWindow(_x5, _x6, _x7) { 183 | return _resizeWindow.apply(this, arguments); 184 | } 185 | 186 | return resizeWindow; 187 | }(), 188 | takeScreenshot: function () { 189 | var _takeScreenshot = (0, _asyncToGenerator2.default)( 190 | /*#__PURE__*/ 191 | _regenerator.default.mark(function _callee5(id, screenshotPath) { 192 | return _regenerator.default.wrap(function _callee5$(_context5) { 193 | while (1) { 194 | switch (_context5.prev = _context5.next) { 195 | case 0: 196 | _context5.next = 2; 197 | return this.openedPages[id].screenshot({ 198 | path: screenshotPath 199 | }); 200 | 201 | case 2: 202 | case "end": 203 | return _context5.stop(); 204 | } 205 | } 206 | }, _callee5, this); 207 | })); 208 | 209 | function takeScreenshot(_x8, _x9) { 210 | return _takeScreenshot.apply(this, arguments); 211 | } 212 | 213 | return takeScreenshot; 214 | }() 215 | }; 216 | exports.default = _default; 217 | module.exports = exports.default; --------------------------------------------------------------------------------