var wgServer = process.env.MW_SERVER,
wgScriptPath = process.env.MW_SCRIPT_PATH,
+ WebdriverIOconfigFile,
karmaProxy = {};
grunt.loadNpmTasks( 'grunt-banana-checker' );
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: [
return require( 'path' ).join( dest, src.replace( 'resources/', '' ) );
}
}
+ },
+
+ // Configure WebdriverIO task
+ webdriver: {
+ test: {
+ configFile: WebdriverIOconfigFile
+ }
}
+
} );
grunt.registerTask( 'assert-mw-env', function () {
"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"
}
}
--- /dev/null
+{
+ "extends": "../../.eslintrc.json",
+ "env": {
+ "es6": true,
+ "mocha": true,
+ "node": true
+ },
+ "globals": {
+ "browser": false
+ }
+}
--- /dev/null
+# 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)
--- /dev/null
+'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();
--- /dev/null
+'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();
--- /dev/null
+'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();
--- /dev/null
+// 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;
--- /dev/null
+'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();
--- /dev/null
+'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();
--- /dev/null
+'use strict';
+const Page = require( './page' );
+
+class UserLogoutPage extends Page {
+
+ open() {
+ super.open( 'Special:UserLogout' );
+ }
+
+}
+module.exports = new UserLogoutPage();
--- /dev/null
+'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}")` );
+
+ } );
+
+} );
--- /dev/null
+'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 );
+
+ } );
+
+} );
--- /dev/null
+/* 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/'
+ }
+ }
+} );
--- /dev/null
+/* 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) {
+ // }
+};