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

Reply via email to