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 );
+}