diff --git a/cypress/e2e/tests/ve-cite/reuseRefs.cy.js b/cypress/e2e/tests/ve-cite/reuseRefs.cy.js new file mode 100644 index 000000000..df58fbb18 --- /dev/null +++ b/cypress/e2e/tests/ve-cite/reuseRefs.cy.js @@ -0,0 +1,137 @@ +import * as helpers from './../../utils/functions.helper.js'; + +const refText1 = 'This is citation #1 for reference #1 and #2'; +const refText2 = 'This is citation #2 for reference #3'; + +const wikiText = `This is reference #1: ${ refText1 }
` + +'This is reference #2
' + +`This is reference #3 ${ refText2 }
` + +''; + +function getTestString( prefix = 'CiteTest-reuseRefs' ) { + return prefix; +} + +describe( 'Re-using refs in Visual Editor', () => { + beforeEach( () => { + const title = getTestString( 'CiteTest-title' ); + const encodedTitle = encodeURIComponent( title ); + + cy.clearCookies(); + cy.visit( '/index.php' ); + + // Rely on the retry behavior of Cypress assertions to use this as a "wait" + // until the specified conditions are met. + cy.window().should( 'have.property', 'mw' ).and( 'have.property', 'loader' ).and( 'have.property', 'using' ); + cy.window().then( async ( win ) => { + await win.mw.loader.using( 'mediawiki.api' ); + const response = await new win.mw.Api().postWithEditToken( { + action: 'edit', + title: title, + text: wikiText, + formatversion: '2' + } ); + expect( response.edit.result ).to.equal( 'Success' ); + // Disable welcome dialog when entering edit mode + win.localStorage.setItem( 've-beta-welcome-dialog', 1 ); + } ); + + cy.visit( `/index.php?title=${ encodedTitle }` ); + + cy.window().should( 'have.property', 'mw' ).and( 'have.property', 'loader' ).and( 'have.property', 'using' ); + cy.window().then( async ( win ) => { + await win.mw.loader.using( 'mediawiki.base' ).then( async function () { + await win.mw.hook( 'wikipage.content' ).add( function () {} ); + } ); + } ); + + // Open Ve edit mode + cy.visit( `/index.php?title=${ encodedTitle }&veaction=edit` ); + + } ); + + it( 'should display existing references in the Cite re-use dialog', () => { + helpers.openVECiteReuseDialog(); + + // Assert reference content for the first reference + helpers.getCiteReuseDialogRefName( 1 ).should( 'contain.text', 'a' ); + helpers.getCiteReuseDialogRefNumber( 1 ).should( 'contain.text', '[1]' ); + helpers.getCiteReuseDialogRefText( 1 ).should( 'have.text', refText1 ); + + // Assert reference content for the second reference + helpers.getCiteReuseDialogRefName( 2 ).should( 'contain.text', '' ); + helpers.getCiteReuseDialogRefNumber( 2 ).should( 'contain.text', '[2]' ); + helpers.getCiteReuseDialogRefText( 2 ).should( 'have.text', refText2 ); + + } ); + + it( 'should display re-used reference in article with correct footnote number and notification in context dialog', () => { + // Currently there are 3 refs in the article + helpers.getRefsFromArticleSection().should( 'have.length', 3 ); + + // Place cursor next to ref #2 in order to add re-use ref next to it + cy.contains( '.mw-reflink-text', '[2]' ).type( '{rightarrow}' ); + + helpers.openVECiteReuseDialog(); + + // Re-use second ref + helpers.getCiteReuseDialogRefWidget( 2 ).click(); + // The context dialog on one of the references shows it's being used twice + cy.get( '.mw-reflink-text' ).contains( '[2]' ).click(); + cy.get( '.oo-ui-popupWidget-popup .ve-ui-mwReferenceContextItem-muted' ).should( 'have.text', 'This reference is used twice on this page.' ); + + helpers.getVEUIToolbarSaveButton().click(); + helpers.getSaveChangesDialogConfirmButton().click(); + + helpers.getMWSuccessNotification().should( 'be.visible' ); + + // ARTICLE SECTION + // Ref has been added to article, there are now 4 refs in the article + helpers.getRefsFromArticleSection().should( 'have.length', 4 ); + // Ref #2 now appears twice in the article with corresponding IDs matching the backlinks in the references section + helpers.backlinksIdShouldMatchFootnoteId( 2, 0, 2 ); + helpers.backlinksIdShouldMatchFootnoteId( 3, 1, 2 ); + + // Both references have the same footnote number + cy.get( '#mw-content-text p sup a' ).eq( 2 ).should( 'have.text', '[2]' ); + cy.get( '#mw-content-text p sup a' ).eq( 3 ).should( 'have.text', '[2]' ); + + // REFERENCES SECTION + // References section contains a list item for each reference + helpers.getRefsFromReferencesSection().should( 'have.length', 2 ); + + // Ref content should match + helpers.getRefFromReferencesSection( 2 ).find( '.reference-text' ).should( 'have.text', refText2 ); + } ); + + it( 'should display correct ref content and name attribute for re-used ref with existing name attribute', () => { + // Place cursor next to ref #1 in order to add re-used ref next to it + cy.contains( '.mw-reflink-text', '[1]' ).first().type( '{rightarrow}' ); + + helpers.openVECiteReuseDialog(); + // reuse first ref which has the name 'a' + helpers.getCiteReuseDialogRefText( 1 ).should( 'have.text', refText1 ); + helpers.getCiteReuseDialogRefName( 1 ).should( 'have.text', 'a' ); + helpers.getCiteReuseDialogRefWidget( 1 ).click(); + + helpers.getVEUIToolbarSaveButton().click(); + helpers.getSaveChangesDialogConfirmButton().click(); + + helpers.getMWSuccessNotification().should( 'be.visible' ); + + // ARTICLE SECTION + // Ref name 'a' has been added correctly + helpers.articleSectionRefMarkersContainCorrectRefName( '1' ); + + // REFERENCES SECTION + // Ref content from re-used ref is displayed correctly in backlink reference + helpers.getRefFromReferencesSection( 1 ).should( 'contain', refText1 ); + // Ref name a has been added to backlink + helpers.verifyBacklinkHrefContent( 'a', 1, 1 ); + + // ref #1 has reference name a assigned to its id + helpers.referenceSectionRefIdContainsRefName( 1, 'a' ); + // ref #2 has no name, if there is no ref name its skipped + helpers.referenceSectionRefIdContainsRefName( 2, null ); + } ); +} ); diff --git a/cypress/e2e/utils/functions.helper.js b/cypress/e2e/utils/functions.helper.js index 4b377c78d..060b95953 100644 --- a/cypress/e2e/utils/functions.helper.js +++ b/cypress/e2e/utils/functions.helper.js @@ -1,3 +1,7 @@ +export function getMWSuccessNotification() { + return cy.get( '.mw-notification-visible .oo-ui-icon-success' ); +} + export function getReference( num ) { return cy.get( `#mw-content-text .reference:nth-of-type(${ num })` ); @@ -31,3 +35,83 @@ export function getVEReferencePopup() { export function getVEDialog() { return cy.get( '.oo-ui-dialog-content .oo-ui-fieldsetLayout .ve-ui-mwTargetWidget .ve-ce-generated-wrapper' ); } + +export function openVECiteReuseDialog() { + cy.contains( '.oo-ui-labelElement-label', 'Cite' ).click(); + cy.get( '.oo-ui-tool-name-reference-existing > a.oo-ui-tool-link' ) + .contains( 'Re-use' ).click(); +} + +export function getVEUIToolbarSaveButton() { + return cy.get( '.ve-ui-toolbar-saveButton' ); +} + +export function getSaveChangesDialogConfirmButton() { + return cy.contains( '.oo-ui-labelElement-label', 'Save changes' ); +} + +export function getCiteReuseDialogRefWidget( rowNumber ) { + return cy.get( '.ve-ui-mwReferenceSearchWidget .oo-ui-selectWidget .ve-ui-mwReferenceResultWidget' ).eq( rowNumber - 1 ); +} + +export function getCiteReuseDialogRefName( rowNumber ) { + return cy.get( '.oo-ui-widget.oo-ui-widget-enabled .ve-ui-mwReferenceResultWidget .ve-ui-mwReferenceSearchWidget-name' ).eq( rowNumber - 1 ); +} + +export function getCiteReuseDialogRefNumber( rowNumber ) { + return cy.get( '.oo-ui-widget.oo-ui-widget-enabled .ve-ui-mwReferenceResultWidget .ve-ui-mwReferenceSearchWidget-citation' ).eq( rowNumber - 1 ); +} + +export function getCiteReuseDialogRefText( rowNumber ) { + return cy.get( '.oo-ui-widget.oo-ui-widget-enabled .ve-ui-mwReferenceResultWidget .ve-ce-paragraphNode' ).eq( rowNumber - 1 ); +} + +export function backlinksIdShouldMatchFootnoteId( supIndex, backlinkIndex, rowNumber ) { + return cy.get( '#mw-content-text p sup' ) + .eq( supIndex ) + .invoke( 'attr', 'id' ) + .then( ( id ) => { + getRefFromReferencesSection( rowNumber ) + .find( '.mw-cite-backlink a' ) + .eq( backlinkIndex ) + .invoke( 'attr', 'href' ) + .should( 'eq', `#${ id }` ); + } ); +} + +// Article Section +export function getRefsFromArticleSection() { + return cy.get( '#mw-content-text p sup' ); +} + +export function articleSectionRefMarkersContainCorrectRefName( refMarkerContent ) { + return getRefsFromArticleSection() + .find( `a:contains('[${ refMarkerContent }]')` ) // Filter by refMarkerContent + .each( ( $el ) => { + cy.wrap( $el ) + .should( 'have.text', `[${ refMarkerContent }]` ) + .and( 'have.attr', 'href', `#cite_note-a-${ refMarkerContent }` ); + } ); +} + +// References Section +export function getRefsFromReferencesSection() { + return cy.get( '#mw-content-text .references li' ); +} + +export function getRefFromReferencesSection( rowNumber ) { + return cy.get( `#mw-content-text .references li:eq(${ rowNumber - 1 })` ); +} + +export function referenceSectionRefIdContainsRefName( rowNumber, refName ) { + const id = refName !== null ? `cite_note-${ refName }-${ rowNumber }` : `cite_note-${ rowNumber }`; + return getRefFromReferencesSection( rowNumber ).should( 'have.attr', 'id', id ); +} + +export function verifyBacklinkHrefContent( refName, rowNumber, index ) { + const expectedHref = `#cite_ref-${ refName }_${ rowNumber }-${ index }`; + return getRefFromReferencesSection( rowNumber ) + .find( '.mw-cite-backlink a' ) + .eq( index ) + .should( 'have.attr', 'href', expectedHref ); +}