From 7f2b69ac143427fa9a70e88d3dec257eb46ad0e1 Mon Sep 17 00:00:00 2001 From: jdlrobson Date: Fri, 5 Apr 2019 14:09:47 -0700 Subject: [PATCH] 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 --- tests/selenium/README.md | 73 +++++++++++++++++++ .../features/editor_wikitext_saving.feature | 0 .../features/step_definitions/common_steps.js | 44 ++++++++++- .../step_definitions/create_page_api_steps.js | 42 ++++++----- .../features/step_definitions/editor_steps.js | 70 ++++++++++++++++++ .../features/step_definitions/index.js | 26 ++++++- .../features/support/pages/article_page.js | 4 + .../pages/article_page_with_editor_overlay.js | 21 ++++++ .../features/support/pages/minerva_page.js | 8 ++ .../features/support/pages/minerva_pages.js | 1 + .../selenium/specs/editor_wikitext_saving.js | 61 ++++++++++++++++ 11 files changed, 331 insertions(+), 19 deletions(-) rename tests/{browser => selenium}/features/editor_wikitext_saving.feature (100%) create mode 100644 tests/selenium/features/step_definitions/editor_steps.js create mode 100644 tests/selenium/features/support/pages/article_page_with_editor_overlay.js create mode 100644 tests/selenium/specs/editor_wikitext_saving.js diff --git a/tests/selenium/README.md b/tests/selenium/README.md index 5c7d7c1b3..81e0e83f3 100644 --- a/tests/selenium/README.md +++ b/tests/selenium/README.md @@ -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/.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/.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: ` with `it( '', () => {` +5) Add closing braces for all scenarios: ` } );` +6) Replace `Feature: ` with `describe(')', () => {` 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. diff --git a/tests/browser/features/editor_wikitext_saving.feature b/tests/selenium/features/editor_wikitext_saving.feature similarity index 100% rename from tests/browser/features/editor_wikitext_saving.feature rename to tests/selenium/features/editor_wikitext_saving.feature diff --git a/tests/selenium/features/step_definitions/common_steps.js b/tests/selenium/features/step_definitions/common_steps.js index b41fb1cf7..1421392f4 100644 --- a/tests/selenium/features/step_definitions/common_steps.js +++ b/tests/selenium/features/step_definitions/common_steps.js @@ -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 diff --git a/tests/selenium/features/step_definitions/create_page_api_steps.js b/tests/selenium/features/step_definitions/create_page_api_steps.js index 7ec194626..24b851920 100644 --- a/tests/selenium/features/step_definitions/create_page_api_steps.js +++ b/tests/selenium/features/step_definitions/create_page_api_steps.js @@ -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 }; diff --git a/tests/selenium/features/step_definitions/editor_steps.js b/tests/selenium/features/step_definitions/editor_steps.js new file mode 100644 index 000000000..ddebd80cb --- /dev/null +++ b/tests/selenium/features/step_definitions/editor_steps.js @@ -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 +}; diff --git a/tests/selenium/features/step_definitions/index.js b/tests/selenium/features/step_definitions/index.js index c356ca093..397adf019 100644 --- a/tests/selenium/features/step_definitions/index.js +++ b/tests/selenium/features/step_definitions/index.js @@ -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 ); diff --git a/tests/selenium/features/support/pages/article_page.js b/tests/selenium/features/support/pages/article_page.js index 4518489ea..8ee6c1b73 100644 --- a/tests/selenium/features/support/pages/article_page.js +++ b/tests/selenium/features/support/pages/article_page.js @@ -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\']' ); } diff --git a/tests/selenium/features/support/pages/article_page_with_editor_overlay.js b/tests/selenium/features/support/pages/article_page_with_editor_overlay.js new file mode 100644 index 000000000..156ecbd3e --- /dev/null +++ b/tests/selenium/features/support/pages/article_page_with_editor_overlay.js @@ -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(); diff --git a/tests/selenium/features/support/pages/minerva_page.js b/tests/selenium/features/support/pages/minerva_page.js index 8feeb9ab6..78a9e2671 100644 --- a/tests/selenium/features/support/pages/minerva_page.js +++ b/tests/selenium/features/support/pages/minerva_page.js @@ -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; diff --git a/tests/selenium/features/support/pages/minerva_pages.js b/tests/selenium/features/support/pages/minerva_pages.js index 4b7249122..be36f46ec 100644 --- a/tests/selenium/features/support/pages/minerva_pages.js +++ b/tests/selenium/features/support/pages/minerva_pages.js @@ -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' ) diff --git a/tests/selenium/specs/editor_wikitext_saving.js b/tests/selenium/specs/editor_wikitext_saving.js new file mode 100644 index 000000000..89f3114f4 --- /dev/null +++ b/tests/selenium/specs/editor_wikitext_saving.js @@ -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' ); + } ); +} );