diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/README.md b/packages/@vue/cli-plugin-e2e-nightwatch/README.md index bee6564b38..4bc631fe58 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/README.md +++ b/packages/@vue/cli-plugin-e2e-nightwatch/README.md @@ -6,32 +6,138 @@ - **`vue-cli-service test:e2e`** - run e2e tests with [NightwatchJS](http://nightwatchjs.org). + Run end-to-end tests with [Nightwatch.js](https://nightwatchjs.org). Options: ``` - --url run e2e tests against given url instead of auto-starting dev server - --config use custom nightwatch config file (overrides internals) - -e, --env specify comma-delimited browser envs to run in (default: chrome) - -t, --test specify a test to run by name - -f, --filter glob to filter tests by filename + --url run the tests against given url instead of auto-starting dev server + --config use custom nightwatch config file (overrides internals) + --headless use chrome or firefox in headless mode + --parallel enable parallel mode via test workers (only available in chromedriver) + --use-selenium use Selenium standalone server instead of chromedriver or geckodriver + -e, --env specify comma-delimited browser envs to run in (default: chrome) + -t, --test specify a test to run by name + -f, --filter glob to filter tests by filename ``` - > Note: this plugin currently uses Nightwatch v0.9.x. We are waiting for Nightwatch 1.0 to stabilize before upgrading. + Additionally, all [Nightwatch CLI options](https://nightwatchjs.org/guide/running-tests/#command-line-options) are also supported. + E.g.: `--verbose`, `--retries` etc. + - Additionally, [all Nightwatch CLI options are also supported](https://nightwatchjs.org/guide#command-line-options). +## Project Structure -## Configuration +The following structure will be generated when installing this plugin. There are examples for most testing concepts in Nightwatch available. + +``` +tests/e2e/ + ├── custom-assertions/ + | └── elementCount.js + ├── custom-commands/ + | ├── customExecute.js + | ├── openHomepage.js + | └── openHomepageClass.js + ├── page-objects/ + | └── homepage.js + ├── specs/ + | ├── test.js + | └── test-with-pageobjects.js + └── globals.js +``` -We've pre-configured Nightwatch to run with Chrome by default. If you wish to run e2e tests in additional browsers, you will need to add a `nightwatch.config.js` or `nightwatch.json` in your project root to configure additional browsers. The config will be merged into the [internal Nightwatch config](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-e2e-nightwatch/nightwatch.config.js). +#### `specs` +The main location where tests are located. Can contain sub-folders which can be targeted during the run using the `--group` argument. [More info](https://nightwatchjs.org/guide/running-tests/#test-groups). -Alternatively, you can completely replace the internal config with a custom config file using the `--config` option. +#### `custom-assertions` +Files located here are loaded automatically by Nightwatch and placed onto the `.assert` and `.verify` api namespaces to extend the Nightwatch built-in assertions. See [writing custom assertions](https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-assertions) for details. + +#### `custom-commands` +Files located here are loaded automatically by Nightwatch and placed onto the main `browser` api object to extend the built-in Nightwatch commands. See [writing custom commands](https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands) for details. -Consult Nightwatch docs for [configuration options](http://nightwatchjs.org/gettingstarted#settings-file) and how to [setup browser drivers](http://nightwatchjs.org/gettingstarted#browser-drivers-setup). +#### `page objects` +Working with page objects is a popular methodology in end-to-end UI testing. Files placed in this folder are automatically loaded onto the `.page` api namespace, with the name of the file being the name of the page object. See [working with page objects](https://nightwatchjs.org/guide/working-with-page-objects/) section for details. + +#### `globals.js` +The external globals file which can hold global properties or hooks. See [test globals](https://nightwatchjs.org/gettingstarted/configuration/#test-globals) section. ## Installing in an Already Created Project ``` sh vue add e2e-nightwatch ``` + +## Configuration + +We've pre-configured Nightwatch to run with Chrome by default. Firefox is also available via `--env firefox`. If you wish to run end-to-end tests in additional browsers (e.g. Safari, Microsoft Edge), you will need to add a `nightwatch.conf.js` or `nightwatch.json` in your project root to configure additional browsers. The config will be merged into the [internal Nightwatch config](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-e2e-nightwatch/nightwatch.config.js). + +Alternatively, you can completely replace the internal config with a custom config file using the `--config` option. + +Consult Nightwatch docs for [configuration options](https://nightwatchjs.org/gettingstarted/configuration/) and how to [setup browser drivers](http://nightwatchjs.org/gettingstarted#browser-drivers-setup). + +## Running Tests + +By default, all tests inside the `specs` folder will be run using Chrome. If you'd like to run end-to-end tests against Chrome (or Firefox) in headless mode, simply pass the `--headless` argument. + +```sh +$ vue-cli-service test:e2e +``` + +**Running a single test** + +To run a single test supply the filename path. E.g.: + +```sh +$ vue-cli-service test:e2e tests/e2e/specs/test.js +``` + +**Skip Dev server auto-start** + +If the development server is already running and you want to skip starting it automatically, pass the `--url` argument: + +```sh +$ vue-cli-service test:e2e --url http://localhost:8080/ +``` + +**Running in Firefox** + +Support for running tests in Firefox is also available by default. Simply run the following (optionally add `--headless` to run Firefox in headless mode): + +```sh +$ vue-cli-service test:e2e --env firefox [--headless] +``` + +**Running in Firefox and Chrome simultaneously** + +You can also run the tests simultaneously in both browsers by supplying both test environments separated by a comma (",") - no spaces. + +```sh +$ vue-cli-service test:e2e --env firefox,chrome [--headless] +``` + +**Running Tests in Parallel** + +For a significantly faster test run, you can enable parallel test running when there are several test suites. Concurrency is performed at the file level and is distributed automatically per available CPU core. + +For now, this is only available in Chromedriver. Read more about [parallel running](https://nightwatchjs.org/guide/running-tests/#parallel-running) in the Nightwatch docs. + +```sh +$ vue-cli-service test:e2e --parallel +``` + +**Running with Selenium** + +Since `v4`, the Selenium standalone server is not included anymore in this plugin and in most cases running with Selenium is not required since Nightwatch v1.0. + +It is still possible to use the Selenium server, by following these steps: + +__1.__ Install `selenium-server` NPM package: + + ```sh + $ npm install selenium-server --save-dev + ``` + +__2.__ Run with `--use-selenium` cli argument: + + ```sh + $ vue-cli-service test:e2e --use-selenium + ``` diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/globals-gecko.js b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/globals-gecko.js new file mode 100644 index 0000000000..fe71865839 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/globals-gecko.js @@ -0,0 +1,10 @@ +/** + * This file is copied during the firefox test inside the project folder and used to inspect the results + */ +const fs = require('fs') + +module.exports = { + reporter (results, cb) { + fs.writeFile('test_results_gecko.json', JSON.stringify(results), cb) + } +} diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/globals-generated.js b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/globals-generated.js new file mode 100644 index 0000000000..3f3139b2d6 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/globals-generated.js @@ -0,0 +1,14 @@ +/** + * This file is copied during the test inside the project folder and used to inpsect the results + */ +const fs = require('fs') + +module.exports = { + afterEach (browser, cb) { + fs.writeFile('test_settings.json', JSON.stringify(browser.options), cb) + }, + + reporter (results, cb) { + fs.writeFile('test_results.json', JSON.stringify(results), cb) + } +} diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/nightwatch.conf.js b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/nightwatch.conf.js new file mode 100644 index 0000000000..382a2831cd --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/lib/nightwatch.conf.js @@ -0,0 +1,6 @@ +/** + * This file is copied during the test inside the project folder + */ +module.exports = { + globals_path: './tests/e2e/globals-gecko.js' +} diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/nightwatchPlugin.spec.js b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/nightwatchPlugin.spec.js index 40259af7c7..5d4f336805 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/nightwatchPlugin.spec.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/__tests__/nightwatchPlugin.spec.js @@ -1,13 +1,100 @@ -jest.setTimeout(40000) +jest.setTimeout(process.env.APPVEYOR ? 300000 : 120000) +const fs = require('fs-extra') +const path = require('path') const create = require('@vue/cli-test-utils/createTestProject') -test('should work', async () => { - const project = await create('e2e-nightwatch', { - plugins: { - '@vue/cli-plugin-babel': {}, - '@vue/cli-plugin-e2e-nightwatch': {} +describe('nightwatch e2e plugin', () => { + let project + + beforeAll(async () => { + project = await create('e2e-nightwatch', { + plugins: { + '@vue/cli-plugin-babel': {}, + '@vue/cli-plugin-e2e-nightwatch': {} + } + }) + + await fs.copy(path.join(__dirname, './lib/globals-generated.js'), + path.join(project.dir, 'tests/e2e/globals-generated.js')) + + const config = { + globals_path: './tests/e2e/globals-generated.js' } + await project.write('nightwatch.json', JSON.stringify(config)) + }) + + test('should run all tests successfully', async () => { + await project.run(`vue-cli-service test:e2e --headless`) + let results = await project.read('test_results.json') + results = JSON.parse(results) + expect(Object.keys(results.modules)).toEqual([ + 'test-with-pageobjects', + 'test' + ]) + }) + + test('should run single test with custom nightwatch.json', async () => { + await project.run(`vue-cli-service test:e2e --headless -t tests/e2e/specs/test.js`) + let results = await project.read('test_results.json') + results = JSON.parse(results) + expect(Object.keys(results.modules)).toEqual([ + 'test' + ]) + }) + + test('should run single test with custom nightwatch.json and selenium server', async () => { + await project.run(`vue-cli-service test:e2e --headless --with-selenium -t tests/e2e/specs/test.js`) + let results = await project.read('test_results.json') + results = JSON.parse(results) + + let testSettings = await project.read('test_settings.json') + testSettings = JSON.parse(testSettings) + + expect(testSettings).toHaveProperty('selenium') + expect(testSettings.selenium.start_process).toStrictEqual(true) + expect(testSettings.selenium).toHaveProperty('cli_args') + expect(Object.keys(results.modules)).toEqual([ + 'test' + ]) + }) + + test('should run tests in parallel', async () => { + await project.run(`vue-cli-service test:e2e --headless --parallel`) + let results = await project.read('test_results.json') + results = JSON.parse(results) + + let testSettings = await project.read('test_settings.json') + testSettings = JSON.parse(testSettings) + + expect(testSettings.parallel_mode).toStrictEqual(true) + expect(testSettings.test_workers).toStrictEqual(true) + + expect(Object.keys(results.modules).sort()).toEqual([ + 'test', 'test-with-pageobjects' + ]) + }) + + // This test requires Firefox to be installed + const testFn = process.env.APPVEYOR ? test.skip : test + testFn('should run single test with custom nightwatch.conf.js in firefox', async () => { + // nightwatch.conf.js take priority over nightwatch.json + const copyConfig = fs.copy(path.join(__dirname, './lib/nightwatch.conf.js'), + path.join(project.dir, 'nightwatch.conf.js')) + + const copyGlobals = fs.copy(path.join(__dirname, './lib/globals-gecko.js'), + path.join(project.dir, 'tests/e2e/globals-gecko.js')) + + await Promise.all([copyConfig, copyGlobals]) + + await project.run(`vue-cli-service test:e2e --headless --env firefox -t tests/e2e/specs/test.js`) + let results = await project.read('test_results_gecko.json') + results = JSON.parse(results) + + expect(Object.keys(results.modules)).toEqual([ + 'test' + ]) + expect(results.modules.test).toHaveProperty('reportPrefix') + expect(results.modules.test.reportPrefix).toMatch(/^FIREFOX_.+/) }) - await project.run(`vue-cli-service test:e2e`) }) diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/index.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/index.js index edd4e76292..db739d73a6 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/generator/index.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/index.js @@ -8,7 +8,8 @@ module.exports = api => { 'test:e2e': 'vue-cli-service test:e2e' }, devDependencies: { - chromedriver: '^74.0.0' + chromedriver: '^76.0.1', + geckodriver: '^1.16.2' } }) } diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-assertions/elementCount.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-assertions/elementCount.js index 5288303858..cb5a224a37 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-assertions/elementCount.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-assertions/elementCount.js @@ -1,13 +1,27 @@ -// A custom Nightwatch assertion. -// The assertion name is the filename. -// Example usage: -// -// browser.assert.elementCount(selector, count) -// -// For more information on custom assertions see: -// http://nightwatchjs.org/guide#writing-custom-assertions +/** + * A custom Nightwatch assertion. The assertion name is the filename. + * + * Example usage: + * browser.assert.elementCount(selector, count) + * + * For more information on custom assertions see: + * https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-assertions + * + * + * @param {string|object} selectorOrObject + * @param {number} count + */ + +exports.assertion = function elementCount (selectorOrObject, count) { + let selector; + + // when called from a page object element or section + if (typeof selectorOrObject == 'object' && selectorOrObject.selector) { + selector = selectorOrObject.selector + } else { + selector = selectorOrObject + } -exports.assertion = function elementCount (selector, count) { this.message = `Testing if element <${selector}> has count: ${count}` this.expected = count this.pass = val => val === count diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/customExecute.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/customExecute.js new file mode 100644 index 0000000000..b965a79399 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/customExecute.js @@ -0,0 +1,37 @@ +/** + * A very basic Nightwatch custom command. The command name is the filename and the + * exported "command" function is the command. + * + * Example usage: + * browser.customExecute(function() { + * console.log('Hello from the browser window') + * }); + * + * For more information on writing custom commands see: + * https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands + * + * @param {*} data + */ +exports.command = function(data) { + // Other Nightwatch commands are available via "this" + + // .execute() inject a snippet of JavaScript into the page for execution. + // the executed script is assumed to be synchronous. + // + // See https://nightwatchjs.org/api/execute.html for more info. + // + this.execute( + // The function argument is converted to a string and sent to the browser + function(argData) {return argData;}, + + // The arguments for the function to be sent to the browser are specified in this array + [data], + + function(result) { + // The "result" object contains the result from the what we have sent back from the browser window + console.log('custom execute result:', result.value) + } + ); + + return this; +}; diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/openHomepage.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/openHomepage.js new file mode 100644 index 0000000000..635fbb7d8c --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/openHomepage.js @@ -0,0 +1,22 @@ +/** + * A basic Nightwatch custom command which demonstrates usage of ES6 async/await instead of using callbacks. + * The command name is the filename and the exported "command" function is the command. + * + * Example usage: + * browser.openHomepage(); + * + * For more information on writing custom commands see: + * https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands + * + */ +module.exports = { + command: async function () { + // Other Nightwatch commands are available via "this" + // .init() simply calls .url() command with the value of the "launch_url" setting + this.init(); + this.waitForElementVisible('#app'); + + const result = await this.elements('css selector', '#app ul'); + this.assert.strictEqual(result.value.length, 3); + } +}; diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/openHomepageClass.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/openHomepageClass.js new file mode 100644 index 0000000000..4c71717382 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/custom-commands/openHomepageClass.js @@ -0,0 +1,24 @@ +/** + * A class-based Nightwatch custom command which is a variation of the openHomepage.js command. + * The command name is the filename and class needs to contain a "command" method. + * + * Example usage: + * browser.openHomepageClass(); + * + * For more information on writing custom commands see: + * https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands + * + */ + +const assert = require('assert'); + +module.exports = class { + async command () { + // Other Nightwatch commands are available via "this.api" + this.api.init(); + this.api.waitForElementVisible('#app'); + + const result = await this.api.elements('css selector', '#app ul'); + assert.strictEqual(result.value.length, 3); + } +}; diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/globals.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/globals.js new file mode 100644 index 0000000000..babc8d8ac1 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/globals.js @@ -0,0 +1,104 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Refer to the entire list of global config settings here: +// https://github.com/nightwatchjs/nightwatch/blob/master/lib/settings/defaults.js#L16 +// +// More info on test globals: +// https://nightwatchjs.org/gettingstarted/configuration/#test-globals +// +/////////////////////////////////////////////////////////////////////////////////// + +module.exports = { + // this controls whether to abort the test execution when an assertion failed and skip the rest + // it's being used in waitFor commands and expect assertions + abortOnAssertionFailure: true, + + // this will overwrite the default polling interval (currently 500ms) for waitFor commands + // and expect assertions that use retry + waitForConditionPollInterval: 500, + + // default timeout value in milliseconds for waitFor commands and implicit waitFor value for + // expect assertions + waitForConditionTimeout : 5000, + + 'default': { + /* + The globals defined here are available everywhere in any test env + */ + + /* + myGlobal: function() { + return 'I\'m a method'; + } + */ + }, + + 'firefox': { + /* + The globals defined here are available only when the chrome testing env is being used + i.e. when running with --env firefox + */ + /* + * myGlobal: function() { + * return 'Firefox specific global'; + * } + */ + }, + + ///////////////////////////////////////////////////////////////// + // Global hooks + // - simple functions which are executed as part of the test run + // - take a callback argument which can be called when an async + // async operation is finished + ///////////////////////////////////////////////////////////////// + /** + * executed before the test run has started, so before a session is created + */ + /* + before(cb) { + //console.log('global before') + cb(); + }, + */ + + /** + * executed before every test suite has started + */ + /* + beforeEach(browser, cb) { + //console.log('global beforeEach') + cb(); + }, + */ + + /** + * executed after every test suite has ended + */ + /* + afterEach(browser, cb) { + browser.perform(function() { + //console.log('global afterEach') + cb(); + }); + }, + */ + + /** + * executed after the test run has finished + */ + /* + after(cb) { + //console.log('global after') + cb(); + }, + */ + + ///////////////////////////////////////////////////////////////// + // Global reporter + // - define your own custom reporter + ///////////////////////////////////////////////////////////////// + /* + reporter(results, cb) { + cb(); + } + */ +}; diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/page-objects/homepage.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/page-objects/homepage.js new file mode 100644 index 0000000000..86a80afc45 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/page-objects/homepage.js @@ -0,0 +1,52 @@ +/** + * A Nightwatch page object. The page object name is the filename. + * + * Example usage: + * browser.page.homepage.navigate() + * + * For more information on working with page objects see: + * https://nightwatchjs.org/guide/working-with-page-objects/ + * + */ + +module.exports = { + url: '/', + commands: [], + + // A page object can have elements + elements: { + appContainer: '#app' + }, + + // Or a page objects can also have sections + sections: { + app: { + selector: '#app', + + elements: { + logo: 'img' + }, + + // - a page object section can also have sub-sections + // - elements or sub-sections located here are retrieved using the "app" section as the base + sections: { + headline: { + selector: 'h1' + }, + + welcome: { + // the equivalent css selector for the "welcome" sub-section would be: + // '#app div.hello' + selector: 'div.hello', + + elements: { + cliPluginLinks: { + selector: 'ul', + index: 0 + } + } + } + } + } + } +}; diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test-with-pageobjects.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test-with-pageobjects.js new file mode 100644 index 0000000000..e666db0167 --- /dev/null +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test-with-pageobjects.js @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////// +// For authoring Nightwatch tests, see +// https://nightwatchjs.org/guide +// +// For more information on working with page objects see: +// https://nightwatchjs.org/guide/working-with-page-objects/ +//////////////////////////////////////////////////////////////// + +module.exports = { + beforeEach: (browser) => browser.init(), + + 'e2e tests using page objects': (browser) => { + const homepage = browser.page.homepage() + homepage.waitForElementVisible('@appContainer') + + const app = homepage.section.app; + app.assert.elementCount('@logo', 1) + app.expect.section('@welcome').to.be.visible + app.expect.section('@headline').text.to.match(/^Welcome to Your Vue\.js (.*)App$/) + + browser.end() + }, + + 'verify if string "e2e-nightwatch" is within the cli plugin links': (browser) => { + const homepage = browser.page.homepage() + const welcomeSection = homepage.section.app.section.welcome + + welcomeSection.expect.element('@cliPluginLinks').text.to.contain('e2e-nightwatch') + } +} diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test.js b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test.js index 54527d1b17..f1877a7ee5 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/generator/template/tests/e2e/specs/test.js @@ -1,14 +1,21 @@ // For authoring Nightwatch tests, see -// http://nightwatchjs.org/guide#usage +// https://nightwatchjs.org/guide module.exports = { 'default e2e tests': browser => { browser - .url(process.env.VUE_DEV_SERVER_URL) - .waitForElementVisible('#app', 5000) + .init() + .waitForElementVisible('#app') .assert.elementPresent('.hello') .assert.containsText('h1', 'Welcome to Your Vue.js <%- hasTS ? '+ TypeScript ' : '' %>App') .assert.elementCount('img', 1) .end() + }, + + 'example e2e test using a custom command': browser => { + browser + .openHomepage() + .assert.elementPresent('.hello') + .end() } } diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/index.js b/packages/@vue/cli-plugin-e2e-nightwatch/index.js index bf12bd9cc1..6c56657ad2 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/index.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/index.js @@ -1,49 +1,59 @@ +const fs = require('fs') + module.exports = (api, options) => { + const { info, chalk, execa } = require('@vue/cli-shared-utils') + api.registerCommand('test:e2e', { - description: 'run e2e tests with nightwatch', + description: 'run end-to-end tests with nightwatch', usage: 'vue-cli-service test:e2e [options]', options: { - '--url': 'run e2e tests against given url instead of auto-starting dev server', + '--url': 'run end-to-end tests against given url instead of auto-starting dev server', '--config': 'use custom nightwatch config file (overrides internals)', + '--headless': 'use chrome or firefox in headless mode', + '--parallel': 'enable parallel mode via test workers (only available in chromedriver)', + '--use-selenium': 'use Selenium standalone server instead of chromedriver or geckodriver', '-e, --env': 'specify comma-delimited browser envs to run in (default: chrome)', '-t, --test': 'specify a test to run by name', '-f, --filter': 'glob to filter tests by filename' }, details: `All Nightwatch CLI options are also supported.\n` + - `https://nightwatchjs.org/guide#command-line-options` + chalk.yellow(`https://nightwatchjs.org/guide/running-tests/#command-line-options`) }, (args, rawArgs) => { - removeArg(rawArgs, 'url') - removeArg(rawArgs, 'mode') + const argsToRemove = ['url', 'mode', 'headless', 'use-selenium', 'parallel'] + argsToRemove.forEach((toRemove) => removeArg(rawArgs, toRemove)) - const serverPromise = args.url - ? Promise.resolve({ url: args.url }) - : api.service.run('serve') + return Promise.all([ + startDevServer(args, api), + loadNightwatchConfig(rawArgs, api) + ]).then((results) => { + const { server, url } = results[0] + let content = args.headless ? 'in headless mode' : '' + if (args.parallel) { + content += ' with concurrency' + } + + info(`Running end-to-end tests ${content}...`) - return serverPromise.then(({ server, url }) => { // expose dev server url to tests process.env.VUE_DEV_SERVER_URL = url - if (rawArgs.indexOf('--config') === -1) { - // expose user options to config file - const fs = require('fs') - let userOptionsPath, userOptions - if (fs.existsSync(userOptionsPath = api.resolve('nightwatch.config.js'))) { - userOptions = require(userOptionsPath) - } else if (fs.existsSync(userOptionsPath = api.resolve('nightwatch.json'))) { - userOptions = require(userOptionsPath) - } else if (fs.existsSync(userOptionsPath = api.resolve('nightwatch.conf.js'))) { - userOptions = require(userOptionsPath) - } - process.env.VUE_NIGHTWATCH_USER_OPTIONS = JSON.stringify(userOptions || {}) - - rawArgs.push('--config', require.resolve('./nightwatch.config.js')) - } if (rawArgs.indexOf('--env') === -1 && rawArgs.indexOf('-e') === -1) { rawArgs.push('--env', 'chrome') } - const execa = require('execa') + if (args['with-selenium']) { + process.env.VUE_NIGHTWATCH_USE_SELENIUM = '1' + } + + if (args.headless) { + process.env.VUE_NIGHTWATCH_HEADLESS = '1' + } + + if (args.parallel) { + process.env.VUE_NIGHTWATCH_CONCURRENT = '1' + } + const nightWatchBinPath = require.resolve('nightwatch/bin/nightwatch') const runner = execa(nightWatchBinPath, rawArgs, { stdio: 'inherit' }) if (server) { @@ -66,11 +76,83 @@ module.exports.defaultModes = { 'test:e2e': 'production' } +function startDevServer (args, api) { + const { url } = args + + if (url) { + return Promise.resolve({ url }) + } + + return api.service.run('serve') +} + +async function loadNightwatchConfig (rawArgs, api) { + if (rawArgs.indexOf('--config') === -1) { + // expose user options to config file + let userOptions + const configFiles = [ + 'nightwatch.config.js', + 'nightwatch.conf.js', + 'nightwatch.json' + ].map((entry) => api.resolve(entry)) + + const userOptionsPath = await findAsync(configFiles, fileExists) + + if (userOptionsPath) { + userOptions = require(userOptionsPath) + } + + process.env.VUE_NIGHTWATCH_USER_OPTIONS = JSON.stringify(userOptions || {}) + + rawArgs.push('--config', require.resolve('./nightwatch.config.js')) + } +} + +async function findAsync (arr, callback) { + while (arr.length) { + const item = arr.shift() + const result = await callback(item) + + if (result) { + return item + } + } + + return false +} + +async function fileExists (path) { + try { + const stats = await checkPath(path) + + return stats.isFile() + } catch (err) { + if (err.code === 'ENOENT') { + return false + } + + throw err + } +} + +function checkPath (source) { + return new Promise(function (resolve, reject) { + fs.stat(source, function (err, stat) { + if (err) { + return reject(err) + } + + resolve(stat) + }) + }) +} + function removeArg (rawArgs, argToRemove, offset = 1) { - const matchRE = new RegExp(`^--${argToRemove}`) + const matchRE = new RegExp(`^--${argToRemove}$`) const equalRE = new RegExp(`^--${argToRemove}=`) - const i = rawArgs.findIndex(arg => matchRE.test(arg)) - if (i > -1) { - rawArgs.splice(i, offset + (equalRE.test(rawArgs[i]) ? 0 : 1)) + + const index = rawArgs.findIndex(arg => matchRE.test(arg)) + if (index > -1) { + rawArgs.splice(index, offset + (equalRE.test(rawArgs[index]) ? 1 : 0)) } } diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/nightwatch.config.js b/packages/@vue/cli-plugin-e2e-nightwatch/nightwatch.config.js index ab2f46d3bb..a0fd12cf62 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/nightwatch.config.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/nightwatch.config.js @@ -1,39 +1,98 @@ // http://nightwatchjs.org/gettingstarted#settings-file +const path = require('path') const deepmerge = require('deepmerge') +const chromedriver = require('chromedriver') +const geckodriver = require('geckodriver') + const userOptions = JSON.parse(process.env.VUE_NIGHTWATCH_USER_OPTIONS || '{}') +const useSelenium = process.env.VUE_NIGHTWATCH_USE_SELENIUM === '1' +const startHeadless = process.env.VUE_NIGHTWATCH_HEADLESS === '1' +const concurrentMode = process.env.VUE_NIGHTWATCH_CONCURRENT === '1' +const chromeArgs = [] +const geckoArgs = [] + +if (startHeadless) { + chromeArgs.push('headless') + geckoArgs.push('--headless') +} -module.exports = deepmerge({ +const defaultSettings = { src_folders: ['tests/e2e/specs'], output_folder: 'tests/e2e/reports', - custom_assertions_path: ['tests/e2e/custom-assertions'], - - selenium: { - start_process: true, - server_path: require('selenium-server').path, - host: '127.0.0.1', - port: 4444, - cli_args: { - 'webdriver.chrome.driver': require('chromedriver').path - } - }, - + page_objects_path: 'tests/e2e/page-objects', + custom_assertions_path: 'tests/e2e/custom-assertions', + custom_commands_path: 'tests/e2e/custom-commands', + test_workers: concurrentMode, test_settings: { default: { - selenium_port: 4444, - selenium_host: 'localhost', - silent: true + detailed_output: !concurrentMode, + launch_url: '${VUE_DEV_SERVER_URL}' }, chrome: { desiredCapabilities: { browserName: 'chrome', chromeOptions: { - w3c: false - }, - javascriptEnabled: true, - acceptSslCerts: true + w3c: false, + args: chromeArgs + } + } + }, + + firefox: { + desiredCapabilities: { + browserName: 'firefox', + alwaysMatch: { + acceptInsecureCerts: true, + 'moz:firefoxOptions': { + args: geckoArgs + } + } + }, + webdriver: useSelenium ? {} : { + server_path: require('geckodriver').path, + port: 4444 + } + } + } +} + +const baseSettings = deepmerge(defaultSettings, webdriverServerSettings()) + +module.exports = deepmerge(baseSettings, adaptUserSettings(userOptions)) + +function adaptUserSettings (settings) { + // The path to nightwatch external globals file needs to be made absolute + // if it is supplied in an additional config file, due to merging of config files + if (settings.globals_path) { + settings.globals_path = path.resolve(settings.globals_path) + } + + return settings +} + +function webdriverServerSettings () { + if (useSelenium) { + return { + selenium: { + start_process: true, + host: '127.0.0.1', + port: 4444, + server_path: require('selenium-server').path, + cli_args: { + 'webdriver.chrome.driver': chromedriver.path, + 'webdriver.gecko.driver': geckodriver.path + } } } } -}, userOptions) + + return { + webdriver: { + start_process: true, + port: 9515, + server_path: chromedriver.path + } + } +} diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/package.json b/packages/@vue/cli-plugin-e2e-nightwatch/package.json index 45e075e987..d64ada1837 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/package.json +++ b/packages/@vue/cli-plugin-e2e-nightwatch/package.json @@ -26,14 +26,22 @@ "@vue/cli-shared-utils": "^4.0.0-rc.2", "deepmerge": "^3.2.0", "execa": "^1.0.0", - "nightwatch": "^1.1.11", - "selenium-server": "^3.141.59" + "nightwatch": "^1.2.2" }, "devDependencies": { - "chromedriver": "^74.0.0" + "chromedriver": "^76.0.1", + "geckodriver": "^1.16.2", + "selenium-server": "^3.141.59" }, "peerDependencies": { "@vue/cli-service": "^3.0.0 || ^4.0.0-0", - "chromedriver": "*" + "selenium-server": "^3.141.59", + "chromedriver": "*", + "geckodriver": "*" + }, + "peerDependenciesMeta": { + "selenium-server": { + "optional": true + } } }