mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Cite
synced 2024-11-23 14:36:51 +00:00
Move reference previews to Cite extension
The ext.cite.referencePreviews module will transparently replace the ext.popups.referencePreviews module after this patch. Configuration stays in Popups for now, we can migrate it in later work. CSS classes may be renamed in the future but this will be handled separately since it could be a breaking change for on-wiki customizations. A lot of fancy footwork happens in this patch to emulate a soft dependency on Popups. This mechanism doesn't exist explicitly in either ResourceLoader or QUnit, so lots of workarounds are used, to conditionally load the module and to dynamically skip dependent tests. renderer.test.js is fully skipped for now, but can be wired up in later work. Bug: T355194 Change-Id: I0dc47abb59a40d4e41e7dda0eb7b415a2e1ae508
This commit is contained in:
parent
66d73af276
commit
dcb513eb0e
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"extends": [
|
"extends": [
|
||||||
"wikimedia/client-es6",
|
"wikimedia/server"
|
||||||
"wikimedia/jquery",
|
|
||||||
"wikimedia/mediawiki"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
80
cypress/e2e/tests/referencePreviews/referencePreviews.cy.js
Normal file
80
cypress/e2e/tests/referencePreviews/referencePreviews.cy.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import * as helpers from '../../utils/functions.helper.js';
|
||||||
|
|
||||||
|
const title = getTestString( 'CiteTest-title' );
|
||||||
|
const encodedTitle = encodeURIComponent( title );
|
||||||
|
|
||||||
|
function getTestString( prefix = '' ) {
|
||||||
|
return prefix + Math.random().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipTest( message ) {
|
||||||
|
cy.log( message );
|
||||||
|
// Dips into secret internals—stealing code from the skip plugin.
|
||||||
|
const mochaContext = cy.state( 'runnable' ).ctx;
|
||||||
|
return mochaContext.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe( 'Cite popups integration', () => {
|
||||||
|
before( () => {
|
||||||
|
cy.visit( '/index.php' );
|
||||||
|
|
||||||
|
const wikiText = 'Lorem ipsum dolor.<ref>small reference</ref>' +
|
||||||
|
'Reference with lots of text.<ref>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</ref>' +
|
||||||
|
'Lorem ipsum dolor.<ref>reference inception{{#tag:ref|body}}</ref>';
|
||||||
|
|
||||||
|
// 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' );
|
||||||
|
// Create a new page containing a reference
|
||||||
|
cy.window().then( async ( win ) => {
|
||||||
|
await win.mw.loader.using( 'mediawiki.api' );
|
||||||
|
const response = await new win.mw.Api().create( title, {}, wikiText );
|
||||||
|
expect( response.result ).to.equal( 'Success' );
|
||||||
|
|
||||||
|
await win.mw.loader.using( 'ext.popups.main', () => {}, () => skipTest( 'Popups not available' ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
cy.visit( `/index.php?title=${ encodedTitle }` );
|
||||||
|
cy.window()
|
||||||
|
.should( 'have.property', 'mw' ).and( 'have.property', 'loader' ).and( 'have.property', 'getState' );
|
||||||
|
cy.window().should( ( win ) => win.mw.loader.getState( 'ext.cite.referencePreviews' ) === 'ready' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'simple popup on hover and hide on leave', () => {
|
||||||
|
helpers.abandonReference( 'cite_ref-1' );
|
||||||
|
helpers.dwellReference( 'cite_ref-1' );
|
||||||
|
cy.get( '.mwe-popups-type-reference', { timeout: 1000 } )
|
||||||
|
.should( 'be.visible' );
|
||||||
|
helpers.assertPreviewIsScrollable( false );
|
||||||
|
cy.get( '.mwe-popups-fade-out' ).should( 'not.exist' );
|
||||||
|
|
||||||
|
helpers.abandonReference( 'cite_ref-1' );
|
||||||
|
cy.get( '.mwe-popups-type-reference' )
|
||||||
|
.should( 'not.exist' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'includes scrollbar and fadeout on long previews', () => {
|
||||||
|
helpers.abandonReference( 'cite_ref-2' );
|
||||||
|
helpers.dwellReference( 'cite_ref-2' );
|
||||||
|
cy.get( '.mwe-popups-type-reference', { timeout: 1000 } )
|
||||||
|
.should( 'be.visible' );
|
||||||
|
helpers.assertPreviewIsScrollable( true );
|
||||||
|
cy.get( '.mwe-popups-fade-out' ).should( 'be.visible' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'hovering nested reference', () => {
|
||||||
|
helpers.abandonReference( 'cite_ref-3' );
|
||||||
|
helpers.dwellReference( 'cite_ref-3' );
|
||||||
|
cy.get( '.mwe-popups-type-reference', { timeout: 1000 } )
|
||||||
|
.should( 'be.visible' );
|
||||||
|
helpers.dwellReference( 'cite_ref-4' );
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait( 1000 );
|
||||||
|
cy.get( '.mwe-popups-type-reference' )
|
||||||
|
.should( 'include.text', 'reference inception' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
} );
|
|
@ -115,3 +115,21 @@ export function verifyBacklinkHrefContent( refName, rowNumber, index ) {
|
||||||
.eq( index )
|
.eq( index )
|
||||||
.should( 'have.attr', 'href', expectedHref );
|
.should( 'have.attr', 'href', expectedHref );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function abandonReference( id ) {
|
||||||
|
cy.get( `:not(.reference-text) > #${ id } a` )
|
||||||
|
.trigger( 'mouseout' );
|
||||||
|
// Wait for the 300ms default ABANDON_END_DELAY.
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait( 500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dwellReference( id ) {
|
||||||
|
cy.get( `:not(.reference-text) > #${ id } a` )
|
||||||
|
.trigger( 'mouseover' );
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertPreviewIsScrollable( isScrollable ) {
|
||||||
|
cy.get( '.mwe-popups-extract .mwe-popups-scroll' )
|
||||||
|
.should( ( $el ) => isScrollable === ( $el.prop( 'scrollHeight' ) > $el.prop( 'offsetHeight' ) ) );
|
||||||
|
}
|
||||||
|
|
|
@ -31,7 +31,8 @@
|
||||||
"ParserCloned": "parser",
|
"ParserCloned": "parser",
|
||||||
"ParserFirstCallInit": "parser",
|
"ParserFirstCallInit": "parser",
|
||||||
"EditPage::showEditForm:initial": "main",
|
"EditPage::showEditForm:initial": "main",
|
||||||
"ResourceLoaderGetConfigVars": "main"
|
"ResourceLoaderGetConfigVars": "main",
|
||||||
|
"ResourceLoaderRegisterModules": "main"
|
||||||
},
|
},
|
||||||
"HookHandlers": {
|
"HookHandlers": {
|
||||||
"main": {
|
"main": {
|
||||||
|
@ -226,21 +227,29 @@
|
||||||
"remoteExtPath": "Cite/modules"
|
"remoteExtPath": "Cite/modules"
|
||||||
},
|
},
|
||||||
"QUnitTestModule": {
|
"QUnitTestModule": {
|
||||||
"localBasePath": "modules/ve-cite/tests",
|
"localBasePath": "",
|
||||||
"remoteExtPath": "Cite/modules/ve-cite/tests",
|
"remoteExtPath": "Cite",
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"ve.dm.citeExample.js",
|
"modules/ve-cite/tests/ve.dm.citeExample.js",
|
||||||
"ve.dm.Converter.test.js",
|
"modules/ve-cite/tests/ve.dm.Converter.test.js",
|
||||||
"ve.dm.InternalList.test.js",
|
"modules/ve-cite/tests/ve.dm.InternalList.test.js",
|
||||||
"ve.dm.MWReferenceModel.test.js",
|
"modules/ve-cite/tests/ve.dm.MWReferenceModel.test.js",
|
||||||
"ve.dm.Transaction.test.js",
|
"modules/ve-cite/tests/ve.dm.Transaction.test.js",
|
||||||
"ve.ui.DiffElement.test.js",
|
"modules/ve-cite/tests/ve.ui.DiffElement.test.js",
|
||||||
"ve.ui.MWWikitextStringTransferHandler.test.js"
|
"modules/ve-cite/tests/ve.ui.MWWikitextStringTransferHandler.test.js",
|
||||||
|
"tests/qunit/ext.cite.referencePreviews/createReferenceGateway.test.js",
|
||||||
|
"tests/qunit/ext.cite.referencePreviews/isReferencePreviewsEnabled.test.js",
|
||||||
|
"tests/qunit/ext.cite.referencePreviews/renderer.test.js",
|
||||||
|
"tests/qunit/ext.cite.referencePreviews/setUserConfigFlags.test.js"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"ext.cite.visualEditor",
|
"ext.cite.visualEditor",
|
||||||
"test.VisualEditor"
|
"test.VisualEditor"
|
||||||
]
|
],
|
||||||
|
"optionalDependencies": {
|
||||||
|
"Popups": "ext.cite.referencePreviews"
|
||||||
|
},
|
||||||
|
"factory": "Cite\\ResourceLoader\\OptionalLoader::addOptionalDependencies"
|
||||||
},
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"CodeMirror": {
|
"CodeMirror": {
|
||||||
|
@ -249,6 +258,11 @@
|
||||||
"references": "text/mediawiki"
|
"references": "text/mediawiki"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Popups": {
|
||||||
|
"PluginModules": [
|
||||||
|
"ext.cite.referencePreviews"
|
||||||
|
]
|
||||||
|
},
|
||||||
"VisualEditor": {
|
"VisualEditor": {
|
||||||
"PluginModules": [
|
"PluginModules": [
|
||||||
"ext.cite.visualEditor"
|
"ext.cite.visualEditor"
|
||||||
|
|
|
@ -18,8 +18,16 @@
|
||||||
},
|
},
|
||||||
"wmf": {
|
"wmf": {
|
||||||
"linkMap": {
|
"linkMap": {
|
||||||
|
"ext.popups.Preview": "https://doc.wikimedia.org/Popups/master/js/js/Popups/module-preview.html",
|
||||||
|
"ext.popups.PreviewModel": "https://doc.wikimedia.org/Popups/master/js/js/Popups/module-preview_model.html",
|
||||||
|
"Gateway": "https://doc.wikimedia.org/Popups/master/js/js/Popups/module-gateway.html",
|
||||||
|
"HTMLAnchorElement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement",
|
||||||
|
"HTMLElement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement",
|
||||||
"jQuery": "https://api.jquery.com/Types/#jQuery",
|
"jQuery": "https://api.jquery.com/Types/#jQuery",
|
||||||
"jQuery.Event": "https://api.jquery.com/category/events/event-object/",
|
"jQuery.Event": "https://api.jquery.com/category/events/event-object/",
|
||||||
|
"mw.Map": "https://doc.wikimedia.org/mediawiki-core/master/js/mw.Map.html",
|
||||||
|
"mw.Title": "https://doc.wikimedia.org/mediawiki-core/master/js/mw.Title.html",
|
||||||
|
"mw.user": "https://doc.wikimedia.org/mediawiki-core/master/js/mw.user.html",
|
||||||
"OO.EventEmitter": "https://doc.wikimedia.org/oojs/master/OO.EventEmitter.html",
|
"OO.EventEmitter": "https://doc.wikimedia.org/oojs/master/OO.EventEmitter.html",
|
||||||
"OO.ui.ComboBoxInputWidget": "https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.ComboBoxInputWidget.html",
|
"OO.ui.ComboBoxInputWidget": "https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.ComboBoxInputWidget.html",
|
||||||
"OO.ui.OptionWidget": "https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.OptionWidget.html",
|
"OO.ui.OptionWidget": "https://doc.wikimedia.org/oojs-ui/master/js/OO.ui.OptionWidget.html",
|
||||||
|
|
11
modules/.eslintrc.json
Normal file
11
modules/.eslintrc.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"wikimedia/client-es6",
|
||||||
|
"wikimedia/jquery",
|
||||||
|
"wikimedia/mediawiki"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"max-len": "off"
|
||||||
|
}
|
||||||
|
}
|
5
modules/ext.cite.referencePreviews/.eslintrc.json
Normal file
5
modules/ext.cite.referencePreviews/.eslintrc.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"commonjs": true
|
||||||
|
}
|
||||||
|
}
|
1
modules/ext.cite.referencePreviews/OWNERS.md
Normal file
1
modules/ext.cite.referencePreviews/OWNERS.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Code in this folder and subfolders is maintained by WMDE.
|
4
modules/ext.cite.referencePreviews/constants.js
Normal file
4
modules/ext.cite.referencePreviews/constants.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
TYPE_REFERENCE: 'reference',
|
||||||
|
FETCH_DELAY_REFERENCE_TYPE: 150
|
||||||
|
};
|
94
modules/ext.cite.referencePreviews/createReferenceGateway.js
Normal file
94
modules/ext.cite.referencePreviews/createReferenceGateway.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* @module gateway/reference
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { TYPE_REFERENCE } = require( './constants.js' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Gateway}
|
||||||
|
*/
|
||||||
|
module.exports = function createReferenceGateway() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @return {HTMLElement}
|
||||||
|
*/
|
||||||
|
function scrapeReferenceText( id ) {
|
||||||
|
const idSelector = `#${ CSS.escape( id ) }`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same alternative selectors with and without mw-… as in the RESTbased endpoint.
|
||||||
|
*
|
||||||
|
* @see https://phabricator.wikimedia.org/diffusion/GMOA/browse/master/lib/transformations/references/structureReferenceListContent.js$138
|
||||||
|
*/
|
||||||
|
return document.querySelector( `${ idSelector } .mw-reference-text, ${ idSelector } .reference-text` );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to find a single reference type identifier, limited to a list of known types.
|
||||||
|
* - When a `class="…"` attribute mentions multiple known types, the last one is used, following
|
||||||
|
* CSS semantics.
|
||||||
|
* - When there are multiple <cite> tags, the first with a known type is used.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} referenceText
|
||||||
|
* @return {string|null}
|
||||||
|
*/
|
||||||
|
function scrapeReferenceType( referenceText ) {
|
||||||
|
const KNOWN_TYPES = [ 'book', 'journal', 'news', 'note', 'web' ];
|
||||||
|
let type = null;
|
||||||
|
const citeTags = referenceText.querySelectorAll( 'cite[class]' );
|
||||||
|
Array.prototype.forEach.call( citeTags, ( element ) => {
|
||||||
|
// don't need to keep scanning if one is found.
|
||||||
|
if ( type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const classNames = element.className.split( /\s+/ );
|
||||||
|
for ( let i = classNames.length; i--; ) {
|
||||||
|
if ( KNOWN_TYPES.indexOf( classNames[ i ] ) !== -1 ) {
|
||||||
|
type = classNames[ i ];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {mw.Title} title
|
||||||
|
* @param {HTMLAnchorElement} el
|
||||||
|
* @return {Promise<ext.popups.PreviewModel>}
|
||||||
|
*/
|
||||||
|
function fetchPreviewForTitle( title, el ) {
|
||||||
|
// Need to encode the fragment again as mw.Title returns it as decoded text
|
||||||
|
const id = title.getFragment().replace( / /g, '_' ),
|
||||||
|
referenceNode = scrapeReferenceText( id );
|
||||||
|
|
||||||
|
if ( !referenceNode ||
|
||||||
|
// Skip references that don't contain anything but whitespace, e.g. a single
|
||||||
|
( !referenceNode.textContent.trim() && !referenceNode.children.length )
|
||||||
|
) {
|
||||||
|
return Promise.reject(
|
||||||
|
// Required to set `showNullPreview` to false and not open an error popup
|
||||||
|
{ textStatus: 'abort', textContext: 'Footnote not found or empty', xhr: { readyState: 0 } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = {
|
||||||
|
url: `#${ id }`,
|
||||||
|
extract: referenceNode.innerHTML,
|
||||||
|
type: TYPE_REFERENCE,
|
||||||
|
referenceType: scrapeReferenceType( referenceNode ),
|
||||||
|
// Note: Even the top-most HTMLHtmlElement is guaranteed to have a parent.
|
||||||
|
sourceElementId: el.parentNode.id
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make promise abortable.
|
||||||
|
const promise = Promise.resolve( model );
|
||||||
|
promise.abort = () => {};
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchPreviewForTitle
|
||||||
|
};
|
||||||
|
};
|
191
modules/ext.cite.referencePreviews/createReferencePreview.js
Normal file
191
modules/ext.cite.referencePreviews/createReferencePreview.js
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/**
|
||||||
|
* @module referencePreview
|
||||||
|
*/
|
||||||
|
const { isTrackingEnabled, LOGGING_SCHEMA } = require( './referencePreviewsInstrumentation.js' );
|
||||||
|
|
||||||
|
const TEMPLATE = document.createElement( 'template' );
|
||||||
|
TEMPLATE.innerHTML = `
|
||||||
|
<div class="mwe-popups mwe-popups mwe-popups-type-reference" aria-hidden>
|
||||||
|
<div class="mwe-popups-container">
|
||||||
|
<div class="mwe-popups-extract">
|
||||||
|
<div class="mwe-popups-scroll">
|
||||||
|
<strong class="mwe-popups-title">
|
||||||
|
<span class="popups-icon"></span>
|
||||||
|
<span class="mwe-popups-title-placeholder"></span>
|
||||||
|
</strong>
|
||||||
|
<bdi><div class="mw-parser-output"></div></bdi>
|
||||||
|
</div>
|
||||||
|
<div class="mwe-popups-fade"></div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="mwe-popups-settings"></div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} node
|
||||||
|
* @param {HTMLElement|string} htmlOrOtherNode
|
||||||
|
*/
|
||||||
|
const replaceWith = ( node, htmlOrOtherNode ) => {
|
||||||
|
if ( typeof htmlOrOtherNode === 'string' ) {
|
||||||
|
node.insertAdjacentHTML( 'afterend', htmlOrOtherNode );
|
||||||
|
} else {
|
||||||
|
node.parentNode.appendChild( htmlOrOtherNode );
|
||||||
|
}
|
||||||
|
node.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ext.popups.PreviewModel} model
|
||||||
|
* @return {jQuery}
|
||||||
|
*/
|
||||||
|
function renderReferencePreview(
|
||||||
|
model
|
||||||
|
) {
|
||||||
|
const type = model.referenceType || 'generic';
|
||||||
|
// The following messages are used here:
|
||||||
|
// * popups-refpreview-book
|
||||||
|
// * popups-refpreview-journal
|
||||||
|
// * popups-refpreview-news
|
||||||
|
// * popups-refpreview-note
|
||||||
|
// * popups-refpreview-web
|
||||||
|
let titleMsg = mw.message( `popups-refpreview-${ type }` );
|
||||||
|
if ( !titleMsg.exists() ) {
|
||||||
|
titleMsg = mw.message( 'popups-refpreview-reference' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = TEMPLATE.content.cloneNode( true ).children[ 0 ];
|
||||||
|
|
||||||
|
replaceWith(
|
||||||
|
el.querySelector( '.mwe-popups-title-placeholder' ),
|
||||||
|
mw.html.escape( titleMsg.text() )
|
||||||
|
);
|
||||||
|
// The following classes are used here:
|
||||||
|
// * popups-icon--reference-generic
|
||||||
|
// * popups-icon--reference-book
|
||||||
|
// * popups-icon--reference-journal
|
||||||
|
// * popups-icon--reference-news
|
||||||
|
// * popups-icon--reference-note
|
||||||
|
// * popups-icon--reference-web
|
||||||
|
el.querySelector( '.mwe-popups-title .popups-icon' )
|
||||||
|
.classList.add( `popups-icon--reference-${ type }` );
|
||||||
|
el.querySelector( '.mw-parser-output' )
|
||||||
|
.innerHTML = model.extract;
|
||||||
|
|
||||||
|
// Make sure to not destroy existing targets, if any
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
el.querySelectorAll( '.mwe-popups-extract a[href][class~="external"]:not([target])' ),
|
||||||
|
( a ) => {
|
||||||
|
a.target = '_blank';
|
||||||
|
// Don't let the external site access and possibly manipulate window.opener.location
|
||||||
|
a.rel = `${ a.rel ? `${ a.rel } ` : '' }noopener`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// We assume elements that benefit from being collapsible are to large for the popup
|
||||||
|
Array.prototype.forEach.call( el.querySelectorAll( '.mw-collapsible' ), ( node ) => {
|
||||||
|
const otherNode = document.createElement( 'div' );
|
||||||
|
otherNode.classList.add( 'mwe-collapsible-placeholder' );
|
||||||
|
const icon = document.createElement( 'span' );
|
||||||
|
icon.classList.add( 'popups-icon', 'popups-icon--infoFilled' );
|
||||||
|
const label = document.createElement( 'span' );
|
||||||
|
label.classList.add( 'mwe-collapsible-placeholder-label' );
|
||||||
|
label.textContent = mw.msg( 'popups-refpreview-collapsible-placeholder' );
|
||||||
|
otherNode.appendChild( icon );
|
||||||
|
otherNode.appendChild( label );
|
||||||
|
replaceWith( node, otherNode );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Undo remaining effects from the jquery.tablesorter.js plugin
|
||||||
|
const undoHeaderSort = ( headerSort ) => {
|
||||||
|
headerSort.classList.remove( 'headerSort' );
|
||||||
|
headerSort.removeAttribute( 'tabindex' );
|
||||||
|
headerSort.removeAttribute( 'title' );
|
||||||
|
};
|
||||||
|
Array.prototype.forEach.call( el.querySelectorAll( 'table.sortable' ), ( node ) => {
|
||||||
|
node.classList.remove( 'sortable', 'jquery-tablesorter' );
|
||||||
|
Array.prototype.forEach.call( node.querySelectorAll( '.headerSort' ), undoHeaderSort );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// TODO: Do not remove this but move it up into the templateHTML constant!
|
||||||
|
const settingsButton = document.createElement( 'a' );
|
||||||
|
settingsButton.classList.add( 'cdx-button', 'cdx-button--fake-button', 'cdx-button--fake-button--enabled', 'cdx-button--weight-quiet', 'cdx-button--icon-only', 'mwe-popups-settings-button' );
|
||||||
|
const settingsIcon = document.createElement( 'span' );
|
||||||
|
settingsIcon.classList.add( 'popups-icon', 'popups-icon--size-small', 'popups-icon--settings' );
|
||||||
|
const settingsButtonLabel = document.createElement( 'span' );
|
||||||
|
settingsButtonLabel.textContent = mw.msg( 'popups-settings-icon-gear-title' );
|
||||||
|
settingsButton.append( settingsIcon );
|
||||||
|
settingsButton.append( settingsButtonLabel );
|
||||||
|
el.querySelector( '.mwe-popups-settings' ).appendChild( settingsButton );
|
||||||
|
|
||||||
|
if ( isTrackingEnabled() ) {
|
||||||
|
el.querySelector( '.mw-parser-output' ).addEventListener( 'click', ( ev ) => {
|
||||||
|
if ( !ev.target.matches( 'a' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mw.track( LOGGING_SCHEMA, {
|
||||||
|
action: 'clickedReferencePreviewsContentLink'
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
el.querySelector( '.mwe-popups-scroll' ).addEventListener( 'scroll', function ( e ) {
|
||||||
|
const element = e.target,
|
||||||
|
// We are dealing with floating point numbers here when the page is zoomed!
|
||||||
|
scrolledToBottom = element.scrollTop >= element.scrollHeight - element.clientHeight - 1;
|
||||||
|
|
||||||
|
if ( isTrackingEnabled() ) {
|
||||||
|
if ( !element.isOpenRecorded ) {
|
||||||
|
mw.track( LOGGING_SCHEMA, {
|
||||||
|
action: 'poppedOpen',
|
||||||
|
scrollbarsPresent: element.scrollHeight > element.clientHeight
|
||||||
|
} );
|
||||||
|
element.isOpenRecorded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
element.scrollTop > 0 &&
|
||||||
|
!element.isScrollRecorded
|
||||||
|
) {
|
||||||
|
mw.track( LOGGING_SCHEMA, {
|
||||||
|
action: 'scrolled'
|
||||||
|
} );
|
||||||
|
element.isScrollRecorded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !scrolledToBottom && element.isScrolling ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extract = element.parentNode,
|
||||||
|
hasHorizontalScroll = element.scrollWidth > element.clientWidth,
|
||||||
|
scrollbarHeight = element.offsetHeight - element.clientHeight,
|
||||||
|
hasVerticalScroll = element.scrollHeight > element.clientHeight,
|
||||||
|
scrollbarWidth = element.offsetWidth - element.clientWidth;
|
||||||
|
const fade = extract.querySelector( '.mwe-popups-fade' );
|
||||||
|
fade.style.bottom = hasHorizontalScroll ? `${ scrollbarHeight }px` : 0;
|
||||||
|
fade.style.right = hasVerticalScroll ? `${ scrollbarWidth }px` : 0;
|
||||||
|
|
||||||
|
element.isScrolling = !scrolledToBottom;
|
||||||
|
extract.classList.toggle( 'mwe-popups-fade-out', element.isScrolling );
|
||||||
|
extract.setAttribute( 'lang', mw.config.get( 'wgPageContentLanguage' ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ext.popups.PreviewModel} model
|
||||||
|
* @return {ext.popups.Preview}
|
||||||
|
*/
|
||||||
|
function createReferencePreview( model ) {
|
||||||
|
return {
|
||||||
|
el: renderReferencePreview( model ),
|
||||||
|
hasThumbnail: false,
|
||||||
|
isTall: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createReferencePreview;
|
46
modules/ext.cite.referencePreviews/index.js
Normal file
46
modules/ext.cite.referencePreviews/index.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const isReferencePreviewsEnabled = require( './isReferencePreviewsEnabled.js' );
|
||||||
|
const { initReferencePreviewsInstrumentation, LOGGING_SCHEMA } = require( './referencePreviewsInstrumentation.js' );
|
||||||
|
const createReferenceGateway = require( './createReferenceGateway.js' );
|
||||||
|
const renderFn = require( './createReferencePreview.js' );
|
||||||
|
const { TYPE_REFERENCE, FETCH_DELAY_REFERENCE_TYPE } = require( './constants.js' );
|
||||||
|
const setUserConfigFlags = require( './setUserConfigFlags.js' );
|
||||||
|
|
||||||
|
setUserConfigFlags( mw.config );
|
||||||
|
const referencePreviewsState = isReferencePreviewsEnabled(
|
||||||
|
mw.user,
|
||||||
|
mw.popups.isEnabled,
|
||||||
|
mw.config
|
||||||
|
);
|
||||||
|
const gateway = createReferenceGateway();
|
||||||
|
|
||||||
|
// For tracking baseline stats in the Cite extension https://phabricator.wikimedia.org/T353798
|
||||||
|
// FIXME: This might be obsolete when the code moves to the Cite extension and the tracking there
|
||||||
|
// can check that state differently.
|
||||||
|
mw.config.set( 'wgPopupsReferencePreviewsVisible', !!referencePreviewsState );
|
||||||
|
|
||||||
|
mw.trackSubscribe( 'Popups.SettingChange', ( data ) => {
|
||||||
|
if ( data.previewType === TYPE_REFERENCE ) {
|
||||||
|
mw.track( LOGGING_SCHEMA, data );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
module.exports = referencePreviewsState !== null ? {
|
||||||
|
type: TYPE_REFERENCE,
|
||||||
|
selector: '#mw-content-text .reference a[ href*="#" ]',
|
||||||
|
delay: FETCH_DELAY_REFERENCE_TYPE,
|
||||||
|
gateway,
|
||||||
|
renderFn,
|
||||||
|
init: () => {
|
||||||
|
initReferencePreviewsInstrumentation();
|
||||||
|
}
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
// Expose private methods for QUnit tests
|
||||||
|
if ( typeof QUnit !== 'undefined' ) {
|
||||||
|
module.exports = { private: {
|
||||||
|
createReferenceGateway: require( './createReferenceGateway.js' ),
|
||||||
|
createReferencePreview: require( './createReferencePreview.js' ),
|
||||||
|
isReferencePreviewsEnabled: require( './isReferencePreviewsEnabled.js' ),
|
||||||
|
setUserConfigFlags: require( './setUserConfigFlags.js' )
|
||||||
|
} };
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
const { TYPE_REFERENCE } = require( './constants.js' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module isReferencePreviewsEnabled
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the global state of the application, creates a function that gets
|
||||||
|
* whether or not the user should have Reference Previews enabled.
|
||||||
|
*
|
||||||
|
* @param {mw.user} user The `mw.user` singleton instance
|
||||||
|
* @param {Function} isPreviewTypeEnabled check whether preview has been disabled or enabled.
|
||||||
|
* @param {mw.Map} config
|
||||||
|
*
|
||||||
|
* @return {boolean|null} Null when there is no way the popup type can be enabled at run-time.
|
||||||
|
*/
|
||||||
|
function isReferencePreviewsEnabled( user, isPreviewTypeEnabled, config ) {
|
||||||
|
// TODO: This and the final `mw.user.options` check are currently redundant. Only this here
|
||||||
|
// should be removed when the wgPopupsReferencePreviews feature flag is not needed any more.
|
||||||
|
if ( !config.get( 'wgPopupsReferencePreviews' ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// T265872: Unavailable when in conflict with (one of the) reference tooltips gadgets.
|
||||||
|
if ( config.get( 'wgPopupsConflictsWithRefTooltipsGadget' ) ||
|
||||||
|
config.get( 'wgPopupsConflictsWithNavPopupGadget' ) ||
|
||||||
|
// T243822: Temporarily disabled in the mobile skin
|
||||||
|
config.get( 'skin' ) === 'minerva'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( user.isAnon() ) {
|
||||||
|
return isPreviewTypeEnabled( TYPE_REFERENCE );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registered users never can enable popup types at run-time.
|
||||||
|
return user.options.get( 'popups-reference-previews' ) === '1' ? true : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = isReferencePreviewsEnabled;
|
89
modules/ext.cite.referencePreviews/referencePreview.less
Normal file
89
modules/ext.cite.referencePreviews/referencePreview.less
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
@import 'mediawiki.skin.variables.less';
|
||||||
|
|
||||||
|
// Should be in sync with Popups/src/ui/variables.less
|
||||||
|
@popupPadding: 16px;
|
||||||
|
@lineHeight: 20px;
|
||||||
|
|
||||||
|
/* stylelint-disable selector-class-pattern */
|
||||||
|
.popups-icon--reference-generic {
|
||||||
|
.cdx-mixin-css-icon( @cdx-icon-reference );
|
||||||
|
}
|
||||||
|
|
||||||
|
.popups-icon--reference-book {
|
||||||
|
.cdx-mixin-css-icon( @cdx-icon-book );
|
||||||
|
}
|
||||||
|
|
||||||
|
.popups-icon--reference-journal {
|
||||||
|
.cdx-mixin-css-icon( @cdx-icon-journal );
|
||||||
|
}
|
||||||
|
|
||||||
|
.popups-icon--reference-news {
|
||||||
|
.cdx-mixin-css-icon( @cdx-icon-newspaper );
|
||||||
|
}
|
||||||
|
|
||||||
|
.popups-icon--reference-web {
|
||||||
|
.cdx-mixin-css-icon( @cdx-icon-browser );
|
||||||
|
}
|
||||||
|
|
||||||
|
.popups-icon--preview-disambiguation {
|
||||||
|
.cdx-mixin-css-icon( @cdx-icon-articles );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't do any event bubbling on childs like <a><span>[</span>2]</a>
|
||||||
|
// see https://phabricator.wikimedia.org/T214693
|
||||||
|
/* stylelint-disable-next-line selector-max-id */
|
||||||
|
#mw-content-text .reference a[ href*='#' ] * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwe-popups.mwe-popups-type-reference .mwe-popups-container {
|
||||||
|
.mwe-popups-title .popups-icon--reference-note {
|
||||||
|
// There is currently no "reference-note" icon specified in extension.json
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwe-popups-extract {
|
||||||
|
margin-right: 0;
|
||||||
|
max-height: inherit;
|
||||||
|
|
||||||
|
.mwe-popups-scroll {
|
||||||
|
// This is how the @previewFooterHeight in popup.less is calculated
|
||||||
|
@marginBottom: @popupPadding + 34px;
|
||||||
|
// Same as @previewPointerHeight in popup.less
|
||||||
|
@pointerHeight: 8px;
|
||||||
|
max-height: 401px - @popupPadding - @marginBottom + @pointerHeight;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: @popupPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mw-parser-output {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the default fade-out effect set by popup.less
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwe-popups-fade {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: @lineHeight;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: linear-gradient( rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) );
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none; // Allows clicking "through" the element
|
||||||
|
transition: opacity 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mwe-popups-fade-out .mwe-popups-fade {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mwe-collapsible-placeholder {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1em 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
let isTracking = false;
|
||||||
|
|
||||||
|
const LOGGING_SCHEMA = 'event.ReferencePreviewsPopups';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run once the preview is initialized.
|
||||||
|
*/
|
||||||
|
function initReferencePreviewsInstrumentation() {
|
||||||
|
if ( mw.config.get( 'wgPopupsReferencePreviews' ) &&
|
||||||
|
navigator.sendBeacon &&
|
||||||
|
mw.config.get( 'wgIsArticle' ) &&
|
||||||
|
!isTracking
|
||||||
|
) {
|
||||||
|
isTracking = true;
|
||||||
|
mw.track( LOGGING_SCHEMA, { action: 'pageview' } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTrackingEnabled() {
|
||||||
|
return isTracking;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LOGGING_SCHEMA,
|
||||||
|
initReferencePreviewsInstrumentation,
|
||||||
|
isTrackingEnabled
|
||||||
|
};
|
29
modules/ext.cite.referencePreviews/setUserConfigFlags.js
Normal file
29
modules/ext.cite.referencePreviews/setUserConfigFlags.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* @module setUserConfigFlags
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as in includes/PopupsContext.php
|
||||||
|
*/
|
||||||
|
const REF_TOOLTIPS_ENABLED = 2,
|
||||||
|
REFERENCE_PREVIEWS_ENABLED = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the bitmask that represents preferences to the related config options.
|
||||||
|
*
|
||||||
|
* @param {mw.Map} config
|
||||||
|
*/
|
||||||
|
module.exports = function setUserConfigFlags( config ) {
|
||||||
|
const popupsFlags = parseInt( config.get( 'wgPopupsFlags' ), 10 );
|
||||||
|
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
config.set(
|
||||||
|
'wgPopupsConflictsWithRefTooltipsGadget',
|
||||||
|
!!( popupsFlags & REF_TOOLTIPS_ENABLED )
|
||||||
|
);
|
||||||
|
config.set(
|
||||||
|
'wgPopupsReferencePreviews',
|
||||||
|
!!( popupsFlags & REFERENCE_PREVIEWS_ENABLED )
|
||||||
|
);
|
||||||
|
/* eslint-enable no-bitwise */
|
||||||
|
};
|
|
@ -14,6 +14,8 @@ use MediaWiki\EditPage\EditPage;
|
||||||
use MediaWiki\Hook\EditPage__showEditForm_initialHook;
|
use MediaWiki\Hook\EditPage__showEditForm_initialHook;
|
||||||
use MediaWiki\Output\OutputPage;
|
use MediaWiki\Output\OutputPage;
|
||||||
use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
|
use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
|
||||||
|
use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
|
||||||
|
use MediaWiki\ResourceLoader\ResourceLoader;
|
||||||
use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook;
|
use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook;
|
||||||
use MediaWiki\Title\Title;
|
use MediaWiki\Title\Title;
|
||||||
use MediaWiki\User\Options\UserOptionsLookup;
|
use MediaWiki\User\Options\UserOptionsLookup;
|
||||||
|
@ -25,6 +27,7 @@ use MediaWiki\User\Options\UserOptionsLookup;
|
||||||
class CiteHooks implements
|
class CiteHooks implements
|
||||||
ContentHandlerDefaultModelForHook,
|
ContentHandlerDefaultModelForHook,
|
||||||
ResourceLoaderGetConfigVarsHook,
|
ResourceLoaderGetConfigVarsHook,
|
||||||
|
ResourceLoaderRegisterModulesHook,
|
||||||
APIQuerySiteInfoGeneralInfoHook,
|
APIQuerySiteInfoGeneralInfoHook,
|
||||||
EditPage__showEditForm_initialHook
|
EditPage__showEditForm_initialHook
|
||||||
{
|
{
|
||||||
|
@ -67,6 +70,36 @@ class CiteHooks implements
|
||||||
$vars['wgCiteBookReferencing'] = $config->get( 'CiteBookReferencing' );
|
$vars['wgCiteBookReferencing'] = $config->get( 'CiteBookReferencing' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules
|
||||||
|
*/
|
||||||
|
public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
|
||||||
|
if ( ExtensionRegistry::getInstance()->isLoaded( 'Popups' ) ) {
|
||||||
|
$dir = dirname( __DIR__, 2 ) . '/modules/';
|
||||||
|
$resourceLoader->register( [
|
||||||
|
'ext.cite.referencePreviews' => [
|
||||||
|
'localBasePath' => $dir . '/ext.cite.referencePreviews',
|
||||||
|
'remoteExtPath' => 'Cite/modules/ext.cite.referencePreviews',
|
||||||
|
'dependencies' => [
|
||||||
|
'ext.popups.main',
|
||||||
|
],
|
||||||
|
'styles' => [
|
||||||
|
'referencePreview.less',
|
||||||
|
],
|
||||||
|
'packageFiles' => [
|
||||||
|
'index.js',
|
||||||
|
'constants.js',
|
||||||
|
'createReferenceGateway.js',
|
||||||
|
'createReferencePreview.js',
|
||||||
|
'isReferencePreviewsEnabled.js',
|
||||||
|
'referencePreviewsInstrumentation.js',
|
||||||
|
'setUserConfigFlags.js'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook: APIQuerySiteInfoGeneralInfo
|
* Hook: APIQuerySiteInfoGeneralInfo
|
||||||
*
|
*
|
||||||
|
|
24
src/ResourceLoader/OptionalLoader.php
Normal file
24
src/ResourceLoader/OptionalLoader.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Cite\ResourceLoader;
|
||||||
|
|
||||||
|
use ExtensionRegistry;
|
||||||
|
use MediaWiki\ResourceLoader\FileModule;
|
||||||
|
use MediaWiki\ResourceLoader\Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
class OptionalLoader {
|
||||||
|
public static function addOptionalDependencies( array $info ): Module {
|
||||||
|
// Copied from DiscussionTools
|
||||||
|
$extensionRegistry = ExtensionRegistry::getInstance();
|
||||||
|
foreach ( $info['optionalDependencies'] as $ext => $deps ) {
|
||||||
|
if ( $extensionRegistry->isLoaded( $ext ) ) {
|
||||||
|
$info['dependencies'] = array_merge( $info['dependencies'], (array)$deps );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$class = $info['class'] ?? FileModule::class;
|
||||||
|
return new $class( $info );
|
||||||
|
}
|
||||||
|
}
|
16
tests/qunit/.eslintrc.json
Normal file
16
tests/qunit/.eslintrc.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"../../modules/.eslintrc.json",
|
||||||
|
"wikimedia/qunit"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"commonjs": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-jquery/no-class-state": "off"
|
||||||
|
}
|
||||||
|
}
|
1
tests/qunit/ext.cite.referencePreviews/OWNERS.md
Normal file
1
tests/qunit/ext.cite.referencePreviews/OWNERS.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Code in this folder and subfolders is maintained by WMDE.
|
|
@ -0,0 +1,179 @@
|
||||||
|
function createStubTitle( fragment = null ) {
|
||||||
|
return {
|
||||||
|
getFragment() {
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
( mw.loader.getModuleNames().indexOf( 'ext.popups.main' ) !== -1 ?
|
||||||
|
QUnit.module :
|
||||||
|
QUnit.module.skip )( 'ext.cite.referencePreviews#createReferenceGateway', {
|
||||||
|
beforeEach() {
|
||||||
|
// FIXME: Is this needed?
|
||||||
|
// global.CSS = {
|
||||||
|
// escape: ( str ) => $.escapeSelector( str )
|
||||||
|
// };
|
||||||
|
mw.msg = ( key ) => `<${ key }>`;
|
||||||
|
mw.message = ( key ) => {
|
||||||
|
return { exists: () => !key.endsWith( 'generic' ), text: () => `<${ key }>` };
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$sourceElement = $( '<a>' ).appendTo(
|
||||||
|
$( '<sup>' ).attr( 'id', 'cite_ref-1' ).appendTo( document.body )
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$references = $( '<ul>' ).append(
|
||||||
|
$( '<li>' ).attr( 'id', 'cite_note-1' ).append(
|
||||||
|
$( '<span>' ).addClass( 'mw-reference-text' ).text( 'Footnote 1' )
|
||||||
|
),
|
||||||
|
$( '<li>' ).attr( 'id', 'cite_note-2' ).append(
|
||||||
|
$( '<span>' ).addClass( 'reference-text' ).append(
|
||||||
|
$( '<cite>' ).addClass( 'journal web unknown' ).text( 'Footnote 2' )
|
||||||
|
)
|
||||||
|
),
|
||||||
|
$( '<li>' ).attr( 'id', 'cite_note-3' ).append(
|
||||||
|
$( '<span>' ).addClass( 'reference-text' ).append(
|
||||||
|
$( '<cite>' ).addClass( 'news' ).text( 'Footnote 3' ),
|
||||||
|
$( '<cite>' ).addClass( 'news citation' ),
|
||||||
|
$( '<cite>' ).addClass( 'citation' )
|
||||||
|
)
|
||||||
|
),
|
||||||
|
$( '<li>' ).attr( 'id', 'cite_note-4' ).append(
|
||||||
|
$( '<span>' ).addClass( 'reference-text' ).append(
|
||||||
|
$( '<cite>' ).addClass( 'news' ).text( 'Footnote 4' ),
|
||||||
|
$( '<cite>' ).addClass( 'web' )
|
||||||
|
)
|
||||||
|
),
|
||||||
|
$( '<li>' ).attr( 'id', 'cite_note-5' ).append(
|
||||||
|
$( '<span>' ).addClass( 'mw-reference-text' ).html( ' ' )
|
||||||
|
)
|
||||||
|
).appendTo( document.body );
|
||||||
|
},
|
||||||
|
afterEach() {
|
||||||
|
mw.msg = null;
|
||||||
|
mw.message = null;
|
||||||
|
this.$sourceElement.parent().remove();
|
||||||
|
this.$references.remove();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway returns the correct data', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-1' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( ( result ) => {
|
||||||
|
assert.propEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
url: '#cite_note-1',
|
||||||
|
extract: 'Footnote 1',
|
||||||
|
type: 'reference',
|
||||||
|
referenceType: null,
|
||||||
|
sourceElementId: 'cite_ref-1'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway accepts alternative text node class name', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-2' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( ( result ) => {
|
||||||
|
assert.propEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
url: '#cite_note-2',
|
||||||
|
extract: '<cite class="journal web unknown">Footnote 2</cite>',
|
||||||
|
type: 'reference',
|
||||||
|
referenceType: 'web',
|
||||||
|
sourceElementId: 'cite_ref-1'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway accepts duplicated types', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-3' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( ( result ) => {
|
||||||
|
assert.propEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
url: '#cite_note-3',
|
||||||
|
extract: '<cite class="news">Footnote 3</cite><cite class="news citation"></cite><cite class="citation"></cite>',
|
||||||
|
type: 'reference',
|
||||||
|
referenceType: 'news',
|
||||||
|
sourceElementId: 'cite_ref-1'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway ignores conflicting types', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-4' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( ( result ) => {
|
||||||
|
assert.propEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
url: '#cite_note-4',
|
||||||
|
extract: '<cite class="news">Footnote 4</cite><cite class="web"></cite>',
|
||||||
|
type: 'reference',
|
||||||
|
referenceType: 'news',
|
||||||
|
sourceElementId: 'cite_ref-1'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway returns source element id', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-1' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( ( result ) => {
|
||||||
|
assert.propEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
url: '#cite_note-1',
|
||||||
|
extract: 'Footnote 1',
|
||||||
|
type: 'reference',
|
||||||
|
referenceType: null,
|
||||||
|
sourceElementId: 'cite_ref-1'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway rejects non-existing references', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'undefined' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( () => {
|
||||||
|
assert.true( false, 'It should not resolve' );
|
||||||
|
} ).catch( ( result ) => {
|
||||||
|
assert.propEqual( result, { textStatus: 'abort', textContext: 'Footnote not found or empty', xhr: { readyState: 0 } } );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway rejects all-whitespace references', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-5' );
|
||||||
|
|
||||||
|
return gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] ).then( () => {
|
||||||
|
assert.true( false, 'It should not resolve' );
|
||||||
|
} ).catch( ( result ) => {
|
||||||
|
assert.propEqual( result, { textStatus: 'abort', textContext: 'Footnote not found or empty', xhr: { readyState: 0 } } );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'Reference preview gateway is abortable', function ( assert ) {
|
||||||
|
const gateway = require( 'ext.cite.referencePreviews' ).private.createReferenceGateway(),
|
||||||
|
title = createStubTitle( 'cite note-1' ),
|
||||||
|
promise = gateway.fetchPreviewForTitle( title, this.$sourceElement[ 0 ] );
|
||||||
|
|
||||||
|
assert.strictEqual( typeof promise.abort, 'function' );
|
||||||
|
} );
|
|
@ -0,0 +1,221 @@
|
||||||
|
function createStubUserSettings( expectEnabled ) {
|
||||||
|
return {
|
||||||
|
isPreviewTypeEnabled() {
|
||||||
|
return expectEnabled !== false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStubUser( isAnon, options ) {
|
||||||
|
return {
|
||||||
|
isNamed() {
|
||||||
|
return !isAnon;
|
||||||
|
},
|
||||||
|
isAnon() {
|
||||||
|
return isAnon;
|
||||||
|
},
|
||||||
|
options
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { get: () => '1' };
|
||||||
|
|
||||||
|
( mw.loader.getModuleNames().indexOf( 'ext.popups.main' ) !== -1 ?
|
||||||
|
QUnit.module :
|
||||||
|
QUnit.module.skip )( 'ext.cite.referencePreviews#isReferencePreviewsEnabled' );
|
||||||
|
|
||||||
|
QUnit.test( 'all relevant combinations of flags', ( assert ) => {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
testCase: 'enabled for an anonymous user',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: true,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: false,
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'turned off via the feature flag (anonymous user)',
|
||||||
|
wgPopupsReferencePreviews: false,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: true,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'not available because of a conflicting gadget (anonymous user)',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: true,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: true,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'not available in the mobile skin (anonymous user)',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: true,
|
||||||
|
isAnon: true,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'manually disabled by the anonymous user',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: true,
|
||||||
|
enabledByAnon: false,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'enabled for a registered user',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: false,
|
||||||
|
enabledByAnon: false,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'turned off via the feature flag (registered user)',
|
||||||
|
wgPopupsReferencePreviews: false,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: false,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'not available because of a conflicting gadget (registered user)',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: true,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: false,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testCase: 'not available in the mobile skin (registered user)',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: true,
|
||||||
|
isAnon: false,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: true,
|
||||||
|
expected: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO: This combination will make much more sense when the server-side
|
||||||
|
// wgPopupsReferencePreviews flag doesn't include the user's setting any more
|
||||||
|
testCase: 'manually disabled by the registered user',
|
||||||
|
wgPopupsReferencePreviews: true,
|
||||||
|
wgPopupsConflictsWithRefTooltipsGadget: false,
|
||||||
|
isMobile: false,
|
||||||
|
isAnon: false,
|
||||||
|
enabledByAnon: true,
|
||||||
|
enabledByRegistered: false,
|
||||||
|
expected: null
|
||||||
|
}
|
||||||
|
].forEach( ( data ) => {
|
||||||
|
const user = {
|
||||||
|
isNamed: () => !data.isAnon && !data.isIPMasked,
|
||||||
|
isAnon: () => data.isAnon,
|
||||||
|
options: {
|
||||||
|
get: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isPreviewTypeEnabled = () => {
|
||||||
|
if ( !data.isAnon ) {
|
||||||
|
assert.true( false, 'not expected to be called' );
|
||||||
|
}
|
||||||
|
return data.enabledByAnon;
|
||||||
|
},
|
||||||
|
config = {
|
||||||
|
get: ( key ) => key === 'skin' && data.isMobile ? 'minerva' : data[ key ]
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( data.isAnon ) {
|
||||||
|
user.options.get = () => assert.true( false, 'not expected to be called 2' );
|
||||||
|
} else {
|
||||||
|
user.options.get = () => data.enabledByRegistered ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.isReferencePreviewsEnabled( user, isPreviewTypeEnabled, config ),
|
||||||
|
data.expected,
|
||||||
|
data.testCase
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'it should display reference previews when conditions are fulfilled', ( assert ) => {
|
||||||
|
const user = createStubUser( false, options ),
|
||||||
|
userSettings = createStubUserSettings( false ),
|
||||||
|
config = new Map();
|
||||||
|
|
||||||
|
config.set( 'wgPopupsReferencePreviews', true );
|
||||||
|
config.set( 'wgPopupsConflictsWithRefTooltipsGadget', false );
|
||||||
|
|
||||||
|
assert.true(
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.isReferencePreviewsEnabled( user, userSettings, config ),
|
||||||
|
'If the user is logged in and the user is in the on group, then it\'s enabled.'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'it should handle the conflict with the Reference Tooltips Gadget', ( assert ) => {
|
||||||
|
const user = createStubUser( false ),
|
||||||
|
userSettings = createStubUserSettings( false ),
|
||||||
|
config = new Map();
|
||||||
|
|
||||||
|
config.set( 'wgPopupsReferencePreviews', true );
|
||||||
|
config.set( 'wgPopupsConflictsWithRefTooltipsGadget', true );
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.isReferencePreviewsEnabled( user, userSettings, config ),
|
||||||
|
null,
|
||||||
|
'Reference Previews is disabled.'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'it should not be enabled when the global is disabling it', ( assert ) => {
|
||||||
|
const user = createStubUser( false ),
|
||||||
|
userSettings = createStubUserSettings( false ),
|
||||||
|
config = new Map();
|
||||||
|
|
||||||
|
config.set( 'wgPopupsReferencePreviews', false );
|
||||||
|
config.set( 'wgPopupsConflictsWithRefTooltipsGadget', false );
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.isReferencePreviewsEnabled( user, userSettings, config ),
|
||||||
|
null,
|
||||||
|
'Reference Previews is disabled.'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'it should not be enabled when minerva skin used', ( assert ) => {
|
||||||
|
const user = createStubUser( false ),
|
||||||
|
userSettings = createStubUserSettings( false ),
|
||||||
|
config = new Map();
|
||||||
|
|
||||||
|
config.set( 'wgPopupsReferencePreviews', true );
|
||||||
|
config.set( 'wgPopupsConflictsWithRefTooltipsGadget', false );
|
||||||
|
config.set( 'skin', 'minerva' );
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.isReferencePreviewsEnabled( user, userSettings, config ),
|
||||||
|
null,
|
||||||
|
'Reference Previews is disabled.'
|
||||||
|
);
|
||||||
|
} );
|
109
tests/qunit/ext.cite.referencePreviews/renderer.test.js
Normal file
109
tests/qunit/ext.cite.referencePreviews/renderer.test.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
let createReferencePreview;
|
||||||
|
const previewTypes = { TYPE_REFERENCE: 'reference' };
|
||||||
|
|
||||||
|
// TODO: Fix this test. Currently failing on `document.getElementById`
|
||||||
|
QUnit.module.skip( 'ext.cite.referencePreviews#renderer', {
|
||||||
|
before() {
|
||||||
|
createReferencePreview = require( 'ext.cite.referencePreviews' ).private.createReferencePreview;
|
||||||
|
},
|
||||||
|
beforeEach() {
|
||||||
|
mw.msg = ( key ) => `<${ key }>`;
|
||||||
|
mw.message = ( key ) => {
|
||||||
|
return { exists: () => !key.endsWith( 'generic' ), text: () => `<${ key }>` };
|
||||||
|
};
|
||||||
|
|
||||||
|
mw.html = {
|
||||||
|
escape: ( str ) => str && str.replace( /'/g, ''' ).replace( /</g, '<' )
|
||||||
|
};
|
||||||
|
|
||||||
|
mw.track = () => {};
|
||||||
|
|
||||||
|
global.navigator = {
|
||||||
|
sendBeacon() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some tests below stub this function. Keep a copy so it can be restored.
|
||||||
|
this.getElementById = document.getElementById;
|
||||||
|
},
|
||||||
|
afterEach() {
|
||||||
|
// Restore getElementsById to its original state.
|
||||||
|
document.getElementById = this.getElementById;
|
||||||
|
mw.msg = null;
|
||||||
|
mw.message = null;
|
||||||
|
mw.html = null;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'createReferencePreview(model)', ( assert ) => {
|
||||||
|
const model = {
|
||||||
|
url: '#custom_id',
|
||||||
|
extract: 'Custom <i>extract</i> with an <a href="/wiki/Internal">internal</a> and an <a href="//wikipedia.de" class="external">external</a> link',
|
||||||
|
type: previewTypes.TYPE_REFERENCE,
|
||||||
|
referenceType: 'web'
|
||||||
|
},
|
||||||
|
preview = createReferencePreview( model );
|
||||||
|
|
||||||
|
assert.strictEqual( preview.hasThumbnail, false );
|
||||||
|
assert.strictEqual( preview.isTall, false );
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
$( preview.el ).find( '.mwe-popups-title' ).text().trim(),
|
||||||
|
'<popups-refpreview-web>'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
$( preview.el ).find( '.mw-parser-output' ).text().trim(),
|
||||||
|
'Custom extract with an internal and an external link'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
$( preview.el ).find( 'a[target="_blank"]' ).length,
|
||||||
|
1,
|
||||||
|
'only external links open in new tabs'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'createReferencePreview default title', ( assert ) => {
|
||||||
|
const model = {
|
||||||
|
url: '',
|
||||||
|
extract: '',
|
||||||
|
type: previewTypes.TYPE_REFERENCE
|
||||||
|
},
|
||||||
|
preview = createReferencePreview( model );
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
$( preview.el ).find( '.mwe-popups-title' ).text().trim(),
|
||||||
|
'<popups-refpreview-reference>'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'createReferencePreview updates fade-out effect on scroll', ( assert ) => {
|
||||||
|
const model = {
|
||||||
|
url: '',
|
||||||
|
extract: '',
|
||||||
|
type: previewTypes.TYPE_REFERENCE
|
||||||
|
},
|
||||||
|
preview = createReferencePreview( model ),
|
||||||
|
$extract = $( preview.el ).find( '.mwe-popups-extract' );
|
||||||
|
|
||||||
|
$extract.children()[ 0 ].dispatchEvent( new Event( 'scroll' ) );
|
||||||
|
|
||||||
|
assert.false( $extract.children()[ 0 ].isScrolling );
|
||||||
|
assert.false( $extract.hasClass( 'mwe-popups-fade-out' ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
QUnit.test( 'createReferencePreview collapsible/sortable handling', ( assert ) => {
|
||||||
|
const model = {
|
||||||
|
url: '',
|
||||||
|
extract: '<table class="mw-collapsible"></table>' +
|
||||||
|
'<table class="sortable"><th class="headerSort" tabindex="1" title="Click here"></th></table>',
|
||||||
|
type: previewTypes.TYPE_REFERENCE
|
||||||
|
},
|
||||||
|
preview = createReferencePreview( model );
|
||||||
|
|
||||||
|
assert.strictEqual( $( preview.el ).find( '.mw-collapsible, .sortable, .headerSort' ).length, 0 );
|
||||||
|
assert.strictEqual( $( preview.el ).find( 'th' ).attr( 'tabindex' ), undefined );
|
||||||
|
assert.strictEqual( $( preview.el ).find( 'th' ).attr( 'title' ), undefined );
|
||||||
|
assert.strictEqual(
|
||||||
|
$( preview.el ).find( '.mwe-collapsible-placeholder' ).text(),
|
||||||
|
'<popups-refpreview-collapsible-placeholder>'
|
||||||
|
);
|
||||||
|
} );
|
|
@ -0,0 +1,51 @@
|
||||||
|
( mw.loader.getModuleNames().indexOf( 'ext.popups.main' ) !== -1 ?
|
||||||
|
QUnit.module :
|
||||||
|
QUnit.module.skip )( 'ext.cite.referencePreviews#setUserConfigFlags' );
|
||||||
|
|
||||||
|
QUnit.test( 'reference preview config settings are successfully set from bitmask', ( assert ) => {
|
||||||
|
const config = new Map();
|
||||||
|
|
||||||
|
config.set( 'wgPopupsFlags', '7' );
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.setUserConfigFlags( config );
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
[
|
||||||
|
config.get( 'wgPopupsConflictsWithRefTooltipsGadget' ),
|
||||||
|
config.get( 'wgPopupsReferencePreviews' )
|
||||||
|
],
|
||||||
|
[ true, true ]
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set( 'wgPopupsFlags', '2' );
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.setUserConfigFlags( config );
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
[
|
||||||
|
config.get( 'wgPopupsConflictsWithRefTooltipsGadget' ),
|
||||||
|
config.get( 'wgPopupsReferencePreviews' )
|
||||||
|
],
|
||||||
|
[ true, false ]
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set( 'wgPopupsFlags', '5' );
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.setUserConfigFlags( config );
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
[
|
||||||
|
config.get( 'wgPopupsConflictsWithRefTooltipsGadget' ),
|
||||||
|
config.get( 'wgPopupsReferencePreviews' )
|
||||||
|
],
|
||||||
|
[ false, true ]
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set( 'wgPopupsFlags', '0' );
|
||||||
|
require( 'ext.cite.referencePreviews' ).private.setUserConfigFlags( config );
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
[
|
||||||
|
config.get( 'wgPopupsConflictsWithRefTooltipsGadget' ),
|
||||||
|
config.get( 'wgPopupsReferencePreviews' )
|
||||||
|
],
|
||||||
|
[ false, false ]
|
||||||
|
);
|
||||||
|
} );
|
Loading…
Reference in a new issue