Adding initial webdriver.io test

Porting first selenium test from Ruby to Node.js using the
mocha framework. Starting with `category.feature` test.

Tests are placed in a new `tests/selenium` folder with their
own eslint config.

Bug: T190710
Change-Id: Iad954405a5ae0608fd5dc90dd5dfa434b3781037
This commit is contained in:
Jan Drewniak 2018-04-10 23:19:08 +02:00 committed by jdlrobson
parent f9ac8d40e5
commit 26e413e04d
19 changed files with 551 additions and 2 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
/composer.phar
.DS_Store
/.eslintcache
/tests/selenium/log

View File

@ -2,7 +2,9 @@
"private": true,
"scripts": {
"test": "grunt test && npm run doc && dev-scripts/svg_check.sh",
"doc": "jsdoc -c jsdoc.json"
"doc": "jsdoc -c jsdoc.json",
"selenium-test-cucumber": "wdio tests/selenium/wdio.conf.cucumber.js",
"selenium-test": "wdio tests/selenium/wdio.conf.js"
},
"dependencies": {},
"devDependencies": {
@ -15,8 +17,13 @@
"grunt-notify": "0.4.5",
"grunt-stylelint": "0.10.1",
"jsdoc": "3.5.5",
"mwbot": "1.0.10",
"pre-commit": "1.2.2",
"stylelint-config-wikimedia": "0.5.0",
"svgo": "0.7.2"
"svgo": "0.7.2",
"wdio-cucumber-framework": "1.1.1",
"wdio-mediawiki": "0.2.0",
"wdio-spec-reporter": "0.1.4",
"webdriverio": "4.13.1"
}
}

View File

@ -0,0 +1,18 @@
{
"root": true,
"extends": [
"wikimedia/server"
],
"env": {
"mocha": true
},
"globals": {
"browser": false,
"$": "readonly",
"mw": false
},
"rules": {
"camelcase": "off",
"no-restricted-syntax": "off"
}
}

42
tests/selenium/README.md Normal file
View File

@ -0,0 +1,42 @@
# Selenium tests
Please see tests/selenium/README.md file in mediawiki/core repository, usually at mediawiki/vagrant/mediawiki folder.
## Setup
Set up MediaWiki-Vagrant:
cd mediawiki/vagrant
vagrant up
vagrant roles enable minerva
vagrant provision
cd mediawiki
npm install
## Start Chromedriver and run all tests
Run both mediawiki/core and extension tests from mediawiki/core repository (usually at mediawiki/vagrant/mediawiki folder):
npm run selenium
## Start Chromedriver
To run only some tests, you first have to start Chromedriver in one terminal tab (or window):
chromedriver --url-base=wd/hub --port=4444
## Run test(s) from one file
Then, in another terminal tab (or window) run this from mediawiki/core repository (usually at mediawiki/vagrant/mediawiki folder):
npm run selenium-test -- --spec tests/selenium/specs/FILE-NAME.js
`wdio` is a dependency of mediawiki/core that you have installed with `npm install`.
## Run specific test(s)
To run only test(s) which name contains string TEST-NAME, run this from mediawiki/core repository (usually at mediawiki/vagrant/mediawiki folder):
./node_modules/.bin/wdio tests/selenium/wdio.conf.js --spec extensions/EXTENSION-NAME/tests/selenium/specs/FILE-NAME.js --mochaOpts.grep TEST-NAME
Make sure Chromedriver is running when executing the above command.

View File

@ -0,0 +1,10 @@
Feature: Categories
Scenario: I can view categories
Given I am in a wiki that has categories
And I am using the mobile site
And I am in beta mode
And I am on the "Selenium categories test page" page
When I click on the category button
Then I should see the categories overlay
And I should see a list of categories

View File

@ -0,0 +1,24 @@
const assert = require( 'assert' ),
{ ArticlePage } = require( './../support/world' );
const iClickOnTheCategoryButton = () => {
ArticlePage.category_element.waitForExist();
ArticlePage.category_element.click();
};
const iShouldSeeTheCategoriesOverlay = () => {
ArticlePage.overlay_heading_element.waitForExist();
assert.strictEqual( ArticlePage.overlay_heading_element.getText(),
'Categories' );
};
const iShouldSeeAListOfCategories = () => {
const el = ArticlePage.overlay_category_topic_item_element.waitForVisible();
assert.strictEqual( el, true );
};
module.exports = {
iClickOnTheCategoryButton,
iShouldSeeTheCategoriesOverlay,
iShouldSeeAListOfCategories
};

View File

@ -0,0 +1,31 @@
const assert = require( 'assert' ),
{ ArticlePage, UserLoginPage } = require( '../support/world' );
const iAmUsingTheMobileSite = () => {
ArticlePage.setMobileMode();
};
const iAmInBetaMode = () => {
ArticlePage.setBetaMode();
};
const iAmOnPage = ( article ) => {
ArticlePage.open( article );
};
const iAmLoggedIn = () => {
UserLoginPage.open();
UserLoginPage.loginAdmin();
assert.strictEqual( ArticlePage.is_authenticated_element.isExisting(), true );
};
const iAmLoggedIntoTheMobileWebsite = () => {
iAmUsingTheMobileSite();
iAmLoggedIn();
};
module.exports = {
iAmLoggedIntoTheMobileWebsite,
iAmUsingTheMobileSite,
iAmLoggedIn, iAmOnPage, iAmInBetaMode
};

View File

@ -0,0 +1,52 @@
const { api } = require( '../support/world' ),
Api = require( 'wdio-mediawiki/Api' );
const login = () => {
return api.loginGetEditToken( {
username: browser.options.username,
password: browser.options.password,
apiUrl: `${browser.options.baseUrl}/api.php`
} );
};
const waitForPropagation = ( timeMs ) => {
// wait 2 seconds so the change can propogate.
const d = new Date();
browser.waitUntil( () => new Date() - d > timeMs );
};
const iAmInAWikiThatHasCategories = ( title ) => {
const msg = 'This page is used by Selenium to test category related features.',
summary = 'edit by selenium test',
wikitext = `
${msg}
[[Category:Test category]]
[[Category:Selenium artifacts]]
[[Category:Selenium hidden category]]
`;
login().then( () => api.batch( [
[ 'create', 'Category:Selenium artifacts', msg, summary ],
[ 'create', 'Category:Test category', msg, summary ],
[ 'create', 'Category:Selenium hidden category', '__HIDDENCAT__', summary ]
] ) )
.catch( ( err ) => {
if ( err.code === 'articleexists' ) {
return;
}
throw err;
} );
// A pause is necessary to let the categories register with database before trying to use
// them in an article
waitForPropagation( 5000 );
Api.edit( title, wikitext );
// categories are handled by a JobRunner so need extra time to appear via API calls!
waitForPropagation( 5000 );
};
module.exports = {
waitForPropagation,
iAmInAWikiThatHasCategories
};

View File

@ -0,0 +1,12 @@
const assert = require( 'assert' ),
{ ArticlePage, SpecialHistoryPage } = require( '../support/world' );
const iClickOnTheHistoryLinkInTheLastModifiedBar = () => {
ArticlePage.last_modified_bar_history_link_element.waitForVisible();
ArticlePage.last_modified_bar_history_link_element.click();
assert.strictEqual( SpecialHistoryPage.side_list_element.isVisible(), true );
};
module.exports = {
iClickOnTheHistoryLinkInTheLastModifiedBar
};

View File

@ -0,0 +1,41 @@
const { defineSupportCode } = require( 'cucumber' ),
{ iClickOnTheCategoryButton,
iShouldSeeTheCategoriesOverlay, iShouldSeeAListOfCategories
} = require( './category_steps' ),
{ iAmInAWikiThatHasCategories } = require( './create_page_api_steps' ),
{
iAmUsingTheMobileSite,
iAmLoggedIntoTheMobileWebsite,
iAmOnPage, iAmInBetaMode
} = require( './common_steps' ),
{
iClickOnTheHistoryLinkInTheLastModifiedBar
} = require( './history_steps' );
defineSupportCode( function ( { Then, When, Given } ) {
// common steps
Given( /^I am using the mobile site$/, iAmUsingTheMobileSite );
Given( /^I am in beta mode$/, iAmInBetaMode );
Given( /^I am on the "(.+)" page$/, iAmOnPage );
Given( /^I am logged into the mobile website$/, iAmLoggedIntoTheMobileWebsite );
// Page steps
Given( /^I am in a wiki that has categories$/, () => {
iAmInAWikiThatHasCategories( 'Selenium categories test page' );
} );
// history steps
When( /^I click on the history link in the last modified bar$/,
iClickOnTheHistoryLinkInTheLastModifiedBar );
// Category steps
When( /^I click on the category button$/, iClickOnTheCategoryButton );
Then( /^I should see the categories overlay$/, iShouldSeeTheCategoriesOverlay );
Then( /^I should see a list of categories$/, iShouldSeeAListOfCategories );
} );

View File

@ -0,0 +1,22 @@
/**
* Hooks
*
* Hooks are used for setup and teardown of the environment before and after each scenario.
* It's preferable to use tags to invoke hooks rather than using the generic 'Before' and 'After'
* events, which execute before and after all scenario.
* https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/hooks.md
*/
const { After, Before } = require( 'cucumber' );
Before( function () {
// This hook will be executed before ALL scenarios
} );
After( function () {
// This hook will be executed after ALL scenarios
} );
Before( { tags: '@foo' }, function () {
// This hook will be executed before scenarios tagged with @foo
} );

View File

@ -0,0 +1,21 @@
/**
* Represents a generic article page
*
* @extends MinervaPage
* @example
* https://en.m.wikipedia.org/wiki/Barack_Obama
*/
const MinervaPage = require( './minerva_page' );
class ArticlePage extends MinervaPage {
get category_element() { return $( '.category-button' ); }
get overlay_heading_element() { return $( '.overlay-title h2' ); }
get overlay_category_topic_item_element() { return $( '.topic-title-list li' ); }
get is_authenticated_element() { return $( 'body.is-authenticated' ); }
get last_modified_bar_history_link_element() { return $( '.last-modifier-tagline a[href*=\'Special:History\']' ); }
}
module.exports = new ArticlePage();

View File

@ -0,0 +1,67 @@
/**
* Represents a page the can be presented in desktop
* or mobile mode (requires mobilefrontend), and has
* features like public 'beta' mode (requires mobilefrontend).
*
* @extends Page
* @example
* https://en.m.wikipedia.org/wiki/Barack_Obama
*/
const { Page } = require( './mw_core_pages' );
class MinervaPage extends Page {
get title() { return browser.getTitle(); }
/**
* Opens a page if it isn't already open.
* @param {string} path
*/
open( path = 'Main_Page' ) {
const currentPage = browser.getUrl(),
newPage = browser.options.baseUrl + '/index.php?title=' + path;
if ( currentPage !== newPage ) {
browser.url( newPage );
}
}
/**
* Ensure browser is opened on a MediaWiki page, and set a specified
* cookie for that domain.
* @param {string} name - name of the cookie
* @param {string} value - value of the cookie
*/
setCookie( name, value ) {
const currentPage = browser.getUrl();
let cookie;
if ( !currentPage.includes( browser.options.baseUrl ) ) {
this.open();
}
cookie = browser.getCookie( name );
if ( !cookie || cookie.value !== value ) {
browser.setCookie( {
name: name,
value: value } );
}
}
/**
* Set the mobile cookie
*/
setMobileMode() {
this.setCookie( 'mf_useformat', 'true' );
}
/**
* Set the beta cookie
*/
setBetaMode() {
this.setCookie( 'optin', 'beta' );
}
}
module.exports = MinervaPage;

View File

@ -0,0 +1,7 @@
/**
* A list of all custom Minerva pageObjects.
* To simplify imports in world.js.
*/
module.exports = {
ArticlePage: require( './article_page' )
};

View File

@ -0,0 +1,9 @@
/**
* A list of all MediaWiki core pageObjects.
* To simplify imports in world.js.
*/
module.exports = {
// Page is a constructor, all other pageObjects are instances.
Page: require( '../../../../../../../tests/selenium/pageobjects/page.js' ),
UserLoginPage: require( '../../../../../../../tests/selenium/pageobjects/userlogin.page.js' )
};

View File

@ -0,0 +1,30 @@
/**
* World
*
* World is a function that is bound as `this` to each step of a scenario.
* It is reset for each scenario.
* https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/world.md
*
* Contrary to Cucumber.js best practices, this `MinervaWorld` is not being
* bound to scenarios with the `setWorldConstructor` like this:
*
* setWorldConstructor(MinervaWorld);
*
* Instead, it acts as a simple function that encapsulates all dependencies,
* and is exported so that it can be imported into each step definition file,
* allowing us to use the dependencies across scenarios.
*/
const MwBot = require( 'mwbot' ),
mwCorePages = require( '../support/pages/mw_core_pages' ),
minervaPages = require( '../support/pages/minerva_pages' );
function MinervaWorld() {
/* dependencies */
this.api = new MwBot();
/* pageObjects */
Object.assign( this, mwCorePages );
Object.assign( this, minervaPages );
}
module.exports = new MinervaWorld();

View File

@ -0,0 +1,46 @@
const { iClickOnTheCategoryButton,
iShouldSeeTheCategoriesOverlay,
iShouldSeeAListOfCategories
} = require( '../features/step_definitions/category_steps' ),
{
iAmInAWikiThatHasCategories
} = require( '../features/step_definitions/create_page_api_steps' ),
{
iAmUsingTheMobileSite,
iAmOnPage, iAmInBetaMode
} = require( '../features/step_definitions/common_steps' );
// Feature: Categories
describe( 'Categories', function () {
// Scenario: I can view categories
it( 'I can view categories', function () {
const title = 'Selenium categories test page';
// Given I am in a wiki that has categories
iAmInAWikiThatHasCategories( title );
// And I am using the mobile site
iAmUsingTheMobileSite();
// And I am in beta mode
iAmInBetaMode();
// And I am on the "Selenium categories test page" page
iAmOnPage( title );
// When I click on the category button
iClickOnTheCategoryButton();
// Then I should see the categories overlay
iShouldSeeTheCategoriesOverlay();
// FIXME: This check is partially skipped as there is no way to lower $wgJobRunRate
// See: T199939#5095838
try {
iShouldSeeAListOfCategories();
} catch ( e ) {
// pass.
// eslint-disable-next-line no-console
console.warn( 'Unable to check the list of the categories. Is wgJobRunRate set correctly?' );
}
} );
} );

View File

@ -0,0 +1,14 @@
const { config } = require( './wdio.conf' );
config.specs = [ __dirname + '/features/*.feature' ];
config.framework = 'cucumber';
config.cucumberOpts = {
require: [
'./tests/selenium/features/support/*.js',
'./tests/selenium/features/step_definitions/index.js'
// search a (sub)folder for JS files with a wildcard
// works since version 1.1 of the wdio-cucumber-framework
// './src/**/*.js',
]
};
exports.config = config;

View File

@ -0,0 +1,95 @@
/**
* See also: http://webdriver.io/guide/testrunner/configurationfile.html
*/
const fs = require( 'fs' ),
saveScreenshot = require( 'wdio-mediawiki' ).saveScreenshot;
exports.config = {
// ======
// Custom WDIO config specific to MediaWiki
// ======
// Use in a test as `browser.options.<key>`.
// Defaults are for convenience with MediaWiki-Vagrant
// Wiki admin
username: process.env.MEDIAWIKI_USER || 'Admin',
password: process.env.MEDIAWIKI_PASSWORD || 'vagrant',
// Base for browser.url() and Page#openTitle()
baseUrl: ( process.env.MW_SERVER || 'http://127.0.0.1:8080' ) + (
process.env.MW_SCRIPT_PATH || '/w'
),
// ==================
// Test Files
// ==================
specs: [
__dirname + '/specs/*.js'
],
// ============
// Capabilities
// ============
capabilities: [ {
// https://sites.google.com/a/chromium.org/chromedriver/capabilities
browserName: 'chrome',
maxInstances: 1,
chromeOptions: {
// If DISPLAY is set, assume developer asked non-headless or CI with Xvfb.
// Otherwise, use --headless (added in Chrome 59)
// https://chromium.googlesource.com/chromium/src/+/59.0.3030.0/headless/README.md
args: [
...( process.env.DISPLAY ? [] : [ '--headless' ] ),
// Chrome sandbox does not work in Docker
...( fs.existsSync( '/.dockerenv' ) ? [ '--no-sandbox' ] : [] )
]
}
} ],
// ===================
// Test Configurations
// ===================
// Level of verbosity: silent | verbose | command | data | result | error
logLevel: 'error',
// Setting this enables automatic screenshots for when a browser command fails
// It is also used by afterTest for capturig failed assertions.
screenshotPath: process.env.LOG_DIR || __dirname + '/log',
// Default timeout for each waitFor* command.
waitforTimeout: 10 * 1000,
// See also: http://webdriver.io/guide/testrunner/reporters.html
reporters: [ 'spec' ],
// See also: http://mochajs.org
mochaOpts: {
ui: 'bdd',
timeout: 60 * 1000
},
// Make sure you have the wdio adapter package for the specific framework
// installed before running any tests.
framework: 'mocha',
// =====
// Hooks
// =====
/**
* Save a screenshot when test fails.
*
* @param {Object} test Mocha Test object
*/
afterTest: function ( test ) {
var filePath;
if ( !test.passed ) {
filePath = saveScreenshot( test.title );
// eslint-disable-next-line no-console
console.log( '\n\tScreenshot: ' + filePath + '\n' );
}
}
};
module.exports = exports;