mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/RelatedArticles
synced 2024-11-23 15:57:06 +00:00
Simplify the RelatedArticles extension to use Codex CSS components
Changes: - Removes redundant styles now inside Codex - With the new component, it's not possible to display 3 cards in a single line at a tablet resolution, so the media query responsible is bumped to apply only at the desktop threshold - Decisions are documented in ADR Bug: T286835 Change-Id: I493e8e601ccc31b3cf1f16c0b5a8975f12ef336c
This commit is contained in:
parent
ed9c2ce120
commit
71de06a682
|
@ -8,5 +8,15 @@
|
|||
"version": ">=14",
|
||||
"ignores": []
|
||||
} ]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/**/*.vue",
|
||||
"extends": "wikimedia/vue-es6",
|
||||
"rules": {
|
||||
"es/no-block-scoped-variables": "off",
|
||||
"es/no-object-assign": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
[
|
||||
{
|
||||
"resourceModule": "ext.relatedArticles.styles",
|
||||
"maxSize": "0.2 kB"
|
||||
"maxSize": "0.3 kB"
|
||||
},
|
||||
{
|
||||
"resourceModule": "ext.relatedArticles.readMore.bootstrap",
|
||||
"maxSize": "1.3 kB"
|
||||
"maxSize": "0.5 kB"
|
||||
},
|
||||
{
|
||||
"resourceModule": "ext.relatedArticles.readMore",
|
||||
"maxSize": "1.9 kB"
|
||||
"maxSize": "3.0 kB"
|
||||
}
|
||||
]
|
||||
|
|
32
doc/adr/0001-use-adrs-in-related-articles.md
Normal file
32
doc/adr/0001-use-adrs-in-related-articles.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# 1. Use ADRs in RelatedArticles
|
||||
|
||||
Date: 2023-11-08
|
||||
|
||||
## Status
|
||||
|
||||
To discuss.
|
||||
|
||||
## Context
|
||||
|
||||
ADRs are being used in a variety of web team extensions (Popups and Vector). They
|
||||
help us reflect on decisions we've made. RelatedArticles is seldom worked on, and
|
||||
refactors and modifications are rare, meaning developers can go long periods of time
|
||||
without touching the codebase, so it would be useful that decisions here are documented.
|
||||
|
||||
## Decision
|
||||
|
||||
We are adopting architecture decision records as we are in other code repositories.
|
||||
|
||||
## Consequences
|
||||
|
||||
When an decision that is deemed important to the codebase or the ecosystem
|
||||
is made inside this repository, it is up to the deciding individuals
|
||||
to create a new markdown file in this directory and add an ADR with regards
|
||||
to that decision.The ADR should be brief and may link to Phabricator or
|
||||
other places that hold more context around the decision.
|
||||
|
||||
ADRs may also be used to propose changes by framing the ADR as a proposal
|
||||
and marking the status as "proposed". If there is agreement to adopt the
|
||||
ADR, it's status should be change to "accepted".
|
||||
|
||||
ADRs can also be marked "deprecated" or "superseded".
|
30
doc/adr/0002-use-codex-css-components.md
Normal file
30
doc/adr/0002-use-codex-css-components.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# 1. Use Codex CSS components
|
||||
|
||||
Date: 2023-11-08
|
||||
|
||||
## Status
|
||||
|
||||
To discuss and refine once https://phabricator.wikimedia.org/T248718 has been resolved.
|
||||
|
||||
## Context
|
||||
|
||||
RelatedArticles is loaded when the user scrolls to the end of the page. Since it was
|
||||
first made, the Codex library has come into existence and RelatedArticle's looks dated. By using Codex we can keep the extension in line with design best practices and the rest
|
||||
of the UI and minimize the amount of modifications we have to make to it in future.
|
||||
|
||||
Given RelatedArticles can be loaded in the article namespace, when a user scrolls to the
|
||||
bottom of the page, it is preferable to make use of Codex CSS components rather than
|
||||
pulling in the full Vue.js library as the library is large. Loading the entirety of Vue
|
||||
and Codex without a clear user interaction is prohibited (https://phabricator.wikimedia.org/T248718).
|
||||
|
||||
## Decision
|
||||
|
||||
* RelatedArticles uses Codex components.
|
||||
* We are using Codex CSS components rather than Vue+Codex.
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
* Markup will need to be kept in sync with Codex CSS components.
|
||||
* In future it may make sense to use Vue.js if there is greater adoption in the skin.
|
||||
At the time of writing for skin only the search widget inside Vector is built in Vue.js.
|
27
doc/adr/0003-do-not-server-side-render.md
Normal file
27
doc/adr/0003-do-not-server-side-render.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# 1. Do not server side render
|
||||
|
||||
Date: 2023-11-08
|
||||
|
||||
## Status
|
||||
|
||||
To discuss.
|
||||
|
||||
## Context
|
||||
|
||||
From a product perspective we do not wish to display related articles until the user has scrolled to the bottom of the page after finishing the reading of an article (which is seen as an intention of "wanting to read more" [1]) and its click-through rates don't merit a feature that needs to be available to all users [2]. I think if we were to server-side render it, we would still add CSS to hide it (visibility: hidden) by default and reveal it on scroll.
|
||||
|
||||
Server side rendering would require an instance of CirrusSearch to be installed which would make development more complicated. Currently developers can point RelatedArticles at a production API when working on the extension locally.
|
||||
|
||||
Given there are no clear benefits of moving to server side rendering and significant work would be required to improve the development workflow in that scenario we decided to continue to not server side render (for now).
|
||||
|
||||
[1] https://www.mediawiki.org/wiki/Reading/Web/Projects/Related_pages
|
||||
[2] https://www.mediawiki.org/wiki/Reading/Web/Projects/Related_pages#Metrics_analysis
|
||||
|
||||
## Decision
|
||||
|
||||
We do not server render Vue.js. Instead the widget is rendered via JavaScript. To avoid
|
||||
the article reflowing, space is reserved for the widget given we know it's height (by proxy of knowing the number of cards that will be displayed) beforehand.
|
||||
|
||||
## Consequences
|
||||
|
||||
* Space reserved may need to be modified with Codex releases.
|
|
@ -42,11 +42,35 @@
|
|||
"manifest_version": 2,
|
||||
"ResourceModules": {
|
||||
"ext.relatedArticles.styles": {
|
||||
"styles": "resources/ext.relatedArticles.styles.less"
|
||||
"styles": "resources/ext.relatedArticles.styles.less",
|
||||
"skinStyles": {
|
||||
"default": [ "skinStyles/ext.relatedArticles.styles/default.less" ],
|
||||
"minerva": []
|
||||
}
|
||||
},
|
||||
"ext.relatedArticles.readMore.bootstrap": {
|
||||
"localBasePath": "resources/ext.relatedArticles.readMore.bootstrap/",
|
||||
"remoteExtPath": "RelatedArticles",
|
||||
"packageFiles": [
|
||||
"index.js"
|
||||
],
|
||||
"dependencies": [
|
||||
"mediawiki.user",
|
||||
"mediawiki.api",
|
||||
"mediawiki.Uri",
|
||||
"mediawiki.util"
|
||||
]
|
||||
},
|
||||
"ext.relatedArticles.readMore": {
|
||||
"class": "MediaWiki\\ResourceLoader\\CodexModule",
|
||||
"codexStyleOnly": "true",
|
||||
"codexComponents": [
|
||||
"CdxCard"
|
||||
],
|
||||
"dependencies": [
|
||||
"mediawiki.util"
|
||||
],
|
||||
"localBasePath": "resources/ext.relatedArticles.readMore/",
|
||||
"packageFiles": [
|
||||
"index.js",
|
||||
"RelatedPagesGateway.js",
|
||||
|
@ -58,34 +82,12 @@
|
|||
"onlyUseCirrusSearch": "RelatedArticlesOnlyUseCirrusSearch",
|
||||
"descriptionSource": "RelatedArticlesDescriptionSource"
|
||||
}
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
"mediawiki.user",
|
||||
"mediawiki.api",
|
||||
"mediawiki.Uri",
|
||||
"mediawiki.util"
|
||||
]
|
||||
},
|
||||
"ext.relatedArticles.readMore": {
|
||||
"dependencies": [
|
||||
"mediawiki.util",
|
||||
"oojs"
|
||||
],
|
||||
"packageFiles": [
|
||||
"resources/ext.relatedArticles.readMore/index.js",
|
||||
"resources/ext.relatedArticles.readMore/CardModel.js",
|
||||
"resources/ext.relatedArticles.readMore/CardView.js",
|
||||
"resources/ext.relatedArticles.readMore/CardListView.js"
|
||||
},
|
||||
"RelatedArticles.js"
|
||||
],
|
||||
"styles": [
|
||||
"resources/ext.relatedArticles.readMore/styles.less",
|
||||
"resources/ext.relatedArticles.readMore/readMore.less"
|
||||
"styles.less"
|
||||
],
|
||||
"skinStyles": {
|
||||
"default": "resources/ext.relatedArticles.readMore/readMore.default.less",
|
||||
"minerva": "resources/ext.relatedArticles.readMore/readMore.minerva.less"
|
||||
},
|
||||
"messages": [
|
||||
"relatedarticles-read-more-heading"
|
||||
]
|
||||
|
@ -96,11 +98,7 @@
|
|||
"remoteExtPath": "RelatedArticles",
|
||||
"packageFiles": [
|
||||
"tests/qunit/index.js",
|
||||
"resources/ext.relatedArticles.readMore.bootstrap/RelatedPagesGateway.js",
|
||||
"resources/ext.relatedArticles.readMore/CardView.js",
|
||||
"resources/ext.relatedArticles.readMore/CardModel.js",
|
||||
"tests/qunit/CardModel.test.js",
|
||||
"tests/qunit/CardView.test.js",
|
||||
"resources/ext.relatedArticles.readMore/RelatedPagesGateway.js",
|
||||
"tests/qunit/RelatedPagesGateway.test.js"
|
||||
]
|
||||
},
|
||||
|
@ -111,7 +109,7 @@
|
|||
},
|
||||
"RelatedArticlesCardLimit": {
|
||||
"description": "Maximum number of articles that should be shown in RelatedArticles widget. This limit is derived from limits in TextExtracts and PageImages extensions. Number should be between 1 and 20.",
|
||||
"value": 3
|
||||
"value": 5
|
||||
},
|
||||
"RelatedArticlesUseCirrusSearch": {
|
||||
"value": false
|
||||
|
|
|
@ -2,22 +2,32 @@
|
|||
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for
|
||||
// which coverage information should be collected
|
||||
collectCoverageFrom: [
|
||||
'resources/ext.relatedArticles.readMore/index.js'
|
||||
'resources/**/*.(js|vue)'
|
||||
],
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
coveragePathIgnorePatterns: [
|
||||
'/node_modules/'
|
||||
'/node_modules/',
|
||||
// currently covered by QUnit
|
||||
'/resources/ext.relatedArticles.readMore/RelatedPagesGateway.js',
|
||||
// Should not need test coverage. Covered by visual regression testing.
|
||||
'/resources/ext.relatedArticles.readMore.bootstrap/index.js'
|
||||
],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
|
@ -27,11 +37,22 @@ module.exports = {
|
|||
statements: 100
|
||||
}
|
||||
},
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
globals: {
|
||||
'vue-jest': {
|
||||
babelConfig: false,
|
||||
hideStyleWarn: true,
|
||||
experimentalCSSCompile: true
|
||||
}
|
||||
},
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'json'
|
||||
],
|
||||
|
||||
// The paths to modules that run some code to configure or
|
||||
// set up the testing environment before each test
|
||||
setupFiles: [
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const wikimediaTestingUtils = require( '@wikimedia/mw-node-qunit' );
|
||||
const fn = () => {};
|
||||
|
||||
global.CSS = {
|
||||
escape: ( str ) => str
|
||||
};
|
||||
|
||||
global.OO = {
|
||||
inheritClass: ( ClassNameObject ) => {
|
||||
ClassNameObject.super = fn;
|
||||
ClassNameObject.prototype.on = fn;
|
||||
},
|
||||
initClass: fn,
|
||||
EventEmitter: fn
|
||||
};
|
||||
|
||||
const { TextEncoder, TextDecoder } = require( 'util' );
|
||||
global.TextEncoder = TextEncoder;
|
||||
|
|
15579
package-lock.json
generated
15579
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -20,13 +20,15 @@
|
|||
"@wdio/local-runner": "7.16.13",
|
||||
"@wdio/mocha-framework": "7.16.13",
|
||||
"@wdio/spec-reporter": "7.16.13",
|
||||
"@wikimedia/codex": "0.13.0",
|
||||
"@wikimedia/mw-node-qunit": "7.0.0",
|
||||
"@wikimedia/types-wikimedia": "0.3.4",
|
||||
"eslint-config-wikimedia": "0.25.1",
|
||||
"grunt-banana-checker": "0.11.0",
|
||||
"jest": "27.4.7",
|
||||
"stylelint-config-wikimedia": "0.16.1",
|
||||
"typescript": "4.3.5",
|
||||
"ts-jest": "27.1.3",
|
||||
"typescript": "4.5.5",
|
||||
"wdio-mediawiki": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,4 @@
|
|||
( function () {
|
||||
|
||||
const data = require( './data.json' ),
|
||||
RelatedPagesGateway = require( './RelatedPagesGateway.js' ),
|
||||
relatedPages = new RelatedPagesGateway(
|
||||
new mw.Api( {
|
||||
ajax: {
|
||||
url: data.searchUrl
|
||||
}
|
||||
} ),
|
||||
mw.config.get( 'wgPageName' ),
|
||||
mw.config.get( 'wgRelatedArticles' ),
|
||||
data.useCirrusSearch,
|
||||
data.onlyUseCirrusSearch,
|
||||
data.descriptionSource
|
||||
),
|
||||
// Make sure this is never undefined as I'm paranoid
|
||||
LIMIT = mw.config.get( 'wgRelatedArticlesCardLimit', 3 );
|
||||
|
||||
/**
|
||||
* Load related articles when the user scrolls past half of the window height.
|
||||
*
|
||||
|
@ -37,17 +19,13 @@
|
|||
*/
|
||||
function initRelatedArticlesModule( container ) {
|
||||
$.when(
|
||||
mw.loader.using( 'ext.relatedArticles.readMore' ),
|
||||
relatedPages.getForCurrentPage( LIMIT )
|
||||
).then( function ( require, pages ) {
|
||||
if ( pages.length ) {
|
||||
require( 'ext.relatedArticles.readMore' ).render(
|
||||
pages,
|
||||
readMore
|
||||
);
|
||||
} else if ( container.parentNode ) {
|
||||
container.parentNode.removeChild( container );
|
||||
}
|
||||
mw.loader.using( 'ext.relatedArticles.readMore' )
|
||||
).then( function (
|
||||
/** @type {Function} */ require
|
||||
) {
|
||||
require( 'ext.relatedArticles.readMore' ).init(
|
||||
container
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* View that renders multiple {@link mw.cards.CardView cards}
|
||||
*
|
||||
* @class mw.cards.CardListView
|
||||
* @param {mw.cards.CardView[]} cardViews
|
||||
*/
|
||||
function CardListView( cardViews ) {
|
||||
const self = this;
|
||||
|
||||
/**
|
||||
* @property {mw.cards.CardView[]|Array}
|
||||
*/
|
||||
this.cardViews = cardViews || [];
|
||||
|
||||
/**
|
||||
* @property {jQuery}
|
||||
*/
|
||||
this.$el = $( '<ul>' ).attr( { class: 'ext-related-articles-card-list' } );
|
||||
|
||||
// We don't want to use template partials because we want to
|
||||
// preserve event handlers of each card view.
|
||||
this.cardViews.forEach( function ( cardView ) {
|
||||
self.$el.append( cardView.$el );
|
||||
} );
|
||||
}
|
||||
OO.initClass( CardListView );
|
||||
|
||||
module.exports = CardListView;
|
|
@ -1,54 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Model for an article
|
||||
* It is the single source of truth about a Card, which is a representation
|
||||
* of a wiki article. It emits a 'change' event when its attribute changes.
|
||||
* A View can listen to this event and update the UI accordingly.
|
||||
*
|
||||
* @class mw.cards.CardModel
|
||||
* @extends OO.EventEmitter
|
||||
* @param {Object} attributes article data, such as title, url, etc. about
|
||||
* an article
|
||||
*/
|
||||
function CardModel( attributes ) {
|
||||
CardModel.super.apply( this, arguments );
|
||||
/**
|
||||
* @property {Object} attributes of the model
|
||||
*/
|
||||
this.attributes = attributes;
|
||||
}
|
||||
OO.inheritClass( CardModel, OO.EventEmitter );
|
||||
|
||||
/**
|
||||
* Set a model attribute.
|
||||
* Emits a 'change' event with the object whose key is the attribute
|
||||
* that's being updated and value is the value that's being set. The event
|
||||
* can also be silenced.
|
||||
*
|
||||
* @param {string} key attribute that's being set
|
||||
* @param {Mixed} value the value of the key param
|
||||
* @param {boolean} [silent] whether to emit the 'change' event. By default
|
||||
* the 'change' event will be emitted.
|
||||
*/
|
||||
CardModel.prototype.set = function ( key, value, silent ) {
|
||||
const event = {};
|
||||
|
||||
this.attributes[ key ] = value;
|
||||
if ( !silent ) {
|
||||
event[ key ] = value;
|
||||
this.emit( 'change', event );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the model attribute's value.
|
||||
*
|
||||
* @param {string} key attribute that's being looked up
|
||||
* @return {Mixed}
|
||||
*/
|
||||
CardModel.prototype.get = function ( key ) {
|
||||
return this.attributes[ key ];
|
||||
};
|
||||
|
||||
module.exports = CardModel;
|
|
@ -1,85 +0,0 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Renders a Card model and updates when it does.
|
||||
*
|
||||
* @class mw.cards.CardView
|
||||
* @param {mw.cards.CardModel} model
|
||||
*/
|
||||
function CardView( model ) {
|
||||
/**
|
||||
* @property {mw.cards.CardModel}
|
||||
*/
|
||||
this.model = model;
|
||||
|
||||
// listen to model changes and re-render the view
|
||||
this.model.on( 'change', this.render.bind( this ) );
|
||||
|
||||
/**
|
||||
* @property {jQuery}
|
||||
*/
|
||||
this.$el = $( this._render() );
|
||||
}
|
||||
OO.initClass( CardView );
|
||||
|
||||
/**
|
||||
* Replace the html of this.$el with a newly rendered html using the model
|
||||
* attributes.
|
||||
*/
|
||||
CardView.prototype.render = function () {
|
||||
this.$el.replaceWith( this._render() );
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the template using the model attributes.
|
||||
*
|
||||
* @ignore
|
||||
* @return {string}
|
||||
*/
|
||||
CardView.prototype._render = function () {
|
||||
const $listItem = $( '<li>' ),
|
||||
attributes = $.extend( {}, this.model.attributes );
|
||||
|
||||
attributes.thumbnailUrl = CSS.escape( attributes.thumbnailUrl );
|
||||
|
||||
$listItem.attr( {
|
||||
title: attributes.title,
|
||||
class: 'ext-related-articles-card'
|
||||
} );
|
||||
|
||||
$listItem.append(
|
||||
$( '<div>' )
|
||||
.addClass( 'ext-related-articles-card-thumb' )
|
||||
.addClass( attributes.hasThumbnail ?
|
||||
'mw-no-invert' :
|
||||
'ext-related-articles-card-thumb-placeholder'
|
||||
)
|
||||
.css( 'background-image', attributes.hasThumbnail ?
|
||||
'url(' + attributes.thumbnailUrl + ')' :
|
||||
null
|
||||
),
|
||||
$( '<a>' )
|
||||
.attr( {
|
||||
href: attributes.url,
|
||||
'aria-hidden': 'true',
|
||||
tabindex: -1
|
||||
} ),
|
||||
$( '<div>' )
|
||||
.attr( { class: 'ext-related-articles-card-detail' } )
|
||||
.append(
|
||||
$( '<h3>' ).append(
|
||||
$( '<a>' )
|
||||
.attr( { href: attributes.url } )
|
||||
.text( attributes.title )
|
||||
),
|
||||
$( '<p>' )
|
||||
.attr( { class: 'ext-related-articles-card-extract' } )
|
||||
.text( attributes.extract )
|
||||
)
|
||||
);
|
||||
|
||||
return $listItem;
|
||||
};
|
||||
|
||||
module.exports = CardView;
|
40
resources/ext.relatedArticles.readMore/RelatedArticles.js
Normal file
40
resources/ext.relatedArticles.readMore/RelatedArticles.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/* eslint-disable indent, quotes */
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference path="./codex.ts" />
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {string} options.heading
|
||||
* @param {boolean} options.isContainerSmall
|
||||
* @param {Codex.ListTitleObject[]} options.cards
|
||||
* @return {string}
|
||||
*/
|
||||
const RelatedArticles = ( options ) => {
|
||||
return [
|
||||
`<div class="read-more-container ${( options.isContainerSmall ) ? 'read-more-container-small' : 'read-more-container-large'}">`,
|
||||
`<aside class="noprint">`,
|
||||
( options.heading ) ?
|
||||
`<h2 class="read-more-container-heading">${options.heading}</h2>` : ``,
|
||||
`<ul class="read-more-container-card-list">`,
|
||||
options.cards.map( ( card ) => `<li title="${card.label}">
|
||||
<a href="${card.url}"><span class="cdx-card">
|
||||
<span class="cdx-card__thumbnail cdx-thumbnail">
|
||||
${( card.thumbnail && card.thumbnail.url ) ?
|
||||
`<span class="cdx-thumbnail__image" style="background-image: url('${card.thumbnail.url}')"></span>` :
|
||||
`<span class="cdx-thumbnail__placeholder">
|
||||
<span class="cdx-thumbnail__placeholder__icon"></span>
|
||||
</span>`}
|
||||
</span>
|
||||
<span class="cdx-card__text">
|
||||
<span class="cdx-card__text__title">${card.label}</span>
|
||||
<span class="cdx-card__text__description">${card.description}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>` ).join( '\n' ),
|
||||
`</ul>`,
|
||||
`</aside>`,
|
||||
`</div>`
|
||||
].join( '\n' );
|
||||
};
|
||||
|
||||
module.exports = RelatedArticles;
|
|
@ -31,6 +31,14 @@ function RelatedPagesGateway(
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JQuery.Promise<any>} jQP
|
||||
* @return {Promise<any>}
|
||||
*/
|
||||
const toPromise = ( jQP ) => new Promise( ( resolve, reject ) => {
|
||||
jQP.then( ( pages ) => resolve( pages ), ( e ) => reject( e ) );
|
||||
} );
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @param {MwApiQueryResponse} result
|
||||
|
@ -41,7 +49,7 @@ function getPages( result ) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the related pages for the current page.
|
||||
* Gets the related pages for the list of pages
|
||||
*
|
||||
* If there are related pages assigned to this page using the `related`
|
||||
* parser function, then they are returned.
|
||||
|
@ -58,21 +66,17 @@ function getPages( result ) {
|
|||
* * The thumbnail, if any
|
||||
* * The page description, if any
|
||||
*
|
||||
* @method
|
||||
* @param {number} limit of pages to get. Should be between 1-20.
|
||||
* @return {JQuery.Promise<MwApiPageObject[]>}
|
||||
* @param {MwApiActionQuery} params for api
|
||||
* @return {Promise<MwApiPageObject[]>}
|
||||
*/
|
||||
RelatedPagesGateway.prototype.getForCurrentPage = function ( limit ) {
|
||||
const parameters = /** @type {MwApiActionQuery} */ ( {
|
||||
action: 'query',
|
||||
formatversion: 2,
|
||||
origin: '*',
|
||||
prop: 'pageimages',
|
||||
piprop: 'thumbnail',
|
||||
pithumbsize: 160 // FIXME: Revert to 80 once pithumbmode is implemented
|
||||
} ),
|
||||
// Enforce limit
|
||||
relatedPages = this.editorCuratedPages.slice( 0, limit );
|
||||
RelatedPagesGateway.prototype.getPagesFromApi = function ( params ) {
|
||||
const parameters = /** @type {MwApiActionQuery} */ Object.assign( {
|
||||
formatversion: 2,
|
||||
origin: '*',
|
||||
prop: 'pageimages',
|
||||
piprop: 'thumbnail',
|
||||
pithumbsize: 160 // FIXME: Revert to 80 once pithumbmode is implemented
|
||||
}, params );
|
||||
|
||||
switch ( this.descriptionSource ) {
|
||||
case 'wikidata':
|
||||
|
@ -90,16 +94,45 @@ RelatedPagesGateway.prototype.getForCurrentPage = function ( limit ) {
|
|||
break;
|
||||
}
|
||||
|
||||
if ( relatedPages.length ) {
|
||||
parameters.pilimit = relatedPages.length;
|
||||
parameters.continue = '';
|
||||
return toPromise(
|
||||
this.api.get( parameters )
|
||||
.then( getPages )
|
||||
);
|
||||
};
|
||||
|
||||
parameters.titles = relatedPages;
|
||||
/**
|
||||
* Gets the related pages for the list of pages
|
||||
*
|
||||
* @param {string[]} titles
|
||||
* @return {Promise<MwApiPageObject[]>}
|
||||
*/
|
||||
RelatedPagesGateway.prototype.getPages = function ( titles ) {
|
||||
return this.getPagesFromApi( {
|
||||
action: 'query',
|
||||
pilimit: titles.length,
|
||||
continue: '',
|
||||
titles
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the related pages for the list of pages
|
||||
*
|
||||
* @param {number} limit of pages to get. Should be between 1-20.
|
||||
* @return {Promise<MwApiPageObject[]>}
|
||||
*/
|
||||
RelatedPagesGateway.prototype.getForCurrentPage = function ( limit ) {
|
||||
const relatedPages = this.editorCuratedPages.slice( 0, limit );
|
||||
if ( relatedPages.length ) {
|
||||
return this.getPages( relatedPages );
|
||||
} else if ( this.useCirrusSearch ) {
|
||||
const parameters = /** @type {MwApiActionQuery} */( {
|
||||
action: 'query'
|
||||
} );
|
||||
parameters.pilimit = limit;
|
||||
|
||||
parameters.generator = 'search';
|
||||
parameters.gsrsearch = 'morelike:' + this.currentPage;
|
||||
parameters.gsrsearch = `morelike:${this.currentPage}`;
|
||||
parameters.gsrnamespace = '0';
|
||||
parameters.gsrlimit = limit;
|
||||
parameters.gsrqiprofile = 'classic_noboostlinks';
|
||||
|
@ -119,12 +152,10 @@ RelatedPagesGateway.prototype.getForCurrentPage = function ( limit ) {
|
|||
|
||||
// Instruct the browser that the response will become stale in 24 hours.
|
||||
parameters.maxage = 86400;
|
||||
return this.getPagesFromApi( parameters ).then( ( pages ) => Promise.resolve( pages ) );
|
||||
} else {
|
||||
return $.Deferred().resolve( [] );
|
||||
return Promise.resolve( [] );
|
||||
}
|
||||
|
||||
return this.api.get( parameters )
|
||||
.then( getPages );
|
||||
};
|
||||
|
||||
module.exports = RelatedPagesGateway;
|
14
resources/ext.relatedArticles.readMore/codex.ts
Normal file
14
resources/ext.relatedArticles.readMore/codex.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
declare namespace Codex {
|
||||
export interface CodexSuggestionThumbnail {
|
||||
width: number
|
||||
height: number
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface ListTitleObject {
|
||||
label: string
|
||||
url: string
|
||||
description: string
|
||||
thumbnail?: CodexSuggestionThumbnail
|
||||
}
|
||||
}
|
|
@ -1,48 +1,85 @@
|
|||
const CardModel = require( './CardModel.js' ),
|
||||
CardView = require( './CardView.js' ),
|
||||
CardListView = require( './CardListView.js' );
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference path="./codex.ts" />
|
||||
const RelatedArticles = require( './RelatedArticles.js' );
|
||||
const data = require( './data.json' );
|
||||
const RelatedPagesGateway = require( './RelatedPagesGateway.js' );
|
||||
const relatedPages = new RelatedPagesGateway(
|
||||
new mw.Api( {
|
||||
ajax: {
|
||||
url: data.searchUrl
|
||||
}
|
||||
} ),
|
||||
mw.config.get( 'wgPageName' ),
|
||||
mw.config.get( 'wgRelatedArticles' ),
|
||||
data.useCirrusSearch,
|
||||
data.onlyUseCirrusSearch,
|
||||
data.descriptionSource
|
||||
);
|
||||
const LIMIT = mw.config.get( 'wgRelatedArticlesCardLimit', 3 );
|
||||
|
||||
/**
|
||||
* Generates `mw.cards.CardView`s from pages
|
||||
* Generates suggestion objects from pages
|
||||
*
|
||||
* @param {Object[]} pages
|
||||
* @return {mw.cards.CardView[]}
|
||||
* @param {MwApiPageObject[]} pages
|
||||
* @return {Codex.ListTitleObject[]}
|
||||
*/
|
||||
function getCards( pages ) {
|
||||
return pages.map( function ( page ) {
|
||||
const result = {
|
||||
title: page.title,
|
||||
id: page.title,
|
||||
label: page.title,
|
||||
url: mw.util.getUrl( page.title ),
|
||||
hasThumbnail: false,
|
||||
extract: ( page.description || page.extract ||
|
||||
thumbnail: page.thumbnail ? {
|
||||
width: page.thumbnail.width,
|
||||
height: page.thumbnail.height,
|
||||
url: page.thumbnail.source
|
||||
} : undefined,
|
||||
description: ( page.description || page.extract ||
|
||||
( page.pageprops ? page.pageprops.description : '' ) )
|
||||
};
|
||||
|
||||
if ( page.thumbnail ) {
|
||||
result.hasThumbnail = true;
|
||||
result.thumbnailUrl = page.thumbnail.source;
|
||||
result.isThumbnailPortrait = page.thumbnail.height >= page.thumbnail.width;
|
||||
}
|
||||
|
||||
return new CardView( new CardModel( result ) );
|
||||
return result;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the related articles.
|
||||
*
|
||||
* @param {Object[]} pages
|
||||
* @param {MwApiPageObject[]} pages
|
||||
* @param {Element} el
|
||||
* @param {string} heading
|
||||
* @param {boolean} isContainerSmall
|
||||
*/
|
||||
function render( pages, el ) {
|
||||
const cards = new CardListView( getCards( pages ) ),
|
||||
$readMore = $( '<aside>' ).addClass( 'ra-read-more noprint' )
|
||||
.append( $( '<h2>' ).text( mw.msg( 'relatedarticles-read-more-heading' ) ) )
|
||||
.append( cards.$el );
|
||||
function render( pages, el, heading, isContainerSmall ) {
|
||||
el.innerHTML = RelatedArticles( {
|
||||
isContainerSmall,
|
||||
heading,
|
||||
cards: getCards( pages )
|
||||
} );
|
||||
}
|
||||
|
||||
$( el ).append( $readMore );
|
||||
/**
|
||||
* @param {HTMLElement} container to initialize into
|
||||
*/
|
||||
function init( container ) {
|
||||
relatedPages.getForCurrentPage( LIMIT ).then( ( /** @type {MwApiPageObject[]} */ pages ) => {
|
||||
if ( pages.length ) {
|
||||
render(
|
||||
pages,
|
||||
container,
|
||||
mw.msg( 'relatedarticles-read-more-heading' ),
|
||||
// Take up multiple columns if possible
|
||||
false
|
||||
);
|
||||
} else if ( container.parentNode ) {
|
||||
container.parentNode.removeChild( container );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
render,
|
||||
getCards
|
||||
getCards,
|
||||
test: {
|
||||
relatedPages
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
@import 'mediawiki.skin.variables.less';
|
||||
|
||||
.ra-read-more h2 {
|
||||
color: @color-subtle;
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
/* We don't want any over-rides. */
|
|
@ -5,155 +5,52 @@
|
|||
@thumbWidth: 80px;
|
||||
@cardBorder: 1px solid rgba( 0, 0, 0, 0.2 );
|
||||
|
||||
.ext-related-articles-card-list {
|
||||
.flex-display();
|
||||
flex-flow: row wrap;
|
||||
.read-more-container-heading {
|
||||
color: @color-subtle;
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.read-more-container-card-list {
|
||||
font-size: @baseFontSize;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding-left: 0;
|
||||
display: grid;
|
||||
row-gap: 10px;
|
||||
|
||||
h3 {
|
||||
@fontSize: 1em;
|
||||
@lineHeight: 1.3;
|
||||
@lineHeightEm: @lineHeight * @fontSize;
|
||||
font-family: inherit;
|
||||
font-size: @fontSize;
|
||||
// max 2 lines
|
||||
max-height: 2 * @lineHeightEm;
|
||||
line-height: @lineHeight;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
|
||||
a {
|
||||
color: @color-emphasized;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 25%;
|
||||
height: @lineHeightEm;
|
||||
background-color: transparent;
|
||||
background-image: linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% );
|
||||
}
|
||||
li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// flex-item
|
||||
.ext-related-articles-card {
|
||||
background-color: @background-color-base;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
height: @thumbWidth;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: @cardBorder;
|
||||
|
||||
& + .ext-related-articles-card {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
// Apply radius to top & bottom cards when stacked
|
||||
&:first-child {
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
}
|
||||
a,
|
||||
a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ext-related-articles-card > a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
a:hover {
|
||||
.cdx-card {
|
||||
box-shadow: 0 1px 1px rgba( 0, 0, 0, 0.1 );
|
||||
}
|
||||
}
|
||||
|
||||
.ext-related-articles-card-detail {
|
||||
// Vertically center the element using the technique described at
|
||||
// http://zerosixthree.se/vertical-align-anything-with-just-3-lines-of-css/.
|
||||
//
|
||||
// This technique is ideal because:
|
||||
// * it's easy to reason about,
|
||||
// * `position: relative` means that the element is laid out as if it weren't
|
||||
// positioned, allowing for `text-overflow: ellipsis` to work (see below)
|
||||
// * it supports more browsers than flexbox does, and
|
||||
// * we don't deliver RelatedArticles assets to those browsers that don't
|
||||
// support CSS 2D transforms
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY( -50% );
|
||||
}
|
||||
|
||||
.ext-related-articles-card-extract {
|
||||
color: @color-subtle;
|
||||
font-size: 0.8em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.ext-related-articles-card-thumb {
|
||||
background-color: #eaecf0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
float: left;
|
||||
height: 100%;
|
||||
width: @thumbWidth;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ext-related-articles-card-thumb-placeholder {
|
||||
background-image: url( article.svg );
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and ( min-width: @min-width-breakpoint-tablet ) {
|
||||
.ext-related-articles-card-list {
|
||||
border-top: 0;
|
||||
@supports ( display: grid ) {
|
||||
@media all and ( min-width: @width-breakpoint-desktop ) {
|
||||
.read-more-container-card-list {
|
||||
grid-template-columns: repeat( 3, 1fr );
|
||||
column-gap: 10px;
|
||||
|
||||
.ext-related-articles-card {
|
||||
border: @cardBorder;
|
||||
@rightMargin: 1%;
|
||||
margin-right: @rightMargin;
|
||||
margin-bottom: 10px;
|
||||
// max space is 100-2/3
|
||||
width: ( 100 - ( 2 * @rightMargin ) ) / 3;
|
||||
|
||||
// Individual border-radius when cards side by side (not stacked)
|
||||
&,
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
border-radius: @border-radius-base;
|
||||
.read-more-container-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
& + .ext-related-articles-card {
|
||||
border: @cardBorder;
|
||||
}
|
||||
|
||||
// every 3rd child drop the right margin
|
||||
&:nth-child( 3n+3 ) {
|
||||
margin-right: 0;
|
||||
.cdx-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,12 @@
|
|||
// Reserve space for the related articles cards.
|
||||
// https://phabricator.wikimedia.org/T223844
|
||||
// Note this is optimized for 3 related articles which is the majority of cases.
|
||||
// There will be a jump when:
|
||||
// - a page has no related articles,
|
||||
// - a page has more than 3 related articles
|
||||
// and there will be visible whitespace on mobile for a page with less than 3.
|
||||
.client-js {
|
||||
.read-more-container {
|
||||
// Assumes 3 cards at 78px.
|
||||
min-height: 274px;
|
||||
min-height: 320px;
|
||||
|
||||
@media ( min-width: @min-width-breakpoint-desktop ) {
|
||||
min-height: 124px;
|
||||
min-height: 163px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
resources/vue.d.ts
vendored
Normal file
5
resources/vue.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
declare module "*.vue" {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<any, any, any>
|
||||
export default component
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
.ra-read-more {
|
||||
.client-js .read-more-container {
|
||||
padding: 1em 0 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Hide RelatedArticles when VE is activated, see T120443
|
||||
.ve-activated & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ext-related-articles-card-list {
|
||||
&-card-list {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
22
tests/jest/RelatedArticles.test.js
Normal file
22
tests/jest/RelatedArticles.test.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
let RelatedArticles;
|
||||
describe( 'RelatedArticles', () => {
|
||||
beforeEach( () => {
|
||||
RelatedArticles = require( '../../resources/ext.relatedArticles.readMore/RelatedArticles.js' );
|
||||
} );
|
||||
|
||||
it( 'renders with cards', () => {
|
||||
const html = RelatedArticles( {
|
||||
cards: [
|
||||
{
|
||||
id: '4',
|
||||
label: 'Title',
|
||||
value: 'Title',
|
||||
description: 'Description'
|
||||
}
|
||||
]
|
||||
} );
|
||||
expect(
|
||||
html
|
||||
).toContain( 'class="cdx-card"' );
|
||||
} );
|
||||
} );
|
|
@ -1,3 +1,76 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ext.relatedArticles.readMore.bootstrap renders without error 1`] = `"<aside class=\\"ra-read-more noprint\\"><h2>relatedarticles-read-more-heading</h2><ul class=\\"ext-related-articles-card-list\\"><li title=\\"Hello no description\\" class=\\"ext-related-articles-card\\"><div class=\\"ext-related-articles-card-thumb ext-related-articles-card-thumb-placeholder\\"></div><a href=\\"Hello no description\\" aria-hidden=\\"true\\" tabindex=\\"-1\\"></a><div class=\\"ext-related-articles-card-detail\\"><h3><a href=\\"Hello no description\\">Hello no description</a></h3><p class=\\"ext-related-articles-card-extract\\"></p></div></li><li title=\\"Hello\\" class=\\"ext-related-articles-card\\"><div class=\\"ext-related-articles-card-thumb mw-no-invert\\" style=\\"background-image: url(hello.gif);\\"></div><a href=\\"Hello\\" aria-hidden=\\"true\\" tabindex=\\"-1\\"></a><div class=\\"ext-related-articles-card-detail\\"><h3><a href=\\"Hello\\">Hello</a></h3><p class=\\"ext-related-articles-card-extract\\">Description</p></div></li><li title=\\"Hello with extract\\" class=\\"ext-related-articles-card\\"><div class=\\"ext-related-articles-card-thumb mw-no-invert\\" style=\\"background-image: url(helloEx.gif);\\"></div><a href=\\"Hello with extract\\" aria-hidden=\\"true\\" tabindex=\\"-1\\"></a><div class=\\"ext-related-articles-card-detail\\"><h3><a href=\\"Hello with extract\\">Hello with extract</a></h3><p class=\\"ext-related-articles-card-extract\\">Extract</p></div></li><li title=\\"Hello with pageprops\\" class=\\"ext-related-articles-card\\"><div class=\\"ext-related-articles-card-thumb ext-related-articles-card-thumb-placeholder\\"></div><a href=\\"Hello with pageprops\\" aria-hidden=\\"true\\" tabindex=\\"-1\\"></a><div class=\\"ext-related-articles-card-detail\\"><h3><a href=\\"Hello with pageprops\\">Hello with pageprops</a></h3><p class=\\"ext-related-articles-card-extract\\">Page props desc</p></div></li></ul></aside>"`;
|
||||
exports[`ext.relatedArticles.readMore.bootstrap init with pages 1`] = `""`;
|
||||
|
||||
exports[`ext.relatedArticles.readMore.bootstrap init with zero pages and parent container 1`] = `"<div></div>"`;
|
||||
|
||||
exports[`ext.relatedArticles.readMore.bootstrap init with zero pages without parent container 1`] = `""`;
|
||||
|
||||
exports[`ext.relatedArticles.readMore.bootstrap renders with small container and custom heading 1`] = `
|
||||
"<div class=\\"read-more-container read-more-container-small\\">
|
||||
<aside class=\\"noprint\\">
|
||||
<h2 class=\\"read-more-container-heading\\">Hello world</h2>
|
||||
<ul class=\\"read-more-container-card-list\\">
|
||||
|
||||
</ul>
|
||||
</aside>
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`ext.relatedArticles.readMore.bootstrap renders without error 1`] = `
|
||||
"<div class=\\"read-more-container read-more-container-small\\">
|
||||
<aside class=\\"noprint\\">
|
||||
|
||||
<ul class=\\"read-more-container-card-list\\">
|
||||
<li title=\\"Hello no description\\">
|
||||
<a href=\\"/wiki/Hello no description\\"><span class=\\"cdx-card\\">
|
||||
<span class=\\"cdx-card__thumbnail cdx-thumbnail\\">
|
||||
<span class=\\"cdx-thumbnail__placeholder\\">
|
||||
<span class=\\"cdx-thumbnail__placeholder__icon\\"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class=\\"cdx-card__text\\">
|
||||
<span class=\\"cdx-card__text__title\\">Hello no description</span>
|
||||
<span class=\\"cdx-card__text__description\\"></span>
|
||||
</span>
|
||||
</span></a>
|
||||
</li>
|
||||
<li title=\\"Hello\\">
|
||||
<a href=\\"/wiki/Hello\\"><span class=\\"cdx-card\\">
|
||||
<span class=\\"cdx-card__thumbnail cdx-thumbnail\\">
|
||||
<span class=\\"cdx-thumbnail__image\\" style=\\"background-image: url('hello.gif')\\"></span>
|
||||
</span>
|
||||
<span class=\\"cdx-card__text\\">
|
||||
<span class=\\"cdx-card__text__title\\">Hello</span>
|
||||
<span class=\\"cdx-card__text__description\\">Description</span>
|
||||
</span>
|
||||
</span></a>
|
||||
</li>
|
||||
<li title=\\"Hello with extract\\">
|
||||
<a href=\\"/wiki/Hello with extract\\"><span class=\\"cdx-card\\">
|
||||
<span class=\\"cdx-card__thumbnail cdx-thumbnail\\">
|
||||
<span class=\\"cdx-thumbnail__image\\" style=\\"background-image: url('helloEx.gif')\\"></span>
|
||||
</span>
|
||||
<span class=\\"cdx-card__text\\">
|
||||
<span class=\\"cdx-card__text__title\\">Hello with extract</span>
|
||||
<span class=\\"cdx-card__text__description\\">Extract</span>
|
||||
</span>
|
||||
</span></a>
|
||||
</li>
|
||||
<li title=\\"Hello with pageprops\\">
|
||||
<a href=\\"/wiki/Hello with pageprops\\"><span class=\\"cdx-card\\">
|
||||
<span class=\\"cdx-card__thumbnail cdx-thumbnail\\">
|
||||
<span class=\\"cdx-thumbnail__placeholder\\">
|
||||
<span class=\\"cdx-thumbnail__placeholder__icon\\"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class=\\"cdx-card__text\\">
|
||||
<span class=\\"cdx-card__text__title\\">Hello with pageprops</span>
|
||||
<span class=\\"cdx-card__text__description\\">Page props desc</span>
|
||||
</span>
|
||||
</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>"
|
||||
`;
|
||||
|
|
|
@ -1,22 +1,69 @@
|
|||
const { render } = require( '../../resources/ext.relatedArticles.readMore/index.js' );
|
||||
const { render, init, getCards, test } = require( '../../resources/ext.relatedArticles.readMore/index.js' );
|
||||
const { createApp } = require( 'vue' );
|
||||
const PAGE = {
|
||||
title: 'Hello no description'
|
||||
};
|
||||
const PAGE_WITH_DESCRIPTION = {
|
||||
title: 'Hello',
|
||||
description: 'Description',
|
||||
thumbnail: {
|
||||
source: 'hello.gif',
|
||||
width: 200,
|
||||
height: 200
|
||||
}
|
||||
};
|
||||
|
||||
describe( 'ext.relatedArticles.readMore.bootstrap', () => {
|
||||
beforeEach( () => {
|
||||
mw.util.getUrl = jest.fn( ( title ) => `/wiki/${title}` );
|
||||
} );
|
||||
|
||||
it( 'init with zero pages and parent container', () => {
|
||||
test.relatedPages.getForCurrentPage = jest.fn( () => Promise.resolve( [] ) );
|
||||
const parent = document.createElement( 'div' );
|
||||
const element = document.createElement( 'div' );
|
||||
parent.appendChild( element );
|
||||
init( element );
|
||||
expect( parent.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'init with zero pages without parent container', () => {
|
||||
test.relatedPages.getForCurrentPage = jest.fn( () => Promise.resolve( [] ) );
|
||||
const element = document.createElement( 'div' );
|
||||
init( element );
|
||||
expect( element.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'init with pages', () => {
|
||||
test.relatedPages.getForCurrentPage = jest.fn( () => Promise.resolve( [
|
||||
PAGE_WITH_DESCRIPTION
|
||||
] ) );
|
||||
const element = document.createElement( 'div' );
|
||||
init( element );
|
||||
expect( element.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'renders with small container and custom heading', () => {
|
||||
const element = document.createElement( 'div' );
|
||||
render( [], element, 'Hello world', function ( options ) {
|
||||
const app = createApp( options );
|
||||
return app;
|
||||
}, true );
|
||||
expect( element.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'renders without error', () => {
|
||||
const element = document.createElement( 'div' );
|
||||
|
||||
const plugin = {
|
||||
install: function ( app ) {
|
||||
app.config.globalProperties.$i18n = () => ( {
|
||||
text: ( key ) => `<${key}>`
|
||||
} );
|
||||
}
|
||||
};
|
||||
render( [
|
||||
{
|
||||
title: 'Hello no description'
|
||||
},
|
||||
{
|
||||
title: 'Hello',
|
||||
description: 'Description',
|
||||
thumbnail: {
|
||||
source: 'hello.gif',
|
||||
width: 200,
|
||||
height: 200
|
||||
}
|
||||
},
|
||||
PAGE,
|
||||
PAGE_WITH_DESCRIPTION,
|
||||
{
|
||||
title: 'Hello with extract',
|
||||
extract: 'Extract',
|
||||
|
@ -32,7 +79,76 @@ describe( 'ext.relatedArticles.readMore.bootstrap', () => {
|
|||
description: 'Page props desc'
|
||||
}
|
||||
}
|
||||
], element );
|
||||
], element, '', function ( options ) {
|
||||
const app = createApp( options );
|
||||
app.use( plugin );
|
||||
return app;
|
||||
}, false );
|
||||
expect( element.innerHTML ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
const DEFAULT_CARD = {
|
||||
title: 'Title',
|
||||
thumbnail: {
|
||||
source: 'puppy.gif',
|
||||
width: 100,
|
||||
height: 100
|
||||
}
|
||||
};
|
||||
|
||||
it( 'maps cards', () => {
|
||||
[
|
||||
[
|
||||
Object.assign( {}, DEFAULT_CARD, {
|
||||
description: 'Description'
|
||||
} )
|
||||
],
|
||||
[
|
||||
Object.assign( {}, DEFAULT_CARD, {
|
||||
extract: 'Description'
|
||||
} )
|
||||
],
|
||||
[
|
||||
Object.assign( {}, DEFAULT_CARD, {
|
||||
pageprops: {
|
||||
description: 'Description'
|
||||
}
|
||||
} )
|
||||
]
|
||||
].forEach( ( testCase ) => {
|
||||
const cards = getCards( testCase );
|
||||
expect( cards ).toEqual( [
|
||||
{
|
||||
id: 'Title',
|
||||
label: 'Title',
|
||||
url: '/wiki/Title',
|
||||
thumbnail: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
url: 'puppy.gif'
|
||||
},
|
||||
description: 'Description'
|
||||
}
|
||||
] );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'maps cards with missing fields', () => {
|
||||
const cards = getCards(
|
||||
[
|
||||
{
|
||||
title: 'Title'
|
||||
}
|
||||
]
|
||||
);
|
||||
expect( cards ).toEqual( [
|
||||
{
|
||||
id: 'Title',
|
||||
label: 'Title',
|
||||
url: '/wiki/Title',
|
||||
description: '',
|
||||
thumbnail: undefined
|
||||
}
|
||||
] );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
( function () {
|
||||
'use strict';
|
||||
|
||||
const CardModel = require( '../../resources/ext.relatedArticles.readMore/CardModel.js' );
|
||||
|
||||
QUnit.module( 'ext.relatedArticles.cards/CardModel' );
|
||||
|
||||
QUnit.test( '#set', function ( assert ) {
|
||||
let model = new CardModel( {} );
|
||||
|
||||
model.on( 'change', function ( attributes ) {
|
||||
assert.strictEqual(
|
||||
attributes.foo,
|
||||
'bar',
|
||||
'It emits an event with the attribute that has changed.'
|
||||
);
|
||||
} );
|
||||
model.set( 'foo', 'bar' );
|
||||
|
||||
model = new CardModel( {} );
|
||||
model.on( 'change', function () {
|
||||
assert.true( false, 'It doesn\'t emit an event when silenced.' );
|
||||
} );
|
||||
|
||||
model.set( 'foo', 'bar', true );
|
||||
} );
|
||||
|
||||
QUnit.test( '#get', function ( assert ) {
|
||||
const model = new CardModel( {} );
|
||||
|
||||
model.set( 'foo', 'bar' );
|
||||
assert.strictEqual( model.get( 'foo' ), 'bar', 'Got the correct value.' );
|
||||
assert.strictEqual( model.get( 'x' ), undefined, 'Got the correct value.' );
|
||||
} );
|
||||
}() );
|
|
@ -1,28 +0,0 @@
|
|||
( function () {
|
||||
'use strict';
|
||||
|
||||
const CardModel = require( '../../resources/ext.relatedArticles.readMore/CardModel.js' ),
|
||||
CardView = require( '../../resources/ext.relatedArticles.readMore/CardView.js' );
|
||||
|
||||
QUnit.module( 'ext.relatedArticles.cards/CardView' );
|
||||
|
||||
QUnit.test( '#_render escapes the thumbnailUrl model attribute', function ( assert ) {
|
||||
const model = new CardModel( {
|
||||
title: 'One',
|
||||
url: mw.util.getUrl( 'One' ),
|
||||
hasThumbnail: true,
|
||||
thumbnailUrl: 'http://foo.bar/\');display:none;"//baz.jpg',
|
||||
isThumbnailProtrait: false
|
||||
} ),
|
||||
view = new CardView( model );
|
||||
|
||||
const style = view.$el.find( '.ext-related-articles-card-thumb' )
|
||||
.eq( 0 )
|
||||
.attr( 'style' );
|
||||
|
||||
assert.strictEqual(
|
||||
style,
|
||||
"background-image: url(\"http://foo.bar/');display:none;\\\"//baz.jpg\");"
|
||||
);
|
||||
} );
|
||||
}() );
|
|
@ -1,5 +1,5 @@
|
|||
( function () {
|
||||
const RelatedPagesGateway = require( '../../resources/ext.relatedArticles.readMore.bootstrap/RelatedPagesGateway.js' ),
|
||||
const RelatedPagesGateway = require( '../../resources/ext.relatedArticles.readMore/RelatedPagesGateway.js' ),
|
||||
lotsaRelatedPages = [ 'A', 'B', 'C', 'D', 'E', 'F' ],
|
||||
relatedPages = {
|
||||
query: {
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
// Run tests
|
||||
require( './CardModel.test.js' );
|
||||
require( './CardView.test.js' );
|
||||
require( './RelatedPagesGateway.test.js' );
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
const CARD_SELECTOR = '.ext-related-articles-card',
|
||||
Page = require( 'wdio-mediawiki/Page' ),
|
||||
READ_MORE_MODULE_NAME = 'ext.relatedArticles.readMore';
|
||||
READ_MORE_MODULE_NAME = 'ext.relatedArticles.readMore.bootstrap';
|
||||
|
||||
class ReadMorePage extends Page {
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"include": [
|
||||
"resources/ext.relatedArticles.readMore.bootstrap/index.js",
|
||||
"resources/ext.relatedArticles.readMore.bootstrap/RelatedPagesGateway.js"
|
||||
"resources/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
|
|
Loading…
Reference in a new issue