Jdrewniak has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/378688 )
Change subject: [WIP] Ruby to Node conversion of integration tests. ...................................................................... [WIP] Ruby to Node conversion of integration tests. Begininng the conversion of integration tests from Ruby to Node. Maintaining Cucumber (here Cucumber.js) as the testing framework. https://github.com/cucumber/cucumber-js Using MWBot as the API helper https://github.com/Fannon/mwbot Requires chromedriver to be installed: https://sites.google.com/a/chromium.org/chromedriver/ Currently set to run in Chrome (not yet headless). Change-Id: I873f1deed555e78cab20201c938ca0a353ef0576 --- M Gruntfile.js M package.json A tests/browser/features/node_example.feature A tests/integration/config/wdio.conf.jenkins.js A tests/integration/config/wdio.conf.js A tests/integration/features/elasticsearch_on_special_version.feature A tests/integration/features/step_definitions/page_step_helpers.js A tests/integration/features/step_definitions/page_steps.js A tests/integration/features/suggest_api.feature A tests/integration/features/support/hooks.js A tests/integration/features/support/pages/article_page.js A tests/integration/features/support/pages/page.js A tests/integration/features/support/pages/special_version.js A tests/integration/features/support/world.js 14 files changed, 811 insertions(+), 2 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/CirrusSearch refs/changes/88/378688/1 diff --git a/Gruntfile.js b/Gruntfile.js index 024f234..b3f7d8f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,6 +10,15 @@ grunt.loadNpmTasks( 'grunt-jsonlint' ); grunt.loadNpmTasks( 'grunt-banana-checker' ); grunt.loadNpmTasks( 'grunt-stylelint' ); + grunt.loadNpmTasks( 'grunt-webdriver' ); + + var WebdriverIOconfigFile; + + if ( process.env.JENKINS_HOME ) { + WebdriverIOconfigFile = './tests/integration/config/wdio.conf.jenkins.js'; + } else { + WebdriverIOconfigFile = './tests/integration/config/wdio.conf.js'; + } grunt.initConfig( { jshint: { @@ -42,6 +51,12 @@ '!tests/browser/articles/**', '!vendor/**' ] + }, + // Configure WebdriverIO Node task + webdriver: { + test: { + configFile: WebdriverIOconfigFile + } } } ); diff --git a/package.json b/package.json index 2c7a0e1..e7f2409 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,21 @@ "private": true, "description": "Build tools for the CirrusSearch extension.", "scripts": { - "test": "grunt test" + "test": "grunt test", + "selenium": "killall -0 chromedriver 2>/dev/null || chromedriver --url-base=/wd/hub --port=4444 & grunt webdriver:test; killall chromedriver" }, "devDependencies": { + "cucumber": "^3.0.1", "grunt": "1.0.1", "grunt-banana-checker": "0.5.0", "grunt-contrib-jshint": "1.0.0", "grunt-jsonlint": "1.0.7", "grunt-stylelint": "0.6.0", + "grunt-webdriver": "^2.0.3", + "mwbot": "^1.0.9", "stylelint": "7.8.0", - "stylelint-config-wikimedia": "0.4.1" + "stylelint-config-wikimedia": "0.4.1", + "wdio-cucumber-framework": "^1.0.1", + "webdriverio": "^4.8.0" } } diff --git a/tests/browser/features/node_example.feature b/tests/browser/features/node_example.feature new file mode 100644 index 0000000..4120478 --- /dev/null +++ b/tests/browser/features/node_example.feature @@ -0,0 +1,10 @@ +Feature: Example feature + Scenario: 1 + 0 + Given I start with 1 + When I add 0 + Then I end up with 1 + + Scenario: 1 + 1 + Given I start with 1 + When I add 1 + Then I end up with 2 \ No newline at end of file diff --git a/tests/integration/config/wdio.conf.jenkins.js b/tests/integration/config/wdio.conf.jenkins.js new file mode 100644 index 0000000..451b1c0 --- /dev/null +++ b/tests/integration/config/wdio.conf.jenkins.js @@ -0,0 +1,22 @@ +/*jshint esversion: 6, node:true */ + +/* 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/integration/config/wdio.conf.js b/tests/integration/config/wdio.conf.js new file mode 100644 index 0000000..890a026 --- /dev/null +++ b/tests/integration/config/wdio.conf.js @@ -0,0 +1,281 @@ +/*jshint esversion: 6, node:true */ +/*global browser, console */ + +/* eslint-env node */ +/* eslint no-undef: "error" */ +/* eslint-disable no-console, comma-dangle */ +'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, + wikis: { + default: 'cirrustest', + cirrustest: { + username: 'Admin', + password: 'vagrant', + apiUrl: 'http://cirrustest.wiki.local.wmftest.net:8080/w/api.php' + }, + commons: { + username: 'Admin', + password: 'vagrant', + apiUrl: 'http://commons.wiki.local.wmftest.net:8080/w/api.php' + }, + ru: { + username: 'Admin', + password: 'vagrant', + apiUrl: 'http://ru.wiki.local.wmftest.net:8080/w/api.php' + }, + beta: {}, + test2: {}, + integration: {}, + cindy: {}, + searchdemo: {} + }, + // + // ====== + // Sauce Labs + // ====== + // + //services: [ 'sauce' ], + //user: process.env.SAUCE_USERNAME, + //key: process.env.SAUCE_ACCESS_KEY, + // + // ================== + // 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( './integration/features/*.feature' ) + ], + cucumberOpts: { + tagsInTitle: true, + require: [ + relPath('./integration/features/support/world.js'), + relPath('./integration/features/support/hooks.js'), + relPath('./integration/features/step_definitions/page_step_helpers.js'), + relPath('./integration/features/step_definitions/page_steps.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://dev.wiki.local.wmftest.net: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: 'cucumber', + + // 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. + // from https://github.com/webdriverio/webdriverio/issues/269#issuecomment-306342170 + afterTest: function ( test ) { + var filename, filePath; + // if test passed, ignore, else take and save screenshot + if ( test.passed ) { + return; + } + // get current test title and clean it, to use it as file name + filename = encodeURIComponent( test.title.replace( /\s+/g, '-' ) ); + // build file path + filePath = this.screenshotPath + filename + '.png'; + // save screenshot + browser.saveScreenshot( filePath ); + console.log( '\n\tScreenshot location:', filePath, '\n' ); + }, + // + // 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) { + // } +}; diff --git a/tests/integration/features/elasticsearch_on_special_version.feature b/tests/integration/features/elasticsearch_on_special_version.feature new file mode 100644 index 0000000..2e2734d --- /dev/null +++ b/tests/integration/features/elasticsearch_on_special_version.feature @@ -0,0 +1,5 @@ +@clean +Feature: Elasticsearch version in Special:Version + Scenario: Elasticsearch version is in Special:Version + When I go to Special:Version + Then there is a software version row for Elasticsearch \ No newline at end of file diff --git a/tests/integration/features/step_definitions/page_step_helpers.js b/tests/integration/features/step_definitions/page_step_helpers.js new file mode 100644 index 0000000..68c4fe8 --- /dev/null +++ b/tests/integration/features/step_definitions/page_step_helpers.js @@ -0,0 +1,46 @@ +/*jshint esversion: 6, node:true */ +/** + * StepHelpers are abstracted functions that usually represent the + * behaviour of a step. They are placed here, instead of in the actual step, + * so that they can be used in the Hook functions as well. + * + * Cucumber.js considers calling steps explicitly an antipattern, + * and therefore this ability has not been implemented in Cucumber.js even though + * it is available in the Ruby implementation. + * https://github.com/cucumber/cucumber-js/issues/634 + */ + +const assert = require('assert'); + +class StepHelpers { + constructor( world ) { + this.world = world; + } + + deletePage( title ) { + return this.world.apiClient.loginAndEditToken().then( () => { + return this.world.apiClient.delete( title, "CirrusSearch integration test delete" ) + .catch( ( err ) => { + // still return true if page doesn't exist + return assert( err.message.includes( "doesn't exist" ) ); + } ); + } ); + } + editPage( title, content ) { + return this.world.apiClient.loginAndEditToken().then( () => { + return this.world.apiClient.edit( title, content, "CirrusSearch integration test edit" ); + } ); + } + + suggestionSearch( query, limit = 'max' ) { + return this.world.apiClient.request( { + action: 'opensearch', + search: query, + cirrusUseCompletionSuggester: 'yes', + limit: limit + } ).then( ( response ) => this.world.setApiResponse( response ) ); + } + +} + +module.exports = StepHelpers; \ No newline at end of file diff --git a/tests/integration/features/step_definitions/page_steps.js b/tests/integration/features/step_definitions/page_steps.js new file mode 100644 index 0000000..caa66d6 --- /dev/null +++ b/tests/integration/features/step_definitions/page_steps.js @@ -0,0 +1,51 @@ +/*jshint esversion: 6, node:true */ + +/** + * Step definitions. Each step definition is bound to the World object, + * so any methods or properties in World are available here. + * + * Not: Do not use the fat-arrow syntax to define step functions, because + * Cucumber explicity binds the 'this' to 'World'. Arrow function would + * bind `this` to the parent function instead, which is not what we want. + */ + +const defineSupportCode = require('cucumber').defineSupportCode, + SpecialVersion = require('../support/pages/special_version'), + ArticlePage = require('../support/pages/article_page'), + assert = require( 'assert' ); + +defineSupportCode( function( {Given, When, Then} ) { + + When( /^I go to (.*)$/, function ( title ) { + this.visit( ArticlePage.title( title ) ); + } ); + + When( /^I ask suggestion API for (.*)$/, function ( query ) { + return this.stepHelpers.suggestionSearch( query ); + } ); + + When( /^I ask suggestion API at most (\d+) items? for (.*)$/, function( limit, query ) { + return this.stepHelpers.suggestionSearch( query, limit ); + } ); + + Then( /^there is a software version row for (.+)$/ , function ( name ) { + assert( SpecialVersion.software_table_row( name ) ); + } ); + + Then( /^the API should produce list containing (.*)/, function( term ) { + assert( this.apiResponse[ 1 ].includes( term ) ); + } ); + + Then( /^the API should produce empty list/, function() { + assert( this.apiResponse[ 1 ].length === 0 ); + } ); + + Then( /^the API should produce list starting with (.*)/, function( term ) { + assert( this.apiResponse[ 1 ][ 0 ] === term ); + } ); + + Then( /^the API should produce list of length (\d+)/, function( length ) { + assert( this.apiResponse[ 1 ].length === parseInt( length ) ); + } ); + +}); diff --git a/tests/integration/features/suggest_api.feature b/tests/integration/features/suggest_api.feature new file mode 100644 index 0000000..79beb0c --- /dev/null +++ b/tests/integration/features/suggest_api.feature @@ -0,0 +1,68 @@ +# +# This file is subject to the license terms in the COPYING file found in the +# CirrusSearch top-level directory and at +# https://phabricator.wikimedia.org/diffusion/ECIR/browse/master/COPYING. No part of +# CirrusSearch, including this file, may be copied, modified, propagated, or +# distributed except according to the terms contained in the COPYING file. +# +# Copyright 2012-2014 by the Mediawiki developers. See the CREDITS file in the +# CirrusSearch top-level directory and at +# https://phabricator.wikimedia.org/diffusion/ECIR/browse/master/CREDITS +# +@api @suggest +Feature: Suggestion API test + + Scenario: Search suggestions + When I ask suggestion API for main + Then the API should produce list containing Main Page + + Scenario: Created pages suggestions + When I ask suggestion API for x-m + Then the API should produce list containing X-Men + + Scenario: Nothing to suggest + When I ask suggestion API for jabberwocky + Then the API should produce empty list + + Scenario: Ordering + When I ask suggestion API for x-m + Then the API should produce list starting with X-Men + + Scenario: Fuzzy + When I ask suggestion API for xmen + Then the API should produce list starting with X-Men + + Scenario: Empty tokens + When I ask suggestion API for はー + Then the API should produce list starting with はーい + And I ask suggestion API for はい + Then the API should produce list starting with はーい + + Scenario Outline: Search redirects shows the best redirect + When I ask suggestion API for <term> + Then the API should produce list containing <suggested> + Examples: + | term | suggested | + | eise | Eisenhardt, Max | + | max | Max Eisenhardt | + | magnetu | Magneto | + + Scenario Outline: Search prefers exact match over fuzzy match and ascii folded + When I ask suggestion API for <term> + Then the API should produce list starting with <suggested> + Examples: + | term | suggested | + | max | Max Eisenhardt | + | mai | Main Page | + | eis | Eisenhardt, Max | + | ele | Elektra | + | éle | Électricité | + + Scenario Outline: Search prefers exact db match over partial prefix match + When I ask suggestion API at most 2 items for <term> + Then the API should produce list starting with <first> + And the API should produce list containing <other> + Examples: + | term | first | other | + | Ic | Iceman | Ice | + | Ice | Ice | Iceman | \ No newline at end of file diff --git a/tests/integration/features/support/hooks.js b/tests/integration/features/support/hooks.js new file mode 100644 index 0000000..8e55b5f --- /dev/null +++ b/tests/integration/features/support/hooks.js @@ -0,0 +1,102 @@ +/*jshint esversion: 6, node:true */ + +/** + * Hooks are run before or after Cucumber executes a test scenario. + * The World object is bound to the hooks as `this`, so any method + * or property in World is available here. + */ +var {defineSupportCode} = require( 'cucumber' ); + +var clean = false, + suggestions = false, + suggest = false; + +defineSupportCode( function( { After, Before } ) { + + Before( {tags: "@clean" }, function () { + if ( clean ) return true; + clean = true; + return this.stepHelpers.deletePage( "DeleteMeRedirect" ); + } ); + + Before( {tags: "@api" }, function () { + return true; + }); + + Before( {tags: "@suggestions" }, function () { + if ( suggestions ) return true; + suggestions = true; + suggestions = true; + return Promise.all( + [ + this.stepHelpers.editPage( "Popular Culture", "popular culture" ), + this.stepHelpers.editPage( "Nobel Prize", "nobel prize" ), + this.stepHelpers.editPage( "Noble Gasses", "noble gasses" ), + this.stepHelpers.editPage( "Noble Somethingelse", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Somethingelse2", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Somethingelse3", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Somethingelse4", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Somethingelse5", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Somethingelse6", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Somethingelse7", "Noble Somethingelse" ), + this.stepHelpers.editPage( "Noble Gasses", "noble gasses" ), + this.stepHelpers.editPage( "Template:Noble Pipe 1", "pipes are so noble" ), + this.stepHelpers.editPage( "Template:Noble Pipe 2", "pipes are so noble" ), + this.stepHelpers.editPage( "Template:Noble Pipe 3", "pipes are so noble" ), + this.stepHelpers.editPage( "Template:Noble Pipe 4", "pipes are so noble" ), + this.stepHelpers.editPage( "Template:Noble Pipe 5", "pipes are so noble" ), + this.stepHelpers.editPage( "Rrr Word 1", "#REDIRECT [[Popular Culture]]" ), + this.stepHelpers.editPage( "Rrr Word 2", "#REDIRECT [[Popular Culture]]" ), + this.stepHelpers.editPage( "Rrr Word 3", "#REDIRECT [[Noble Somethingelse3]]" ), + this.stepHelpers.editPage( "Rrr Word 4", "#REDIRECT [[Noble Somethingelse4]]" ), + this.stepHelpers.editPage( "Rrr Word 5", "#REDIRECT [[Noble Somethingelse5]]" ), + this.stepHelpers.editPage( "Nobel Gassez", "#REDIRECT [[Noble Gasses]]" ), + this.stepHelpers.editPage( "my suggest1 suggest2", "list of grammy awards winners" ), + this.stepHelpers.editPage( "my suggest2 suggest3", "list of grammy awards winners" ), + this.stepHelpers.editPage( "my suggest3 suggest4", "list of grammy awards winners" ), + this.stepHelpers.editPage( "my suggest4 suggest5", "list of grammy awards winners" ), + this.stepHelpers.editPage( "my suggest5 suggest6", "list of grammy awards winners" ), + this.stepHelpers.editPage( "my suggest6 suggest1", "list of grammy awards winners" ), + this.stepHelpers.editPage( "suggest1 suggest2 suggest3", "list of grammy awards winners" ), + this.stepHelpers.editPage( "suggest2 suggest3 suggest4", "list of grammy awards winners" ), + this.stepHelpers.editPage( "suggest3 suggest4 suggest5", "list of grammy awards winners" ) + ] + ); + } ); + + Before( {tags: "@suggest" }, function () { + if ( suggest ) return true; + suggest = true; + let batchJobs = { + edit: { + "X-Men": "The X-Men are a fictional team of superheroes", + "Xavier: Charles": "Professor Charles Francis Xavier (also known as Professor X) is the founder of [[X-Men]]", + "X-Force": "X-Force is a fictional team of of [[X-Men]]", + "Magneto": "Magneto is a fictional character appearing in American comic books", + "Max Eisenhardt": "#REDIRECT [[Magneto]]", + "Eisenhardt: Max": "#REDIRECT [[Magneto]]", + "Magnetu": "#REDIRECT [[Magneto]]", + "Ice": "It's cold.", + "Iceman": "Iceman (Robert \"Bobby\" Drake) is a fictional superhero appearing in American comic books published by Marvel Comics and is...", + "Ice Man (Marvel Comics)": "#REDIRECT [[Iceman]]", + "Ice-Man (comics books)": "#REDIRECT [[Iceman]]", + "Ultimate Iceman": "#REDIRECT [[Iceman]]", + "Électricité": "This is electicity in french.", + "Elektra": "Elektra is a fictional character appearing in American comic books published by Marvel Comics.", + "Help:Navigation": "When viewing any page on MediaWiki...", + "V:N": "#REDIRECT [[Help:Navigation]]", + "Z:Navigation": "#REDIRECT [[Help:Navigation]]", + "Venom": "Venom: or the Venom Symbiote: is a fictional supervillain appearing in American comic books published by Marvel Comics", + "Sam Wilson": "Warren Kenneth Worthington III: originally known as Angel and later as Archangel: ... Marvel Comics like [[Venom]]. {{DEFAULTSORTKEY:Wilson: Sam}}", + "Zam Wilson": "#REDIRECT [[Sam Wilson]]", + "The Doors": "The Doors were an American rock band formed in 1965 in Los Angeles.", + "Hyperion Cantos/Endymion": "Endymion is the third science fiction novel by Dan Simmons.", + "はーい": "makes sure we do not fail to index empty tokens (T156234)" + } + }; + return this.apiClient.loginAndEditToken().then( () => { + return this.apiClient.batch(batchJobs, 'CirrusSearch integration test edit'); + } ); + }); + +} ); diff --git a/tests/integration/features/support/pages/article_page.js b/tests/integration/features/support/pages/article_page.js new file mode 100644 index 0000000..8432bde --- /dev/null +++ b/tests/integration/features/support/pages/article_page.js @@ -0,0 +1,15 @@ +/*jshint esversion: 6, node:true */ + +// TODO: Incomplete +// Page showing the article with some actions. This is the page that everyone +// is used to reading on wikpedia. My mom would recognize this page. + +var Page = require('./page'); + +class ArticlePage extends Page { + constructor(){ + super(); + } +} + +module.exports = new ArticlePage(); \ No newline at end of file diff --git a/tests/integration/features/support/pages/page.js b/tests/integration/features/support/pages/page.js new file mode 100644 index 0000000..5544eb8 --- /dev/null +++ b/tests/integration/features/support/pages/page.js @@ -0,0 +1,77 @@ +/*jshint esversion: 6, node:true */ +/*global browser */ + +/** + * The Page object contains shortcuts and properties you would expect + * to find on a wiki page such as title, url. + */ + +class Page { + + constructor( title ){ + // tag selector shortcut. + // analogous to Ruby's link(:create_link, text: "Create") etc. + // assuming first param is a selector, second is text. + ['h1', + 'table', + 'td', + 'a', + 'ul', + 'li', + 'button', + 'textarea', + 'div', + 'span', + 'p', + 'input[type=text]', + 'input[type=submit]', + ].forEach( ( el ) => { + var alias = el; + switch ( el ) { + case 'a': alias = 'link'; break; + case 'input[type=text]': alias = 'text_field'; break; + case 'textarea': alias = 'text_area'; break; + case 'p': alias = 'paragraph'; break; + case 'ul': alias = 'unordered_list'; break; + case 'td': alias = 'cell'; break; + } + // the text option here doesn't work on child selectors + // when more that one element is returned. + // so "table#something td=text" doesn't work! + this[el] = this[alias] = ( selector, text ) => { + let s = selector || ''; + let t = ( text ) ? '='+text : ''; + let sel = el + s + t; + let elems = browser.elements( sel ); + // browser.elements selects all matching elements. + // If only one element is found, return it, else + // return the array of elements + if ( elems.value.length === 0 ) { + return elems.value[0]; + } else { + return elems; + } + }; + } ); + this._title = title || ''; + this._url = `/wiki/${this._title}`; + } + + get url() { + return this._url; + } + set url( title ) { + this._url = `/wiki/${title}`; + } + + title( title ) { + if ( !title ) { + return this._title; + } else { + this.url = title; + this._title = title; + return this; + } + } +} +module.exports = Page; diff --git a/tests/integration/features/support/pages/special_version.js b/tests/integration/features/support/pages/special_version.js new file mode 100644 index 0000000..4a8660e --- /dev/null +++ b/tests/integration/features/support/pages/special_version.js @@ -0,0 +1,15 @@ +/*jshint esversion: 6, node:true */ + +var Page = require('./page'); + +class SpecialVersion extends Page { + constructor() { + super(); + } + + software_table_row( name ) { + return this.table('#sv-software').element( 'td=' + name ); + } +} + +module.exports = new SpecialVersion( 'Special:Version' ); diff --git a/tests/integration/features/support/world.js b/tests/integration/features/support/world.js new file mode 100644 index 0000000..b20a37d --- /dev/null +++ b/tests/integration/features/support/world.js @@ -0,0 +1,96 @@ +/*jshint esversion: 6, node:true */ +/*global browser, console */ + +/** + * The World is a container for global state shared across test steps. + * The World is instanciated after every scenario, so state cannot be + * carried over between scenarios. + * + * Not: the `StepHelpers` are bound to the World object so that they have access + * to the same apiClient instance as `World` (useful because the apiClient + * keeps a user/login state). + */ +var {defineSupportCode} = require( 'cucumber' ); +var Bot = require( 'mwbot' ); +var StepHelpers = require( '../step_definitions/page_step_helpers' ); +var Page = require( './pages/page' ); + +function World( { attach, parameters } ) { + + // default properties + this.attach = attach; + this.parameters = parameters; + + // Binding step helpers to this World. + // Step helpers are just step functions that are abstracted + // for the purpose of using them outside of the steps themselves (like in hooks). + this.stepHelpers = new StepHelpers( this ); + + // Since you can't pass values between step definitions directly, + // the last Api response is stored here so it can be accessed between steps. + // (I have a feeling this is prone to race conditions). + // By suggestion of this stack overflow question. + // https://stackoverflow.com/questions/26372724/pass-variables-between-step-definitions-in-cucumber-groovy + this.apiResponse= ""; + + this.setApiResponse = function( value ) { + this.apiResponse = value; + }; + + // Shortcut to environment configs + this.config = browser.options; + + // Specified which wiki to use for the API client. + this.onWiki = function( wiki = this.config.wikis.default ) { + let w = this.config.wikis[ wiki ]; + return { + username: w.username, + password: w.password, + apiUrl: w.apiUrl + }; + }; + + // Instanciates new `mwbot` Api Client + this.apiClient = new Bot(); + this.apiClient.setOptions({ + verbose: true, + silent: false, + defaultSummary: 'MWBot', + concurrency: 1, + apiUrl: this.onWiki().apiUrl + }); + + /** + * Shortcut to `loginGetEditToken` that sets a default parameter for login. + * Hopefully `mwbot` can handle multiple loggins at once (not yet tested). + */ + this.apiClient.loginAndEditToken = ( wiki = this.config.wikis.default ) => { + let w = this.onWiki( wiki ); + return this.apiClient.loginGetEditToken( w ); + }; + + // Shortcut for browser.url(), accepts a Page object + // as well as a string, assumes the Page object + // has a url property + this.visit = function( page ) { + var tmpUrl; + if ( page instanceof Page && page.url ) { + tmpUrl = page.url; + } + if ( page instanceof String && page ) { + tmpUrl = page; + } + if ( !tmpUrl ) { + throw Error( `In "World.visit(page)" page is falsy: page=${ page }` ); + } + console.log( `Visiting page: ${tmpUrl}` ); + browser.url( tmpUrl ); + // logs full URL in case of typos, misplaced backslashes. + console.log( `Visited page: ${browser.getUrl()}` ); + + }; +} + +defineSupportCode( function( { setWorldConstructor } ) { + setWorldConstructor( World ); +}); \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/378688 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I873f1deed555e78cab20201c938ca0a353ef0576 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/CirrusSearch Gerrit-Branch: master Gerrit-Owner: Jdrewniak <jdrewn...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits