Migrate editor_wikitext_saving.feature from Ruby to Node

This migrates the first of the browser tests which had a @login
step from Ruby to Node.js

Bug: T219920
Change-Id: I84e217e2a781aab9eb10e7d873c527d578ec8fd4
This commit is contained in:
jdlrobson 2019-04-05 14:09:47 -07:00
parent b2448e0d23
commit 7f2b69ac14
11 changed files with 331 additions and 19 deletions

View File

@ -40,3 +40,76 @@ To run only test(s) which name contains string TEST-NAME, run this from mediawik
./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.
# Migrating a test from Ruby to Node.js
Currently we are in the midst of porting our Ruby tests to Node.js.
When the tests/browser/features folder is empty, we are done and the whole tests/browser folder can be removed.
This is a slow process (porting a single test can take an entire afternoon).
## Step 1 - move feature file
Move the feature you want to port to Node.js
```
mv tests/browser/features/<name>.feature tests/selenium/features/
```
Example: https://gerrit.wikimedia.org/r/#/c/mediawiki/skins/MinervaNeue/+/501792/1/tests/selenium/features/editor_wikitext_saving.feature
## Step 2 - add boilerplate for missing steps
Run the feature in cucumber
```
./node_modules/.bin/wdio tests/selenium/wdio.conf.cucumber.js --spec tests/selenium/features/<name>.feature
```
You will get warnings such as:
```
Step "I go to a page that has languages" is not defined. You can ignore this error by setting cucumberOpts.ignoreUndefinedDefinitions as true.
```
For each missing step define them as one liners inside selenium/features/step_definitions/index.js
Create functions with empty bodies for each step.
Function anmes should be camel case without space, for example, `I go to a page that has languages` becomes `iGoToAPageThatHasLanguages`. Each function should be imported from a step file inside the features/step_definitions folder.
Re-reun the test. If done correctly this should now pass.
Example: https://gerrit.wikimedia.org/r/#/c/mediawiki/skins/MinervaNeue/+/501792/1..2
## Step 3 - copy across Ruby function bodies
Copy across the body of the Ruby equivalent inside each function body in tests/browser/features/step_definitions as comments.
Example: https://gerrit.wikimedia.org/r/#/c/mediawiki/skins/MinervaNeue/+/501792/2..3
## Step 4 - rewrite Ruby to Node.js
Sadly there is no shortcut here. Reuse as much as you can. Work with the knowledge that the parts you are adding will aid the next browser test migration.
The docs are your friend: http://v4.webdriver.io/api/utility/waitForVisible.html
Example: https://gerrit.wikimedia.org/r/#/c/mediawiki/skins/MinervaNeue/+/501792/2..4
## Step 5 - Make it work without Cucumber
Now the tests should be passing when run the browser tests using wdio.conf.cucumber.js or `npm run selenium-test-cucumber`
The final step involves making these run with
`npm run selenium-test`
This is relatively straightforward and mechanical.
1) Copy the feature file to the specs folder
```
cp tests/selenium/features/editor_wikitext_saving.feature tests/selenium/specs/editor_wikitext_saving.js
```
2) Convert indents to tabs
3) Add `//` before any tags
4) Replace `Scenario: <name>` with `it( '<name>', () => {`
5) Add closing braces for all scenarios: ` } );`
6) Replace `Feature: <feature>` with `describe('<feature>)', () => {` and add closing brace.
7) Replace `Background:` with `beforeEach( () => {` and add closing brace.
8) Find and replace `Given `, `When `, `And `, `Then ` with empy strings.
9) At top of file copy and paste imports from `selenium/features/step_definitions/index.js` to top of your new file and rewrite their paths.
10) Relying on autocomplete (VisualStudio Code works great) replace all the lines with the imports
11) Drop unused imports from the file.

View File

@ -1,5 +1,27 @@
const assert = require( 'assert' ),
{ ArticlePage, UserLoginPage } = require( '../support/world' );
Api = require( 'wdio-mediawiki/Api' ),
{ ArticlePage, UserLoginPage, api } = require( '../support/world.js' );
const login = () => {
return api.loginGetEditToken( {
username: browser.options.username,
password: browser.options.password,
apiUrl: `${browser.options.baseUrl}/api.php`
} );
};
const createPages = ( pages ) => {
const summary = 'edit by selenium test';
return login().then( () =>
api.batch(
pages.map( ( page ) => [ 'create' ].concat( page ).concat( [ summary ] ) )
)
);
};
const createPage = ( title, wikitext ) => {
return login().then( () => Api.edit( title, wikitext ) );
};
const iAmUsingTheMobileSite = () => {
ArticlePage.setMobileMode();
@ -11,6 +33,8 @@ const iAmInBetaMode = () => {
const iAmOnPage = ( article ) => {
ArticlePage.open( article );
// Make sure the article opened and JS loaded.
ArticlePage.waitUntilResourceLoaderModuleReady( 'skins.minerva.scripts' );
};
const iAmLoggedIn = () => {
@ -24,7 +48,25 @@ const iAmLoggedIntoTheMobileWebsite = () => {
iAmLoggedIn();
};
const pageExists = ( title ) => {
return createPage( title, 'Page created by Selenium browser test.' ).then( () => {
const d = new Date();
// wait 2 seconds so the change can propogate.
browser.waitUntil( () => new Date() - d > 2000 );
} );
};
const iAmOnAPageThatDoesNotExist = () => {
return iAmOnPage( `NewPage ${new Date()}` );
};
const iShouldSeeAToastNotification = () => {
ArticlePage.notification_element.waitForVisible();
};
module.exports = {
createPage, createPages,
pageExists, iAmOnAPageThatDoesNotExist, iShouldSeeAToastNotification,
iAmLoggedIntoTheMobileWebsite,
iAmUsingTheMobileSite,
iAmLoggedIn, iAmOnPage, iAmInBetaMode

View File

@ -1,13 +1,10 @@
const { api, ArticlePage } = 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 { api, ArticlePage } = require( '../support/world' );
const Api = require( 'wdio-mediawiki/Api' );
const {
iAmOnPage,
createPages,
createPage
} = require( './common_steps' );
const waitForPropagation = ( timeMs ) => {
// wait 2 seconds so the change can propogate.
@ -17,7 +14,6 @@ const waitForPropagation = ( timeMs ) => {
const iAmInAWikiThatHasCategories = ( title ) => {
const msg = 'This page is used by Selenium to test category related features.',
summary = 'edit by selenium test',
wikitext = `
${msg}
@ -26,11 +22,11 @@ const iAmInAWikiThatHasCategories = ( title ) => {
[[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 ]
] ) )
createPages( [
[ 'create', 'Category:Selenium artifacts', msg ],
[ 'create', 'Category:Test category', msg ],
[ 'create', 'Category:Selenium hidden category', '__HIDDENCAT__' ]
] )
.catch( ( err ) => {
if ( err.code === 'articleexists' ) {
return;
@ -63,8 +59,20 @@ const iAmOnAPageThatHasTheFollowingEdits = function ( table ) {
waitForPropagation( 5000 );
};
const iGoToAPageThatHasLanguages = () => {
const wikitext = `This page is used by Selenium to test language related features.
[[es:Selenium language test page]]
`;
return createPage( 'Selenium language test page', wikitext ).then( () => {
iAmOnPage( 'Selenium language test page' );
} );
};
module.exports = {
waitForPropagation,
iAmOnAPageThatHasTheFollowingEdits,
iAmInAWikiThatHasCategories
iAmInAWikiThatHasCategories,
iGoToAPageThatHasLanguages
};

View File

@ -0,0 +1,70 @@
const assert = require( 'assert' );
const { ArticlePageWithEditorOverlay, ArticlePage } = require( '../support/world.js' );
const iClickTheEditButton = () => {
ArticlePage.edit_link_element.waitForVisible();
ArticlePage.edit_link_element.click();
};
const iSeeTheWikitextEditorOverlay = () => {
ArticlePageWithEditorOverlay.editor_overlay_element.waitForVisible();
ArticlePageWithEditorOverlay.editor_textarea_element.waitForExist();
};
const iClearTheEditor = () => {
ArticlePageWithEditorOverlay.editor_textarea_element.setValue( '' );
};
const iDoNotSeeTheWikitextEditorOverlay = () => {
browser.waitUntil( () => {
return ArticlePageWithEditorOverlay.editor_overlay_element.isVisible() === false;
}, 10000 );
};
const iTypeIntoTheEditor = ( text ) => {
ArticlePageWithEditorOverlay.editor_overlay_element.waitForExist();
ArticlePageWithEditorOverlay.editor_textarea_element.waitForExist();
ArticlePageWithEditorOverlay.editor_textarea_element.waitForVisible();
ArticlePageWithEditorOverlay.editor_textarea_element.addValue( text );
browser.waitUntil( () => {
return !ArticlePageWithEditorOverlay
.continue_element.getAttribute( 'disabled' );
} );
};
const iClickContinue = () => {
ArticlePageWithEditorOverlay.continue_element.waitForExist();
ArticlePageWithEditorOverlay.continue_element.click();
};
const iClickSubmit = () => {
ArticlePageWithEditorOverlay.submit_element.waitForExist();
ArticlePageWithEditorOverlay.submit_element.click();
};
const iSayOkayInTheConfirmDialog = () => {
browser.waitUntil( () => {
try {
const text = browser.alertText;
return text && true;
} catch ( e ) {
return false;
}
}, 2000 );
browser.alertAccept();
};
const theTextOfTheFirstHeadingShouldBe = ( title ) => {
ArticlePage.first_heading_element.waitForVisible();
assert.strictEqual(
ArticlePage.first_heading_element.getText(),
title
);
};
const thereShouldBeARedLinkWithText = ( text ) => {
ArticlePage.red_link_element.waitForExist();
assert.strictEqual(
ArticlePage.red_link_element.getText(),
text
);
};
module.exports = {
iClickTheEditButton, iSeeTheWikitextEditorOverlay, iClearTheEditor,
thereShouldBeARedLinkWithText,
iDoNotSeeTheWikitextEditorOverlay,
iTypeIntoTheEditor, iClickContinue, iClickSubmit, iSayOkayInTheConfirmDialog,
theTextOfTheFirstHeadingShouldBe
};

View File

@ -3,8 +3,10 @@ const { defineSupportCode } = require( 'cucumber' ),
iShouldSeeTheCategoriesOverlay, iShouldSeeAListOfCategories
} = require( './category_steps' ),
{ iAmInAWikiThatHasCategories,
iAmOnAPageThatHasTheFollowingEdits } = require( './create_page_api_steps' ),
iAmOnAPageThatHasTheFollowingEdits,
iGoToAPageThatHasLanguages } = require( './create_page_api_steps' ),
{
pageExists, iAmOnAPageThatDoesNotExist, iShouldSeeAToastNotification,
iAmUsingTheMobileSite,
iAmLoggedIntoTheMobileWebsite,
iAmOnPage, iAmInBetaMode
@ -14,11 +16,29 @@ const { defineSupportCode } = require( 'cucumber' ),
} = require( './diff_steps' ),
{
iOpenTheLatestDiff,
iClickTheEditButton, iSeeTheWikitextEditorOverlay, iClearTheEditor,
iDoNotSeeTheWikitextEditorOverlay,
iTypeIntoTheEditor, iClickContinue, iClickSubmit, iSayOkayInTheConfirmDialog,
theTextOfTheFirstHeadingShouldBe, thereShouldBeARedLinkWithText
} = require( './editor_steps' ),
{
iClickOnTheHistoryLinkInTheLastModifiedBar
} = require( './history_steps' );
defineSupportCode( function ( { Then, When, Given } ) {
// Editor steps
Given( /^I click the edit button$/, iClickTheEditButton );
Then( /^I see the wikitext editor overlay$/, iSeeTheWikitextEditorOverlay );
When( /^I clear the editor$/, iClearTheEditor );
When( /^I type "(.+)" into the editor$/, iTypeIntoTheEditor );
When( /^I click continue$/, iClickContinue );
When( /^I click submit$/, iClickSubmit );
When( /^I say OK in the confirm dialog$/, iSayOkayInTheConfirmDialog );
Then( /^I do not see the wikitext editor overlay$/, iDoNotSeeTheWikitextEditorOverlay );
Then( /^the text of the first heading should be "(.+)"$/, theTextOfTheFirstHeadingShouldBe );
Then( /^there should be a red link with text "(.+)"$/, thereShouldBeARedLinkWithText );
// common steps
Given( /^I am using the mobile site$/, iAmUsingTheMobileSite );
@ -27,12 +47,16 @@ defineSupportCode( function ( { Then, When, Given } ) {
Given( /^I am on the "(.+)" page$/, iAmOnPage );
Given( /^I am logged into the mobile website$/, iAmLoggedIntoTheMobileWebsite );
Then( /^I should see a toast notification$/, iShouldSeeAToastNotification );
// Page steps
Given( /^I am in a wiki that has categories$/, () => {
iAmInAWikiThatHasCategories( 'Selenium categories test page' );
} );
Given( /^I am on a page that has the following edits:$/, iAmOnAPageThatHasTheFollowingEdits );
Given( /^I am on a page that does not exist$/, iAmOnAPageThatDoesNotExist );
Given( /^I go to a page that has languages$/, iGoToAPageThatHasLanguages );
Given( /^the page "(.+)" exists$/, pageExists );
// history steps
When( /^I open the latest diff$/, iOpenTheLatestDiff );

View File

@ -11,8 +11,12 @@ const MinervaPage = require( './minerva_page' );
class ArticlePage extends MinervaPage {
get category_element() { return $( '.category-button' ); }
get edit_link_element() { return $( '#ca-edit' ); }
get first_heading_element() { return $( '#section_0' ); }
get notification_element() { return $( '.mw-notification-area .mw-notification' ); }
get overlay_heading_element() { return $( '.overlay-title h2' ); }
get overlay_category_topic_item_element() { return $( '.topic-title-list li' ); }
get red_link_element() { return $( 'a.new' ); }
get is_authenticated_element() { return $( 'body.is-authenticated' ); }
get last_modified_bar_history_link_element() { return $( '.last-modifier-tagline a[href*=\'Special:History\']' ); }

View File

@ -0,0 +1,21 @@
/**
* Represents a generic article page with the editor overlay open
*
* @class ArticlePageWithEditorOverlay
* @extends MinervaPage
* @example
* https://en.m.wikipedia.org/wiki/Barack_Obama#/editor/0
*/
const MinervaPage = require( './minerva_page' );
class ArticlePageWithEditorOverlay extends MinervaPage {
get editor_overlay_element() { return $( '.editor-overlay' ); }
// overlay components
get editor_textarea_element() { return $( '.editor-overlay .wikitext-editor' ); }
get continue_element() { return $( '.editor-overlay .continue' ); }
get submit_element() { return $( '.editor-overlay .submit' ); }
}
module.exports = new ArticlePageWithEditorOverlay();

View File

@ -62,6 +62,14 @@ class MinervaPage extends Page {
this.setCookie( 'optin', 'beta' );
}
waitUntilResourceLoaderModuleReady( moduleName ) {
browser.waitUntil( () => {
const state = browser.execute( ( m ) => {
return mw.loader.getState( m );
}, moduleName );
return state.value === 'ready';
} );
}
}
module.exports = MinervaPage;

View File

@ -4,6 +4,7 @@
*/
module.exports = {
ArticlePage: require( './article_page' ),
ArticlePageWithEditorOverlay: require( './article_page_with_editor_overlay' ),
SpecialHistoryPage: require( './special_history_page' ),
SpecialMobileDiffPage: require( './special_mobilediff_page' ),
DiffPage: require( './diff_page' )

View File

@ -0,0 +1,61 @@
const { iGoToAPageThatHasLanguages } = require( './../features/step_definitions/create_page_api_steps' ),
{
pageExists, iAmOnAPageThatDoesNotExist, iShouldSeeAToastNotification,
iAmLoggedIntoTheMobileWebsite
} = require( './../features/step_definitions/common_steps' ),
{
iClickTheEditButton, iSeeTheWikitextEditorOverlay, iClearTheEditor,
iDoNotSeeTheWikitextEditorOverlay,
iTypeIntoTheEditor, iClickContinue, iClickSubmit, iSayOkayInTheConfirmDialog,
theTextOfTheFirstHeadingShouldBe, thereShouldBeARedLinkWithText
} = require( './../features/step_definitions/editor_steps' );
// @test2.m.wikipedia.org @login
describe( 'Wikitext Editor (Makes actual saves)', () => {
beforeEach( () => {
iAmLoggedIntoTheMobileWebsite();
} );
// @editing
it( 'It is possible to edit', () => {
iGoToAPageThatHasLanguages();
iClickTheEditButton();
iSeeTheWikitextEditorOverlay();
iTypeIntoTheEditor( 'ABC GHI' );
iClickContinue();
iClickSubmit();
iDoNotSeeTheWikitextEditorOverlay();
iShouldSeeAToastNotification();
} );
// @editing @en.m.wikipedia.beta.wmflabs.org
it( 'Redirects', () => {
const title = 'Selenium wikitext editor test ' + Math.random();
pageExists( title );
iAmOnAPageThatDoesNotExist();
iClickTheEditButton();
iSeeTheWikitextEditorOverlay();
iClearTheEditor();
iTypeIntoTheEditor( `#REDIRECT [[${title}]]` );
iClickContinue();
iClickSubmit();
iSayOkayInTheConfirmDialog();
iDoNotSeeTheWikitextEditorOverlay();
theTextOfTheFirstHeadingShouldBe( title );
} );
// @editing @en.m.wikipedia.beta.wmflabs.org
it( 'Broken redirects', () => {
iAmOnAPageThatDoesNotExist();
iClickTheEditButton();
iSeeTheWikitextEditorOverlay();
iClearTheEditor();
iTypeIntoTheEditor( '#REDIRECT [[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]]' );
iClickContinue();
iClickSubmit();
iSayOkayInTheConfirmDialog();
iDoNotSeeTheWikitextEditorOverlay();
thereShouldBeARedLinkWithText( 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' );
} );
} );