Merge "Selenium tests in Node.js using WebdriverIO"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 20 Mar 2017 17:05:58 +0000 (17:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 20 Mar 2017 17:05:58 +0000 (17:05 +0000)
15 files changed:
Gruntfile.js
package.json
tests/selenium/.eslintrc.json [new file with mode: 0644]
tests/selenium/README.md [new file with mode: 0644]
tests/selenium/pageobjects/createaccount.page.js [new file with mode: 0644]
tests/selenium/pageobjects/edit.page.js [new file with mode: 0644]
tests/selenium/pageobjects/history.page.js [new file with mode: 0644]
tests/selenium/pageobjects/page.js [new file with mode: 0644]
tests/selenium/pageobjects/preferences.page.js [new file with mode: 0644]
tests/selenium/pageobjects/userlogin.page.js [new file with mode: 0644]
tests/selenium/pageobjects/userlogout.page.js [new file with mode: 0644]
tests/selenium/specs/page.js [new file with mode: 0644]
tests/selenium/specs/user.js [new file with mode: 0644]
tests/selenium/wdio.conf.jenkins.js [new file with mode: 0644]
tests/selenium/wdio.conf.js [new file with mode: 0644]

index 0e1c8cb..1ecc2c0 100644 (file)
@@ -4,6 +4,7 @@ module.exports = function ( grunt ) {
 
        var wgServer = process.env.MW_SERVER,
                wgScriptPath = process.env.MW_SCRIPT_PATH,
+               WebdriverIOconfigFile,
                karmaProxy = {};
 
        grunt.loadNpmTasks( 'grunt-banana-checker' );
@@ -13,12 +14,19 @@ module.exports = function ( grunt ) {
        grunt.loadNpmTasks( 'grunt-jsonlint' );
        grunt.loadNpmTasks( 'grunt-karma' );
        grunt.loadNpmTasks( 'grunt-stylelint' );
+       grunt.loadNpmTasks( 'grunt-webdriver' );
 
        karmaProxy[ wgScriptPath ] = {
                target: wgServer + wgScriptPath,
                changeOrigin: true
        };
 
+       if ( process.env.JENKINS_HOME ) {
+               WebdriverIOconfigFile = './tests/selenium/wdio.conf.jenkins.js';
+       } else {
+               WebdriverIOconfigFile = './tests/selenium/wdio.conf.js';
+       }
+
        grunt.initConfig( {
                eslint: {
                        all: [
@@ -105,7 +113,15 @@ module.exports = function ( grunt ) {
                                        return require( 'path' ).join( dest, src.replace( 'resources/', '' ) );
                                }
                        }
+               },
+
+               // Configure WebdriverIO task
+               webdriver: {
+                       test: {
+                               configFile: WebdriverIOconfigFile
+                       }
                }
+
        } );
 
        grunt.registerTask( 'assert-mw-env', function () {
index b868295..5a5635b 100644 (file)
@@ -3,9 +3,11 @@
   "scripts": {
     "test": "grunt test",
     "doc": "jsduck",
-    "postdoc": "grunt copy:jsduck"
+    "postdoc": "grunt copy:jsduck",
+    "selenium": "killall -0 chromedriver 2>/dev/null || chromedriver --url-base=/wd/hub --port=4444 & grunt webdriver:test; killall chromedriver"
   },
   "devDependencies": {
+    "deepmerge": "1.3.2",
     "eslint": "3.12.2",
     "eslint-config-wikimedia": "0.3.0",
     "grunt": "1.0.1",
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "2.0.0",
     "grunt-stylelint": "0.7.0",
+    "grunt-webdriver": "2.0.3",
     "karma": "1.1.0",
     "karma-chrome-launcher": "2.0.0",
     "karma-firefox-launcher": "1.0.0",
     "karma-qunit": "1.0.0",
     "qunitjs": "1.22.0",
-    "stylelint-config-wikimedia": "0.4.1"
+    "stylelint-config-wikimedia": "0.4.1",
+    "wdio-junit-reporter": "0.2.0",
+    "wdio-mocha-framework": "0.5.8",
+    "wdio-spec-reporter": "0.0.5",
+    "webdriverio": "4.6.2"
   }
 }
diff --git a/tests/selenium/.eslintrc.json b/tests/selenium/.eslintrc.json
new file mode 100644 (file)
index 0000000..b2d0bdd
--- /dev/null
@@ -0,0 +1,11 @@
+{
+       "extends": "../../.eslintrc.json",
+       "env": {
+               "es6": true,
+               "mocha": true,
+               "node": true
+       },
+       "globals": {
+               "browser": false
+       }
+}
diff --git a/tests/selenium/README.md b/tests/selenium/README.md
new file mode 100644 (file)
index 0000000..479b958
--- /dev/null
@@ -0,0 +1,52 @@
+# Selenium tests
+
+## Prerequisites
+
+- [Chrome](https://www.google.com/chrome/)
+- [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/)
+- [Node.js](https://nodejs.org/en/)
+- [MediaWiki-Vagrant](https://www.mediawiki.org/wiki/MediaWiki-Vagrant)
+
+Set up MediaWiki-Vagrant:
+
+    cd mediawiki/vagrant
+    vagrant up
+
+## Installation
+
+    cd mediawiki
+    npm install
+
+## Usage
+
+    npm run selenium
+
+To run only one file (for example page.js), you first need to spawn the chromedriver:
+
+    chromedriver --url-base=/wd/hub --port=4444
+
+Then in another terminal:
+
+    cd mediawiki/tests/selenium
+    ../../node_modules/.bin/wdio --spec page.js
+
+The runner reads the config file `wdio.conf.js` and runs the spec listed in
+`page.js`.
+
+The defaults in the configuration files aim are targetting  a MediaWiki-Vagrant
+installation on installation on http://127.0.0.1:8080 with a user Admin and
+password 'vagrant'.  Those settings can be overriden using environment
+variables:
+
+`MW_SERVER`: to be set to the value of your $wgServer
+`MW_SCRIPT_PATH`: ditto with  $wgScriptPath
+`MEDIAWIKI_USER`: username of an account that can create users on the wiki.
+`MEDIAWIKI_PASSWORD`: password for above user
+
+Example:
+
+    MW_SERVER=http://example.org MW_SCRIPT_PATH=/dev/w npm run selenium
+
+## Links
+
+- [Selenium/Node.js](https://www.mediawiki.org/wiki/Selenium/Node.js)
diff --git a/tests/selenium/pageobjects/createaccount.page.js b/tests/selenium/pageobjects/createaccount.page.js
new file mode 100644 (file)
index 0000000..a0b9490
--- /dev/null
@@ -0,0 +1,25 @@
+'use strict';
+const Page = require( './page' );
+
+class CreateAccountPage extends Page {
+
+       get username() { return browser.element( '#wpName2' ); }
+       get password() { return browser.element( '#wpPassword2' ); }
+       get confirmPassword() { return browser.element( '#wpRetype' ); }
+       get create() { return browser.element( '#wpCreateaccount' ); }
+       get heading() { return browser.element( '#firstHeading' ); }
+
+       open() {
+               super.open( 'Special:CreateAccount' );
+       }
+
+       createAccount( username, password ) {
+               this.open();
+               this.username.setValue( username );
+               this.password.setValue( password );
+               this.confirmPassword.setValue( password );
+               this.create.click();
+       }
+
+}
+module.exports = new CreateAccountPage();
diff --git a/tests/selenium/pageobjects/edit.page.js b/tests/selenium/pageobjects/edit.page.js
new file mode 100644 (file)
index 0000000..819c546
--- /dev/null
@@ -0,0 +1,22 @@
+'use strict';
+const Page = require( './page' );
+
+class EditPage extends Page {
+
+       get content() { return browser.element( '#wpTextbox1' ); }
+       get displayedContent() { return browser.element( '#mw-content-text' ); }
+       get heading() { return browser.element( '#firstHeading' ); }
+       get save() { return browser.element( '#wpSave' ); }
+
+       open( name ) {
+               super.open( name + '&action=edit' );
+       }
+
+       edit( name, content ) {
+               this.open( name );
+               this.content.setValue( content );
+               this.save.click();
+       }
+
+}
+module.exports = new EditPage();
diff --git a/tests/selenium/pageobjects/history.page.js b/tests/selenium/pageobjects/history.page.js
new file mode 100644 (file)
index 0000000..869484e
--- /dev/null
@@ -0,0 +1,13 @@
+'use strict';
+const Page = require( './page' );
+
+class HistoryPage extends Page {
+
+       get comment() { return browser.element( '#pagehistory .comment' ); }
+
+       open( name ) {
+               super.open( name + '&action=history' );
+       }
+
+}
+module.exports = new HistoryPage();
diff --git a/tests/selenium/pageobjects/page.js b/tests/selenium/pageobjects/page.js
new file mode 100644 (file)
index 0000000..864bdae
--- /dev/null
@@ -0,0 +1,11 @@
+// From http://webdriver.io/guide/testrunner/pageobjects.html
+'use strict';
+class Page {
+       constructor() {
+               this.title = 'My Page';
+       }
+       open( path ) {
+               browser.url( '/index.php?title=' + path );
+       }
+}
+module.exports = Page;
diff --git a/tests/selenium/pageobjects/preferences.page.js b/tests/selenium/pageobjects/preferences.page.js
new file mode 100644 (file)
index 0000000..98b87fe
--- /dev/null
@@ -0,0 +1,20 @@
+'use strict';
+const Page = require( './page' );
+
+class PreferencesPage extends Page {
+
+       get realName() { return browser.element( '#mw-input-wprealname' ); }
+       get save() { return browser.element( '#prefcontrol' ); }
+
+       open() {
+               super.open( 'Special:Preferences' );
+       }
+
+       changeRealName( realName ) {
+               this.open();
+               this.realName.setValue( realName );
+               this.save.click();
+       }
+
+}
+module.exports = new PreferencesPage();
diff --git a/tests/selenium/pageobjects/userlogin.page.js b/tests/selenium/pageobjects/userlogin.page.js
new file mode 100644 (file)
index 0000000..bdbd41b
--- /dev/null
@@ -0,0 +1,23 @@
+'use strict';
+const Page = require( './page' );
+
+class UserLoginPage extends Page {
+
+       get username() { return browser.element( '#wpName1' ); }
+       get password() { return browser.element( '#wpPassword1' ); }
+       get loginButton() { return browser.element( '#wpLoginAttempt' ); }
+       get userPage() { return browser.element( '#pt-userpage' ); }
+
+       open() {
+               super.open( 'Special:UserLogin' );
+       }
+
+       login( username, password ) {
+               this.open();
+               this.username.setValue( username );
+               this.password.setValue( password );
+               this.loginButton.click();
+       }
+
+}
+module.exports = new UserLoginPage();
diff --git a/tests/selenium/pageobjects/userlogout.page.js b/tests/selenium/pageobjects/userlogout.page.js
new file mode 100644 (file)
index 0000000..e355fd5
--- /dev/null
@@ -0,0 +1,11 @@
+'use strict';
+const Page = require( './page' );
+
+class UserLogoutPage extends Page {
+
+       open() {
+               super.open( 'Special:UserLogout' );
+       }
+
+}
+module.exports = new UserLogoutPage();
diff --git a/tests/selenium/specs/page.js b/tests/selenium/specs/page.js
new file mode 100644 (file)
index 0000000..da80aaf
--- /dev/null
@@ -0,0 +1,54 @@
+'use strict';
+const assert = require( 'assert' ),
+       HistoryPage = require( '../pageobjects/history.page' ),
+       EditPage = require( '../pageobjects/edit.page' );
+
+describe( 'Page', function () {
+
+       var content,
+               name;
+
+       beforeEach( function () {
+               content = Math.random().toString();
+               name = Math.random().toString();
+       } );
+
+       it( 'should be creatable', function () {
+
+               // create
+               EditPage.edit( name, content );
+
+               // check
+               assert.equal( EditPage.heading.getText(), name );
+               assert.equal( EditPage.displayedContent.getText(), content );
+
+       } );
+
+       it( 'should be editable', function () {
+
+               var content2 = Math.random().toString();
+
+               // create
+               EditPage.edit( name, content );
+
+               // edit
+               EditPage.edit( name, content2 );
+
+               // check content
+               assert.equal( EditPage.heading.getText(), name );
+               assert.equal( EditPage.displayedContent.getText(), content2 );
+
+       } );
+
+       it( 'should have history', function () {
+
+               // create
+               EditPage.edit( name, content );
+
+               // check
+               HistoryPage.open( name );
+               assert.equal( HistoryPage.comment.getText(), `(Created page with "${content}")` );
+
+       } );
+
+} );
diff --git a/tests/selenium/specs/user.js b/tests/selenium/specs/user.js
new file mode 100644 (file)
index 0000000..6746c5b
--- /dev/null
@@ -0,0 +1,59 @@
+'use strict';
+const assert = require( 'assert' ),
+       CreateAccountPage = require( '../pageobjects/createaccount.page' ),
+       UserLoginPage = require( '../pageobjects/userlogin.page' ),
+       UserLogoutPage = require( '../pageobjects/userlogout.page' ),
+       PreferencesPage = require( '../pageobjects/preferences.page' );
+
+describe( 'User', function () {
+
+       var password,
+               username;
+
+       beforeEach( function () {
+               username = `User-${Math.random().toString()}`;
+               password = Math.random().toString();
+       } );
+
+       it( 'should be able to create account', function () {
+
+               // create
+               CreateAccountPage.createAccount( username, password );
+
+               // check
+               assert.equal( CreateAccountPage.heading.getText(), `Welcome, ${username}!` );
+
+       } );
+
+       it( 'should be able to log in', function () {
+
+               // create
+               CreateAccountPage.createAccount( username, password );
+
+               // logout
+               UserLogoutPage.open();
+
+               // log in
+               UserLoginPage.login( username, password );
+
+               // check
+               assert.equal( UserLoginPage.userPage.getText(), username );
+
+       } );
+
+       it( 'should be able to change preferences', function () {
+
+               var realName = Math.random().toString();
+
+               // create
+               CreateAccountPage.createAccount( username, password );
+
+               // change real name
+               PreferencesPage.changeRealName( realName );
+
+               // check
+               assert.equal( PreferencesPage.realName.getValue(), realName );
+
+       } );
+
+} );
diff --git a/tests/selenium/wdio.conf.jenkins.js b/tests/selenium/wdio.conf.jenkins.js
new file mode 100644 (file)
index 0000000..4491c8e
--- /dev/null
@@ -0,0 +1,20 @@
+/* eslint no-undef: "error"*/
+/* eslint-env node*/
+'use strict';
+var merge = require( 'deepmerge' ),
+       wdioConf = require( './wdio.conf.js' );
+
+// Overwrite default settings
+exports.config = merge( wdioConf.config, {
+       username: 'WikiAdmin',
+       password: 'testpass',
+       screenshotPath: '../log/',
+       baseUrl: process.env.MW_SERVER + process.env.MW_SCRIPT_PATH,
+
+       reporters: [ 'spec', 'junit' ],
+       reporterOptions: {
+               junit: {
+                       outputDir: '../log/'
+               }
+       }
+} );
diff --git a/tests/selenium/wdio.conf.js b/tests/selenium/wdio.conf.js
new file mode 100644 (file)
index 0000000..b5dc61a
--- /dev/null
@@ -0,0 +1,228 @@
+/* eslint comma-dangle: 0 */
+/* eslint no-undef: "error"*/
+/* eslint no-console: 0 */
+/* eslint-env node*/
+'use strict';
+
+const path = require( 'path' );
+
+function relPath( foo ) {
+       return path.resolve( __dirname, '../..', foo );
+}
+
+exports.config = {
+
+       //
+       // ======
+       //
+       // ======
+       // Custom
+       // ======
+       // Define any custom variables.
+       // Example:
+       // username: 'Admin',
+       // Use if from tests with:
+       // browser.options.username
+       username: process.env.MEDIAWIKI_USER === undefined ?
+               'Admin' :
+               process.env.MEDIAWIKI_USER,
+       password: process.env.MEDIAWIKI_PASSWORD === undefined ?
+               'vagrant' :
+               process.env.MEDIAWIKI_PASSWORD,
+       //
+       // ==================
+       // Specify Test Files
+       // ==================
+       // Define which test specs should run. The pattern is relative to the directory
+       // from which `wdio` was called. Notice that, if you are calling `wdio` from an
+       // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
+       // directory is where your package.json resides, so `wdio` will be called from there.
+       //
+       specs: [
+               relPath( './tests/selenium/specs/**/*.js' ),
+               relPath( './extensions/*/tests/selenium/specs/**/*.js' ),
+               relPath( './extensions/VisualEditor/modules/ve-mw/tests/selenium/specs/**/*.js' )
+       ],
+       // Patterns to exclude.
+       exclude: [
+       // 'path/to/excluded/files'
+       ],
+       //
+       // ============
+       // Capabilities
+       // ============
+       // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
+       // time. Depending on the number of capabilities, WebdriverIO launches several test
+       // sessions. Within your capabilities you can overwrite the spec and exclude options in
+       // order to group specific specs to a specific capability.
+       //
+       // First, you can define how many instances should be started at the same time. Let's
+       // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
+       // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
+       // files and you set maxInstances to 10, all spec files will get tested at the same time
+       // and 30 processes will get spawned. The property handles how many capabilities
+       // from the same test should run tests.
+       //
+       maxInstances: 1,
+       //
+       // If you have trouble getting all important capabilities together, check out the
+       // Sauce Labs platform configurator - a great tool to configure your capabilities:
+       // https://docs.saucelabs.com/reference/platforms-configurator
+       //
+       // For Chrome/Chromium https://sites.google.com/a/chromium.org/chromedriver/capabilities
+       capabilities: [ {
+               // maxInstances can get overwritten per capability. So if you have an in-house Selenium
+               // grid with only 5 firefox instances available you can make sure that not more than
+               // 5 instances get started at a time.
+               maxInstances: 1,
+               //
+               browserName: 'chrome',
+               // Since Chrome v57 https://bugs.chromium.org/p/chromedriver/issues/detail?id=1625
+               chromeOptions: {
+                       args: [ '--enable-automation' ]
+               }
+       } ],
+       //
+       // ===================
+       // Test Configurations
+       // ===================
+       // Define all options that are relevant for the WebdriverIO instance here
+       //
+       // By default WebdriverIO commands are executed in a synchronous way using
+       // the wdio-sync package. If you still want to run your tests in an async way
+       // e.g. using promises you can set the sync option to false.
+       sync: true,
+       //
+       // Level of logging verbosity: silent | verbose | command | data | result | error
+       logLevel: 'error',
+       //
+       // Enables colors for log output.
+       coloredLogs: true,
+       //
+       // Saves a screenshot to a given path if a command fails.
+       screenshotPath: './log/',
+       //
+       // Set a base URL in order to shorten url command calls. If your url parameter starts
+       // with "/", then the base url gets prepended.
+       baseUrl: (
+               process.env.MW_SERVER === undefined ?
+               'http://127.0.0.1:8080' :
+               process.env.MW_SERVER
+       ) + (
+               process.env.MW_SCRIPT_PATH === undefined ?
+               '/w' :
+               process.env.MW_SCRIPT_PATH
+       ),
+       //
+       // Default timeout for all waitFor* commands.
+       waitforTimeout: 20000,
+       //
+       // Default timeout in milliseconds for request
+       // if Selenium Grid doesn't send response
+       connectionRetryTimeout: 90000,
+       //
+       // Default request retries count
+       connectionRetryCount: 3,
+       //
+       // Initialize the browser instance with a WebdriverIO plugin. The object should have the
+       // plugin name as key and the desired plugin options as properties. Make sure you have
+       // the plugin installed before running any tests. The following plugins are currently
+       // available:
+       // WebdriverCSS: https://github.com/webdriverio/webdrivercss
+       // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
+       // Browserevent: https://github.com/webdriverio/browserevent
+       // plugins: {
+       //     webdrivercss: {
+       //         screenshotRoot: 'my-shots',
+       //         failedComparisonsRoot: 'diffs',
+       //         misMatchTolerance: 0.05,
+       //         screenWidth: [320,480,640,1024]
+       //     },
+       //     webdriverrtc: {},
+       //     browserevent: {}
+       // },
+       //
+       // Test runner services
+       // Services take over a specific job you don't want to take care of. They enhance
+       // your test setup with almost no effort. Unlike plugins, they don't add new
+       // commands. Instead, they hook themselves up into the test process.
+       // services: [],//
+       // Framework you want to run your specs with.
+       // The following are supported: Mocha, Jasmine, and Cucumber
+       // see also: http://webdriver.io/guide/testrunner/frameworks.html
+       //
+       // Make sure you have the wdio adapter package for the specific framework installed
+       // before running any tests.
+       framework: 'mocha',
+
+       // Test reporter for stdout.
+       // The only one supported by default is 'dot'
+       // see also: http://webdriver.io/guide/testrunner/reporters.html
+       reporters: [ 'spec' ],
+       //
+       // Options to be passed to Mocha.
+       // See the full list at http://mochajs.org/
+       mochaOpts: {
+               ui: 'bdd',
+               timeout: 20000
+       },
+       //
+       // =====
+       // Hooks
+       // =====
+       // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
+       // it and to build services around it. You can either apply a single function or an array of
+       // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
+       // resolved to continue.
+       //
+       // Gets executed once before all workers get launched.
+       // onPrepare: function ( config, capabilities ) {
+       // }
+       //
+       // Gets executed before test execution begins. At this point you can access all global
+       // variables, such as `browser`. It is the perfect place to define custom commands.
+       // before: function (capabilities, specs) {
+       // },
+       //
+       // Hook that gets executed before the suite starts
+       // beforeSuite: function (suite) {
+       // },
+       //
+       // Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
+       // beforeEach in Mocha)
+       // beforeHook: function () {
+       // },
+       //
+       // Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
+       // afterEach in Mocha)
+       //
+       // Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+       // beforeTest: function (test) {
+       // },
+       //
+       // Runs before a WebdriverIO command gets executed.
+       // beforeCommand: function (commandName, args) {
+       // },
+       //
+       // Runs after a WebdriverIO command gets executed
+       // afterCommand: function (commandName, args, result, error) {
+       // },
+       //
+       // Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+       // afterTest: function (test) {
+       // },
+       //
+       // Hook that gets executed after the suite has ended
+       // afterSuite: function (suite) {
+       // },
+       //
+       // Gets executed after all tests are done. You still have access to all global variables from
+       // the test.
+       // after: function (result, capabilities, specs) {
+       // },
+       //
+       // Gets executed after all workers got shut down and the process is about to exit. It is not
+       // possible to defer the end of the process using a promise.
+       // onComplete: function(exitCode) {
+       // }
+};