Merge remote-tracking branch 'gerrit/page-issues-cleanup'
Bug: T198765 Bug: T208514 Change-Id: I9467101c9b01cbd7682de859e43e04966760eea6
This commit is contained in:
commit
7b5928d795
|
@ -91,7 +91,6 @@ class MinervaHooks {
|
|||
// additional scaffolding (minus initialisation scripts)
|
||||
'tests/qunit/skins.minerva.scripts/stubs.js',
|
||||
|
||||
'resources/skins.minerva.scripts/pageIssuesLogger.js',
|
||||
'resources/skins.minerva.scripts/pageIssuesParser.js',
|
||||
'resources/skins.minerva.scripts/DownloadIcon.js',
|
||||
'resources/skins.minerva.scripts/AB.js',
|
||||
|
@ -101,7 +100,6 @@ class MinervaHooks {
|
|||
'tests/qunit/skins.minerva.scripts/DownloadIcon.test.js',
|
||||
'tests/qunit/skins.minerva.scripts/pageIssuesParser.test.js',
|
||||
'tests/qunit/skins.minerva.scripts/AB.test.js',
|
||||
'tests/qunit/skins.minerva.scripts/PageIssuesOverlay.test.js',
|
||||
'tests/qunit/skins.minerva.scripts/pageIssues.test.js',
|
||||
'tests/qunit/skins.minerva.notifications.badge/NotificationBadge.test.js'
|
||||
],
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
var
|
||||
router = require( 'mediawiki.router' ),
|
||||
issues = M.require( 'skins.minerva.scripts/pageIssues' ),
|
||||
overlayManager = M.require( 'skins.minerva.scripts/overlayManager' ),
|
||||
loader = M.require( 'mobile.startup/rlModuleLoader' ),
|
||||
skin = M.require( 'skins.minerva.scripts/skin' ),
|
||||
|
@ -34,7 +33,6 @@
|
|||
*/
|
||||
function onEditLinkClick() {
|
||||
var section = ( new mw.Uri( this.href ) ).query.section || 'all';
|
||||
issues.log( { action: 'editClicked' } );
|
||||
router.navigate( '#/editor/' + section );
|
||||
// prevent folding section when clicking Edit by stopping propagation
|
||||
return false;
|
||||
|
|
|
@ -13,13 +13,11 @@
|
|||
* @extends Overlay
|
||||
*
|
||||
* @param {IssueSummary[]} issues list of page issue summaries for display.
|
||||
* @param {PageIssuesLogger} logger E.g., { log: console.log }.
|
||||
* @param {string} section
|
||||
* @param {number} namespaceID
|
||||
*/
|
||||
function PageIssuesOverlay( issues, logger, section, namespaceID ) {
|
||||
function PageIssuesOverlay( issues, section, namespaceID ) {
|
||||
var
|
||||
options,
|
||||
// Note only the main namespace is expected to make use of section issues, so the
|
||||
// heading will always be minerva-meta-data-issues-section-header regardless of
|
||||
// namespace.
|
||||
|
@ -27,24 +25,10 @@
|
|||
getNamespaceHeadingText( namespaceID ) :
|
||||
mwMsg( 'minerva-meta-data-issues-section-header' );
|
||||
|
||||
this.issues = issues;
|
||||
this.logger = logger;
|
||||
this.section = section;
|
||||
|
||||
options = {};
|
||||
options.issues = issues;
|
||||
|
||||
// Set default logging data
|
||||
this.defaultLoggerData = {};
|
||||
// In the case of KEYWORD_ALL_SECTIONS all issues are in the overlay and the sectionNumbers
|
||||
// field should be no different from the default behaviour.
|
||||
if ( this.section !== KEYWORD_ALL_SECTIONS ) {
|
||||
this.defaultLoggerData.sectionNumbers = [ this.section ];
|
||||
}
|
||||
options.heading = '<strong>' + headingText + '</strong>';
|
||||
Overlay.call( this, options );
|
||||
|
||||
this.on( Overlay.EVENT_EXIT, this.onExit.bind( this ) );
|
||||
Overlay.call( this, {
|
||||
issues: issues,
|
||||
heading: '<strong>' + headingText + '</strong>'
|
||||
} );
|
||||
}
|
||||
|
||||
OO.mfExtend( PageIssuesOverlay, Overlay, {
|
||||
|
@ -54,127 +38,14 @@
|
|||
*/
|
||||
className: 'overlay overlay-issues',
|
||||
|
||||
/**
|
||||
* @memberof PageIssuesOverlay
|
||||
* @instance
|
||||
*/
|
||||
events: util.extend( {}, Overlay.prototype.events, {
|
||||
'click a[href*=redlink]': 'onRedLinkClick',
|
||||
'click a:not(.external):not([href*=edit])': 'onInternalClick',
|
||||
// Only register attempts to edit an existing page (should be the one we are on),
|
||||
// not internal clicks on redlinks to nonexistent pages:
|
||||
'click a[href*="edit"]:not([href*=redlink])': 'onEditClick'
|
||||
} ),
|
||||
|
||||
/**
|
||||
* @memberof PageIssuesOverlay
|
||||
* @instance
|
||||
*/
|
||||
templatePartials: util.extend( {}, Overlay.prototype.templatePartials, {
|
||||
content: mw.template.get( 'skins.minerva.scripts', 'PageIssuesOverlayContent.hogan' )
|
||||
} ),
|
||||
|
||||
/**
|
||||
* Log data via the associated logger, adding sectionNumbers to override the event default
|
||||
* if applicable.
|
||||
* @param {Object} data
|
||||
* @instance
|
||||
*/
|
||||
log: function ( data ) {
|
||||
this.logger.log( util.extend( {}, this.defaultLoggerData, data ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Note: an "on enter" state is tracked by the issueClicked log event.
|
||||
* @return {void}
|
||||
*/
|
||||
onExit: function () {
|
||||
var logData = {
|
||||
action: 'modalClose',
|
||||
issuesSeverity: this.issues.map( issueSummaryToSeverity )
|
||||
},
|
||||
currentSection = this.section;
|
||||
// When users close the modal, `sectionNumbers` should correlate to each visible issue
|
||||
// in the modal, provided that this.section is a valid number and not
|
||||
// `KEYWORD_ALL_SECTIONS`.
|
||||
if ( this.section !== KEYWORD_ALL_SECTIONS ) {
|
||||
logData.sectionNumbers = this.issues.map( function () {
|
||||
return currentSection;
|
||||
} )
|
||||
} );
|
||||
}
|
||||
this.log( logData );
|
||||
},
|
||||
|
||||
/**
|
||||
* Event that is triggered when an internal link inside the overlay is clicked.
|
||||
* This event will not be triggered if the link contains the edit keyword,
|
||||
* in which case onEditClick will be
|
||||
* fired. This is primarily used for instrumenting page issues (see
|
||||
* https://meta.wikimedia.org/wiki/Schema:PageIssues).
|
||||
* @param {JQuery.Event} ev
|
||||
* @memberof PageIssuesOverlay
|
||||
* @instance
|
||||
*/
|
||||
onInternalClick: function ( ev ) {
|
||||
var severity = parseSeverity( this.$( ev.target ) );
|
||||
this.log( {
|
||||
action: 'modalInternalClicked',
|
||||
issuesSeverity: [ severity ]
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Event that is triggered when a red link (e.g. a link to a page which doesn't exist)
|
||||
* inside the overlay is clicked.
|
||||
* @param {JQuery.Event} ev
|
||||
* @memberof PageIssuesOverlay
|
||||
* @instance
|
||||
*/
|
||||
onRedLinkClick: function ( ev ) {
|
||||
var severity = parseSeverity( this.$( ev.target ) );
|
||||
this.log( {
|
||||
action: 'modalRedLinkClicked',
|
||||
issuesSeverity: [ severity ]
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Event that is triggered when an edit link inside the overlay is clicked.
|
||||
* This is primarily
|
||||
* used for instrumenting page issues (see https://meta.wikimedia.org/wiki/Schema:PageIssues).
|
||||
* The event will not be triggered in the case of red links.
|
||||
* See onRedLinkClick for red links.
|
||||
* @param {JQuery.Event} ev
|
||||
* @memberof PageIssuesOverlay
|
||||
* @instance
|
||||
*/
|
||||
onEditClick: function ( ev ) {
|
||||
var severity = parseSeverity( this.$( ev.target ) );
|
||||
this.log( {
|
||||
action: 'modalEditClicked',
|
||||
issuesSeverity: [ severity ]
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* Obtain severity associated with a given $target node by looking at associated parent node
|
||||
* (defined by templatePartials, PageIssuesOverlayContent.hogan).
|
||||
*
|
||||
* @param {JQuery.Object} $target
|
||||
* @return {string[]} severity as defined in associated PageIssue
|
||||
*/
|
||||
function parseSeverity( $target ) {
|
||||
return $target.parents( '.issue-notice' ).data( 'severity' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IssueSummary} issue
|
||||
* @return {string} A PageIssue.severity.
|
||||
*/
|
||||
function issueSummaryToSeverity( issue ) {
|
||||
return issue.severity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a suitable heading for the issues overlay based on the namespace
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{#issues}}
|
||||
<li>
|
||||
<div class="issue-notice" data-severity="{{severity}}">
|
||||
{{{icon}}}
|
||||
{{{iconString}}}
|
||||
<div class="issue-details">{{{text}}}</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -1,105 +1,17 @@
|
|||
( function ( M, $ ) {
|
||||
var AB = M.require( 'skins.minerva.scripts/AB' ),
|
||||
Page = M.require( 'mobile.startup/Page' ),
|
||||
var Page = M.require( 'mobile.startup/Page' ),
|
||||
allIssues = {},
|
||||
KEYWORD_ALL_SECTIONS = 'all',
|
||||
config = mw.config,
|
||||
user = mw.user,
|
||||
NS_MAIN = 0,
|
||||
NS_TALK = 1,
|
||||
NS_CATEGORY = 14,
|
||||
CURRENT_NS = config.get( 'wgNamespaceNumber' ),
|
||||
Icon = M.require( 'mobile.startup/Icon' ),
|
||||
pageIssuesLogger = M.require( 'skins.minerva.scripts/pageIssuesLogger' ),
|
||||
pageIssuesParser = M.require( 'skins.minerva.scripts/pageIssuesParser' ),
|
||||
PageIssuesOverlay = M.require( 'skins.minerva.scripts/PageIssuesOverlay' ),
|
||||
// setup ab test
|
||||
abTest = new AB( {
|
||||
testName: 'WME.PageIssuesAB',
|
||||
// Run AB only on article namespace, otherwise set samplingRate to 0,
|
||||
// forcing user into control (i.e. ignored/not logged) group.
|
||||
samplingRate: ( CURRENT_NS === NS_MAIN ) ? config.get( 'wgMinervaABSamplingRate', 0 ) : 0,
|
||||
sessionId: user.sessionId()
|
||||
} ),
|
||||
QUERY_STRING_FLAG = mw.util.getParamValue( 'minerva-issues' ),
|
||||
// Per T204746 a user can request the new treatment regardless of test group
|
||||
isUserRequestingNewTreatment = QUERY_STRING_FLAG === 'b',
|
||||
newTreatmentEnabled = abTest.isB() || isUserRequestingNewTreatment;
|
||||
|
||||
/**
|
||||
* @typedef {Object} IssueSummary
|
||||
* @prop {string} severity A PageIssue.severity.
|
||||
* @prop {Boolean} isMultiple Whether or not the issue is part of a "multiple issues" template.
|
||||
* @prop {string} icon HTML string.
|
||||
* @prop {string} text HTML string.
|
||||
*/
|
||||
|
||||
function isLoggingRequired( pageIssues ) {
|
||||
// No logging necessary when the A/B test is disabled (control group).
|
||||
return !isUserRequestingNewTreatment && abTest.isEnabled() && pageIssues.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array.reduce callback that returns the severity of page issues.
|
||||
* In the case that a page-issue is part of a "multiple issues" template,
|
||||
* returns the maximum severity for that group of issues.
|
||||
*
|
||||
* @param {array} formattedArr - the return array containing severities
|
||||
* @param {IssueSummary} currentItem current IssueSummary object
|
||||
* @param {number} currentIndex current index of pageIssues
|
||||
* @param {array} pageIssues array of pageIssues
|
||||
*
|
||||
* @return {array} acc
|
||||
*/
|
||||
function formatPageIssuesSeverity( formattedArr, currentItem, currentIndex, pageIssues ) {
|
||||
var lastItem = pageIssues[ currentIndex - 1 ],
|
||||
lastFormattedIndex = formattedArr.length - 1,
|
||||
lastFormattedValue = formattedArr[ lastFormattedIndex ];
|
||||
// If the last and current item `isMultiple`, fold the maxSeverity
|
||||
// of the two items into a single value.
|
||||
if ( lastItem && lastItem.isMultiple && currentItem.isMultiple ) {
|
||||
formattedArr[ lastFormattedIndex ] = pageIssuesParser.maxSeverity(
|
||||
[ lastFormattedValue, currentItem.severity ]
|
||||
);
|
||||
} else {
|
||||
formattedArr.push( currentItem.severity );
|
||||
}
|
||||
return formattedArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a summary message from a cleanup template generated element that is
|
||||
* friendly for mobile display.
|
||||
* @param {Object} $box element to extract the message from
|
||||
* @return {IssueSummary}
|
||||
*/
|
||||
function extractMessage( $box ) {
|
||||
var SELECTOR = '.mbox-text, .ambox-text',
|
||||
MULTIPLE_SELECTOR = '.mw-collapsible-content',
|
||||
$container = $( '<div>' ),
|
||||
pageIssue;
|
||||
|
||||
$box.find( SELECTOR ).each( function () {
|
||||
var contents,
|
||||
$this = $( this );
|
||||
// Clean up talk page boxes
|
||||
$this.find( 'table, .noprint' ).remove();
|
||||
contents = $this.html();
|
||||
|
||||
if ( contents ) {
|
||||
$( '<p>' ).html( contents ).appendTo( $container );
|
||||
}
|
||||
} );
|
||||
|
||||
pageIssue = pageIssuesParser.parse( $box.get( 0 ) );
|
||||
|
||||
return {
|
||||
severity: pageIssue.severity,
|
||||
isMultiple: $box.parent().is( MULTIPLE_SELECTOR ),
|
||||
icon: pageIssue.icon.toHtmlString(),
|
||||
text: $container.html()
|
||||
};
|
||||
}
|
||||
// T206179 should update this value to enable it
|
||||
newTreatmentEnabled = QUERY_STRING_FLAG === 'b';
|
||||
|
||||
/**
|
||||
* Create a link element that opens the issues overlay.
|
||||
|
@ -138,8 +50,7 @@
|
|||
issueUrl = section === KEYWORD_ALL_SECTIONS ? '#/issues/' + KEYWORD_ALL_SECTIONS : '#/issues/' + section,
|
||||
selector = 'table.ambox, table.tmbox, table.cmbox, table.fmbox',
|
||||
issues = [],
|
||||
$link,
|
||||
severity;
|
||||
$link;
|
||||
|
||||
if ( section === KEYWORD_ALL_SECTIONS ) {
|
||||
$metadata = page.$( selector );
|
||||
|
@ -154,8 +65,8 @@
|
|||
$this = $( this );
|
||||
|
||||
if ( $this.find( selector ).length === 0 ) {
|
||||
issue = extractMessage( $this );
|
||||
// Some issues after "extractMessage" has been run will have no text.
|
||||
issue = pageIssuesParser.extract( $this );
|
||||
// Some issues after "extract" has been run will have no text.
|
||||
// For example in Template:Talk header the table will be removed and no issue found.
|
||||
// These should not be rendered.
|
||||
if ( issue.text ) {
|
||||
|
@ -166,14 +77,10 @@
|
|||
// store it for later
|
||||
allIssues[section] = issues;
|
||||
|
||||
if ( $metadata.length && inline ) {
|
||||
severity = pageIssuesParser.maxSeverity(
|
||||
issues.map( function ( issue ) { return issue.severity; } )
|
||||
);
|
||||
new Icon( {
|
||||
glyphPrefix: 'minerva',
|
||||
name: pageIssuesParser.iconName( $metadata.get( 0 ), severity )
|
||||
} ).prependTo( $metadata.find( '.mbox-text' ) );
|
||||
// If issues were extracted and there are inline amboxes, add learn more
|
||||
// and icon to the UI element.
|
||||
if ( issues.length && $metadata.length && inline ) {
|
||||
issues[0].issue.icon.$el.prependTo( $metadata.eq( 0 ).find( '.mbox-text' ) );
|
||||
$learnMore = $( '<span>' )
|
||||
.addClass( 'ambox-learn-more' )
|
||||
.text( mw.msg( 'skin-minerva-issue-learn-more' ) );
|
||||
|
@ -185,34 +92,12 @@
|
|||
$learnMore.appendTo( $metadata.find( '.mbox-text' ) );
|
||||
}
|
||||
$metadata.click( function () {
|
||||
var pageIssue = pageIssuesParser.parse( this );
|
||||
pageIssuesLogger.log( {
|
||||
action: 'issueClicked',
|
||||
issuesSeverity: [ pageIssue.severity ],
|
||||
sectionNumbers: [ section ]
|
||||
} );
|
||||
overlayManager.router.navigate( issueUrl );
|
||||
return false;
|
||||
} );
|
||||
} else {
|
||||
$link = createLinkElement( labelText );
|
||||
$link.attr( 'href', '#/issues/' + section );
|
||||
$link.click( function () {
|
||||
pageIssuesLogger.log( {
|
||||
action: 'issueClicked',
|
||||
issuesSeverity: [
|
||||
pageIssuesParser.maxSeverity(
|
||||
getIssues( '0' )
|
||||
.map( function ( issue ) { return issue.severity; } )
|
||||
)
|
||||
],
|
||||
// In the old treatment, an issuesClicked event will always be '0'
|
||||
// as the old treatment is always associated with the lead section and we
|
||||
// are only sending one maximum severity for all of them.
|
||||
// An issuesClicked event should only ever be associated with one issue box.
|
||||
sectionNumbers: [ '0' ]
|
||||
} );
|
||||
} );
|
||||
if ( $metadata.length ) {
|
||||
$link.insertAfter( $( 'h1#section_0' ) );
|
||||
$metadata.remove();
|
||||
|
@ -257,7 +142,7 @@
|
|||
var lastIssue = allIssues[ section ][i - 1];
|
||||
// If the last issue belongs to a "Multiple issues" template,
|
||||
// and so does the current one, don't add the current one.
|
||||
if ( lastIssue && lastIssue.isMultiple && issue.isMultiple ) {
|
||||
if ( lastIssue && lastIssue.grouped && issue.grouped ) {
|
||||
acc[ acc.length - 1 ] = section;
|
||||
} else {
|
||||
acc.push( section );
|
||||
|
@ -316,40 +201,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
if ( isLoggingRequired( getIssues( KEYWORD_ALL_SECTIONS ) ) ) {
|
||||
// Enable logging of the PageIssues schema, setting up defaults.
|
||||
pageIssuesLogger.subscribe(
|
||||
newTreatmentEnabled,
|
||||
pageIssuesLogger.newPageIssueSchemaData(
|
||||
newTreatmentEnabled,
|
||||
CURRENT_NS,
|
||||
getIssues( KEYWORD_ALL_SECTIONS ).reduce( formatPageIssuesSeverity, [] ),
|
||||
getAllIssuesSections( allIssues )
|
||||
)
|
||||
);
|
||||
|
||||
// Report that the page has been loaded.
|
||||
pageIssuesLogger.log( {
|
||||
action: 'pageLoaded'
|
||||
} );
|
||||
}
|
||||
|
||||
// Setup the overlay route.
|
||||
overlayManager.add( new RegExp( '^/issues/(\\d+|' + KEYWORD_ALL_SECTIONS + ')$' ), function ( section ) {
|
||||
return new PageIssuesOverlay(
|
||||
getIssues( section ), pageIssuesLogger, section, CURRENT_NS );
|
||||
getIssues( section ), section, CURRENT_NS );
|
||||
} );
|
||||
}
|
||||
|
||||
M.define( 'skins.minerva.scripts/pageIssues', {
|
||||
init: initPageIssues,
|
||||
// The logger requires initialization (subscription). Ideally, the logger would be
|
||||
// initialized and passed to initPageIssues() by the client. Since it's not, expose a log
|
||||
// method and hide the subscription call in cleanuptemplates.
|
||||
log: pageIssuesLogger.log,
|
||||
test: {
|
||||
formatPageIssuesSeverity: formatPageIssuesSeverity,
|
||||
extractMessage: extractMessage,
|
||||
getAllIssuesSections: getAllIssuesSections,
|
||||
createBanner: createBanner
|
||||
}
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
( function ( M, mwConfig, mwTrack, mwTrackSubscribe, mwUser ) {
|
||||
var
|
||||
util = M.require( 'mobile.startup/util' ),
|
||||
EVENT_PAGE_ISSUE_LOG = 'minerva.PageIssuesAB';
|
||||
|
||||
/**
|
||||
* Defines default data for Schema:PageIssues that will be recorded with every event.
|
||||
* @param {boolean} newTreatmentEnabled
|
||||
* @param {number} namespaceId The namespace for the page that has issues.
|
||||
* @param {string[]} pageIssueSeverities An array of PageIssue severities.
|
||||
* @param {array} pageIssuesSections
|
||||
*
|
||||
* @return {Object} A Partial<Schema:PageIssues> Object meant to be mixed with track data.
|
||||
*/
|
||||
function newPageIssueSchemaData(
|
||||
newTreatmentEnabled, namespaceId, pageIssueSeverities, pageIssuesSections
|
||||
) {
|
||||
return {
|
||||
pageTitle: mwConfig.get( 'wgTitle' ),
|
||||
namespaceId: namespaceId,
|
||||
pageIdSource: mwConfig.get( 'wgArticleId' ),
|
||||
issuesVersion: bucketToVersion( newTreatmentEnabled ),
|
||||
issuesSeverity: pageIssueSeverities,
|
||||
sectionNumbers: pageIssuesSections,
|
||||
isAnon: mwUser.isAnon(),
|
||||
editCountBucket: getUserEditBuckets(),
|
||||
sessionToken: mwUser.sessionId()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable tracking and add page token to every logged event.
|
||||
* @param {boolean} newTreatmentEnabled
|
||||
* @param {Object} pageIssueSchemaData A Partial<Schema:PageIssues> Object that will be mixed
|
||||
* with track data.
|
||||
* @return {void}
|
||||
*/
|
||||
function subscribe( newTreatmentEnabled, pageIssueSchemaData ) {
|
||||
// set the page token on the request.
|
||||
pageIssueSchemaData.pageToken = mw.user.getPageviewToken();
|
||||
|
||||
// intermediary event bus that extends the event data before being passed to event-logging.
|
||||
mwTrackSubscribe( EVENT_PAGE_ISSUE_LOG, function ( topic, data ) {
|
||||
var mixedData = util.extend( {}, pageIssueSchemaData, data );
|
||||
|
||||
// Although we use strings inside the issues code (due to the usage of the key word
|
||||
// `all`) - these need to be numbers to be validated by the schema
|
||||
mixedData.sectionNumbers = ( mixedData.sectionNumbers || [] ).map(
|
||||
function ( sectionStr ) { return parseInt( sectionStr, 10 ); }
|
||||
);
|
||||
|
||||
// Log readingDepth schema.(ReadingDepth is guarded against multiple enables).
|
||||
// See https://gerrit.wikimedia.org/r/#/c/mediawiki/extensions/WikimediaEvents/+/437686/
|
||||
mwTrack( 'wikimedia.ReadingDepthSchema.enable', bucketToGroup( newTreatmentEnabled ) );
|
||||
// Log PageIssues schema.
|
||||
mwTrack( 'wikimedia.event.PageIssues', mixedData );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} newTreatmentEnabled
|
||||
* @return {string} The page issues group associated with the treatment bucket.
|
||||
*/
|
||||
function bucketToGroup( newTreatmentEnabled ) {
|
||||
return newTreatmentEnabled ? 'page-issues-b_sample' : 'page-issues-a_sample';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} newTreatmentEnabled
|
||||
* @return {string} The page issues version associated with the treatment bucket.
|
||||
*/
|
||||
function bucketToVersion( newTreatmentEnabled ) {
|
||||
return newTreatmentEnabled ? 'new2018' : 'old';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts user edit count into a predefined string. Note: these buckets have *nothing* to do
|
||||
* with A/B bucketing.
|
||||
* @return {string}
|
||||
*/
|
||||
function getUserEditBuckets() {
|
||||
var editCount = mwConfig.get( 'wgUserEditCount', 0 );
|
||||
|
||||
if ( editCount === 0 ) { return '0 edits'; }
|
||||
if ( editCount < 5 ) { return '1-4 edits'; }
|
||||
if ( editCount < 100 ) { return '5-99 edits'; }
|
||||
if ( editCount < 1000 ) { return '100-999 edits'; }
|
||||
if ( editCount >= 1000 ) { return '1000+ edits'; }
|
||||
|
||||
// This is unlikely to ever happen. If so, we'll want to cast to a string
|
||||
// that is not accepted and allow EventLogging to complain
|
||||
// about invalid events so we can investigate.
|
||||
return 'error (' + editCount + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log data to the PageIssuesAB test schema. It's safe to call this function prior to
|
||||
* subscription.
|
||||
* @param {Object} data to log
|
||||
* @return {void}
|
||||
*/
|
||||
function log( data ) {
|
||||
mwTrack( EVENT_PAGE_ISSUE_LOG, data );
|
||||
}
|
||||
|
||||
M.define( 'skins.minerva.scripts/pageIssuesLogger', {
|
||||
newPageIssueSchemaData: newPageIssueSchemaData,
|
||||
subscribe: subscribe,
|
||||
log: log
|
||||
} );
|
||||
}( mw.mobileFrontend, mw.config, mw.track, mw.trackSubscribe, mw.user ) );
|
|
@ -2,8 +2,16 @@
|
|||
/**
|
||||
* @typedef PageIssue
|
||||
* @prop {string} severity A SEVERITY_LEVEL key.
|
||||
* @prop {boolean} grouped True if part of a group of multiple issues, false if singular.
|
||||
* @prop {Icon} icon
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object} IssueSummary
|
||||
* @prop {PageIssue} issue
|
||||
* @prop {string} iconString a string representation of icon.
|
||||
* This is kept for template compatibility (our views do not yet support composition).
|
||||
* @prop {string} text HTML string.
|
||||
*/
|
||||
|
||||
var Icon = M.require( 'mobile.startup/Icon' ),
|
||||
// Icons are matching the type selector below use a TYPE_* icon. When unmatched, the icon is
|
||||
|
@ -78,6 +86,7 @@
|
|||
].join( '|' ) )
|
||||
// ..And everything else that doesn't match is mapped to a "SEVERITY" type.
|
||||
},
|
||||
GROUPED_PARENT_REGEX = /mw-collapsible-content/,
|
||||
// Variants supported by specific types. The "severity icon" supports all severities but the
|
||||
// type icons only support one each by ResourceLoader.
|
||||
TYPE_SEVERITY = {
|
||||
|
@ -117,6 +126,14 @@
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} box
|
||||
* @return {boolean} True if part of a group of multiple issues, false if singular.
|
||||
*/
|
||||
function parseGroup( box ) {
|
||||
return !!box.parentNode && GROUPED_PARENT_REGEX.test( box.parentNode.className );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} box
|
||||
* @param {string} severity An SEVERITY_LEVEL key.
|
||||
|
@ -147,6 +164,7 @@
|
|||
var severity = parseSeverity( box );
|
||||
return {
|
||||
severity: severity,
|
||||
grouped: parseGroup( box ),
|
||||
icon: new Icon( {
|
||||
glyphPrefix: 'minerva',
|
||||
name: iconName( box, severity )
|
||||
|
@ -154,21 +172,51 @@
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a summary message from a cleanup template generated element that is
|
||||
* friendly for mobile display.
|
||||
* @param {Object} $box element to extract the message from
|
||||
* @return {IssueSummary}
|
||||
*/
|
||||
function extract( $box ) {
|
||||
var SELECTOR = '.mbox-text, .ambox-text',
|
||||
$container = $( '<div>' ),
|
||||
pageIssue;
|
||||
|
||||
$box.find( SELECTOR ).each( function () {
|
||||
var contents,
|
||||
$this = $( this );
|
||||
// Clean up talk page boxes
|
||||
$this.find( 'table, .noprint' ).remove();
|
||||
contents = $this.html();
|
||||
|
||||
if ( contents ) {
|
||||
$( '<p>' ).html( contents ).appendTo( $container );
|
||||
}
|
||||
} );
|
||||
|
||||
pageIssue = parse( $box.get( 0 ) );
|
||||
|
||||
return {
|
||||
issue: pageIssue,
|
||||
// For template compatibility with PageIssuesOverlay
|
||||
iconString: pageIssue.icon.toHtmlString(),
|
||||
text: $container.html()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @module skins.minerva.scripts/utils
|
||||
*/
|
||||
M.define( 'skins.minerva.scripts/pageIssuesParser', {
|
||||
/**
|
||||
* Extract an icon for use with the issue.
|
||||
* @param {JQuery.Object} $box element to extract the icon from
|
||||
* @return {Icon} representing the icon
|
||||
*/
|
||||
extract: extract,
|
||||
parse: parse,
|
||||
maxSeverity: maxSeverity,
|
||||
iconName: iconName,
|
||||
test: {
|
||||
parseSeverity: parseSeverity,
|
||||
parseType: parseType
|
||||
parseType: parseType,
|
||||
parseGroup: parseGroup
|
||||
}
|
||||
} );
|
||||
|
||||
|
|
|
@ -155,7 +155,6 @@
|
|||
}
|
||||
},
|
||||
"EventLoggingSchemas": {
|
||||
"PageIssues": 18392542,
|
||||
"WebClientError": 18340282
|
||||
},
|
||||
"ResourceModules": {
|
||||
|
@ -435,7 +434,6 @@
|
|||
"resources/skins.minerva.scripts/errorLogging.js",
|
||||
"resources/skins.minerva.scripts/preInit.js",
|
||||
"resources/skins.minerva.scripts/DownloadIcon.js",
|
||||
"resources/skins.minerva.scripts/pageIssuesLogger.js",
|
||||
"resources/skins.minerva.scripts/pageIssuesParser.js",
|
||||
"resources/skins.minerva.scripts/AB.js",
|
||||
"resources/skins.minerva.scripts/PageIssuesOverlay.js",
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
( function ( M ) {
|
||||
var PageIssuesOverlay = M.require( 'skins.minerva.scripts/PageIssuesOverlay' );
|
||||
|
||||
QUnit.module( 'Minerva PageIssuesOverlay', {
|
||||
beforeEach: function () {
|
||||
this.logger = {
|
||||
log: this.sandbox.spy()
|
||||
};
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( '#log (section=all)', function ( assert ) {
|
||||
var overlay = new PageIssuesOverlay( [], this.logger, 'all', 0 );
|
||||
overlay.onExit();
|
||||
assert.strictEqual( this.logger.log.calledOnce, true, 'Logger called once' );
|
||||
assert.strictEqual(
|
||||
this.logger.log.calledWith( {
|
||||
action: 'modalClose',
|
||||
issuesSeverity: []
|
||||
} ), true, 'sectionNumbers is not set (T202940)'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( '#log (section=1)', function ( assert ) {
|
||||
var overlay = new PageIssuesOverlay( [
|
||||
{
|
||||
severity: 'MEDIUM'
|
||||
}
|
||||
], this.logger, '1', 0 );
|
||||
overlay.onExit();
|
||||
assert.strictEqual(
|
||||
this.logger.log.calledWith( {
|
||||
action: 'modalClose',
|
||||
issuesSeverity: [ 'MEDIUM' ],
|
||||
sectionNumbers: [ '1' ]
|
||||
} ), true, 'sectionNumbers is set'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( '#log (section=2) multiple issues', function ( assert ) {
|
||||
var overlay = new PageIssuesOverlay(
|
||||
[ {
|
||||
severity: 'MEDIUM'
|
||||
},
|
||||
{
|
||||
severity: 'LOW'
|
||||
} ], this.logger, '2', 0 );
|
||||
overlay.onExit();
|
||||
assert.strictEqual(
|
||||
this.logger.log.calledWith( {
|
||||
action: 'modalClose',
|
||||
issuesSeverity: [ 'MEDIUM', 'LOW' ],
|
||||
sectionNumbers: [ '2', '2' ]
|
||||
} ), true, 'sectionNumbers is set for each issue'
|
||||
);
|
||||
} );
|
||||
}( mw.mobileFrontend ) );
|
|
@ -1,35 +1,30 @@
|
|||
( function ( M ) {
|
||||
var pageIssues = M.require( 'skins.minerva.scripts/pageIssues' ),
|
||||
util = M.require( 'mobile.startup/util' ),
|
||||
pageIssuesParser = M.require( 'skins.minerva.scripts/pageIssuesParser' ),
|
||||
extractMessage = pageIssues.test.extractMessage,
|
||||
createBanner = pageIssues.test.createBanner,
|
||||
formatPageIssuesSeverity = pageIssues.test.formatPageIssuesSeverity,
|
||||
icon = {},
|
||||
MEDIUM_ISSUE = {
|
||||
issue: {
|
||||
severity: 'MEDIUM',
|
||||
icon: 'i',
|
||||
text: 't'
|
||||
icon: icon
|
||||
},
|
||||
MEDIUM_MULTIPLE_ISSUE = {
|
||||
severity: 'MEDIUM',
|
||||
isMultiple: true,
|
||||
icon: 'i',
|
||||
text: 't'
|
||||
},
|
||||
LOW_MULTIPLE_ISSUE = {
|
||||
severity: 'LOW',
|
||||
isMultiple: true,
|
||||
icon: 'i',
|
||||
iconString: 'i',
|
||||
text: 't'
|
||||
},
|
||||
LOW_ISSUE = {
|
||||
issue: {
|
||||
severity: 'LOW',
|
||||
icon: 'i',
|
||||
icon: icon
|
||||
},
|
||||
iconString: 'i',
|
||||
text: 't'
|
||||
},
|
||||
HIGH_ISSUE = {
|
||||
issue: {
|
||||
severity: 'HIGH',
|
||||
icon: 'i',
|
||||
icon: icon
|
||||
},
|
||||
iconString: 'i',
|
||||
text: 't'
|
||||
},
|
||||
getAllIssuesSections = pageIssues.test.getAllIssuesSections,
|
||||
|
@ -67,93 +62,6 @@
|
|||
assert.strictEqual( window.location.hash, '#/issues/' + SECTION );
|
||||
} );
|
||||
|
||||
// NOTE: Only for PageIssues AB
|
||||
QUnit.test( 'clicking on the product of createBanner() should trigger a custom event', function ( assert ) {
|
||||
var mockAction = {
|
||||
action: 'issueClicked',
|
||||
issuesSeverity: [ 'MEDIUM' ],
|
||||
sectionNumbers: [ SECTION ]
|
||||
};
|
||||
mw.trackSubscribe( 'minerva.PageIssuesAB', function ( topic, data ) {
|
||||
assert.deepEqual( mockAction, data );
|
||||
} );
|
||||
processedAmbox.click();
|
||||
} );
|
||||
|
||||
QUnit.test( 'formatPageIssuesSeverity', function ( assert ) {
|
||||
var multipleIssues = [
|
||||
MEDIUM_MULTIPLE_ISSUE,
|
||||
LOW_MULTIPLE_ISSUE,
|
||||
LOW_MULTIPLE_ISSUE
|
||||
],
|
||||
multipleSingleIssues = [
|
||||
LOW_ISSUE,
|
||||
HIGH_ISSUE,
|
||||
MEDIUM_ISSUE
|
||||
],
|
||||
mixedMultipleSingle = [
|
||||
HIGH_ISSUE,
|
||||
LOW_MULTIPLE_ISSUE,
|
||||
MEDIUM_MULTIPLE_ISSUE,
|
||||
LOW_ISSUE,
|
||||
MEDIUM_ISSUE,
|
||||
HIGH_ISSUE
|
||||
],
|
||||
testMultiple = multipleIssues.reduce( formatPageIssuesSeverity, [] ),
|
||||
testSingle = multipleSingleIssues.reduce( formatPageIssuesSeverity, [] ),
|
||||
testMixed = mixedMultipleSingle.reduce( formatPageIssuesSeverity, [] );
|
||||
|
||||
assert.deepEqual( testMultiple, [ 'MEDIUM' ], 'Multiple issues return one maxSeverity value' );
|
||||
assert.deepEqual( testSingle, [ 'LOW', 'HIGH', 'MEDIUM' ], 'Single issues return each corresponding severity' );
|
||||
assert.deepEqual( testMixed, [ 'HIGH', 'MEDIUM', 'LOW', 'MEDIUM', 'HIGH' ], 'Mixed single/multiple return one value for multiples' );
|
||||
|
||||
} );
|
||||
|
||||
QUnit.test( 'extractMessage', function ( assert ) {
|
||||
this.sandbox.stub( pageIssuesParser, 'parse' ).returns(
|
||||
{
|
||||
severity: 'LOW',
|
||||
icon: {
|
||||
toHtmlString: function () {
|
||||
return '<icon />';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
[
|
||||
[
|
||||
$( '<div />' ).html(
|
||||
'<div class="mbox-text">Smelly</div>'
|
||||
).appendTo( '<div class="mw-collapsible-content" />' ),
|
||||
{
|
||||
severity: 'LOW',
|
||||
isMultiple: true,
|
||||
icon: '<icon />',
|
||||
text: '<p>Smelly</p>'
|
||||
},
|
||||
'When the box is a child of mw-collapsible-content it isMultiple'
|
||||
],
|
||||
[
|
||||
$( '<div />' ).html(
|
||||
'<div class="mbox-text">Dirty</div>'
|
||||
),
|
||||
{
|
||||
severity: 'LOW',
|
||||
isMultiple: false,
|
||||
icon: '<icon />',
|
||||
text: '<p>Dirty</p>'
|
||||
},
|
||||
'When the box is not child of mw-collapsible-content it !isMultiple'
|
||||
]
|
||||
].forEach( function ( test ) {
|
||||
assert.deepEqual(
|
||||
extractMessage( test[ 0 ] ),
|
||||
test[ 1 ],
|
||||
test[ 2 ]
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'getAllIssuesSections', function ( assert ) {
|
||||
var multipleIssuesWithDeletion,
|
||||
multipleIssues, allIssuesOldTreatment, allIssuesNewTreatment;
|
||||
|
@ -166,17 +74,17 @@
|
|||
};
|
||||
multipleIssues = {
|
||||
0: [
|
||||
util.extend( {}, MEDIUM_ISSUE, { isMultiple: true } ),
|
||||
util.extend( {}, LOW_ISSUE, { isMultiple: true } ),
|
||||
util.extend( {}, MEDIUM_ISSUE, { isMultiple: true } )
|
||||
util.extend( {}, MEDIUM_ISSUE, { grouped: true } ),
|
||||
util.extend( {}, LOW_ISSUE, { grouped: true } ),
|
||||
util.extend( {}, MEDIUM_ISSUE, { grouped: true } )
|
||||
]
|
||||
};
|
||||
multipleIssuesWithDeletion = {
|
||||
0: [
|
||||
HIGH_ISSUE,
|
||||
util.extend( {}, MEDIUM_ISSUE, { isMultiple: true } ),
|
||||
util.extend( {}, LOW_ISSUE, { isMultiple: true } ),
|
||||
util.extend( {}, MEDIUM_ISSUE, { isMultiple: true } )
|
||||
util.extend( {}, MEDIUM_ISSUE, { grouped: true } ),
|
||||
util.extend( {}, LOW_ISSUE, { grouped: true } ),
|
||||
util.extend( {}, MEDIUM_ISSUE, { grouped: true } )
|
||||
]
|
||||
};
|
||||
allIssuesNewTreatment = {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
( function ( M ) {
|
||||
var pageIssuesParser = M.require( 'skins.minerva.scripts/pageIssuesParser' );
|
||||
var icon = {},
|
||||
pageIssuesParser = M.require( 'skins.minerva.scripts/pageIssuesParser' ),
|
||||
extractMessage = pageIssuesParser.extract;
|
||||
|
||||
QUnit.module( 'Minerva pageIssuesParser' );
|
||||
|
||||
|
@ -13,6 +15,47 @@
|
|||
return box;
|
||||
}
|
||||
|
||||
QUnit.test( 'extractMessage', function () {
|
||||
[
|
||||
[
|
||||
$( '<div />' ).html(
|
||||
'<div class="mbox-text">Smelly</div>'
|
||||
).appendTo( '<div class="mw-collapsible-content" />' ),
|
||||
{
|
||||
issue: {
|
||||
severity: 'DEFAULT',
|
||||
grouped: true,
|
||||
icon: icon
|
||||
},
|
||||
iconString: this.sandbox.match.typeOf( 'string' ),
|
||||
text: '<p>Smelly</p>'
|
||||
},
|
||||
'When the box is a child of mw-collapsible-content it grouped'
|
||||
],
|
||||
[
|
||||
$( '<div />' ).html(
|
||||
'<div class="mbox-text">Dirty</div>'
|
||||
),
|
||||
{
|
||||
issue: {
|
||||
severity: 'DEFAULT',
|
||||
grouped: false,
|
||||
icon: icon
|
||||
},
|
||||
iconString: this.sandbox.match.typeOf( 'string' ),
|
||||
text: '<p>Dirty</p>'
|
||||
},
|
||||
'When the box is not child of mw-collapsible-content it !grouped'
|
||||
]
|
||||
].forEach( function ( test ) {
|
||||
sinon.assert.match( // eslint-disable-line no-undef
|
||||
extractMessage( test[ 0 ] ),
|
||||
test[ 1 ],
|
||||
test[ 2 ]
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'parseSeverity', function ( assert ) {
|
||||
var tests = [
|
||||
[ '', 'DEFAULT', 'empty' ],
|
||||
|
@ -70,6 +113,32 @@
|
|||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'parseGroup', function ( assert ) {
|
||||
var tests = [
|
||||
[ undefined, false, 'orphaned' ],
|
||||
[ '', false, 'ungrouped' ],
|
||||
[ 'mw-collapsible-content', true, 'grouped' ]
|
||||
];
|
||||
tests.forEach( function ( params, i ) {
|
||||
var
|
||||
parentClassName = params[0],
|
||||
expect = params[1],
|
||||
test = params[2],
|
||||
parent,
|
||||
box = newBox( '' );
|
||||
if ( parentClassName !== undefined ) {
|
||||
parent = document.createElement( 'div' );
|
||||
parent.className = parentClassName;
|
||||
parent.appendChild( box );
|
||||
}
|
||||
assert.strictEqual(
|
||||
pageIssuesParser.test.parseGroup( box ),
|
||||
expect,
|
||||
'Result should be the correct grouping; case ' + i + ': ' + test + '.'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'iconName', function ( assert ) {
|
||||
var tests = [
|
||||
[ '', 'DEFAULT', 'issue-generic-defaultColor' ],
|
||||
|
|
Loading…
Reference in New Issue