Skip to content

Commit 9275c21

Browse files
committed
Highlight existing link recommendations on article
Bug: T378354 Change-Id: I6622c9e5063ab387a8d6c851e0235c86a044f0a8
1 parent eb338b2 commit 9275c21

File tree

19 files changed

+825
-3
lines changed

19 files changed

+825
-3
lines changed

cypress.config.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { defineConfig } from 'cypress';
22
// eslint-disable-next-line n/no-missing-import
33
import { mwApiCommands } from './cypress/support/MwApiPlugin';
4+
// eslint-disable-next-line n/no-missing-import
5+
import LocalSettingsSetup from './cypress/support/LocalSettingsSetup';
46

57
const envLogDir = process.env.LOG_DIR ? process.env.LOG_DIR + '/GrowthExperiments' : null;
68

@@ -23,6 +25,14 @@ export default defineConfig( {
2325
on( 'task', {
2426
...mwApiCommands( config ),
2527
} );
28+
on( 'before:run', async () => {
29+
LocalSettingsSetup.overrideLocalSettings();
30+
await LocalSettingsSetup.restartPhpFpmService();
31+
} );
32+
on( 'after:run', async () => {
33+
LocalSettingsSetup.restoreLocalSettings();
34+
await LocalSettingsSetup.restartPhpFpmService();
35+
} );
2636
},
2737
defaultCommandTimeout: 20000,
2838
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
describe( 'Surfacing Link recommendations (with api responses stubbed)', () => {
2+
it( 'highlights the results returned by the API', function () {
3+
4+
const articleName = 'Surfacing Link recommendations cypress test page';
5+
cy.fixture( 'LoremIpsum.txt' ).as( 'loremIpsumText' );
6+
7+
cy.task( 'MwApi:CreateUser', { usernamePrefix: 'Alice' } ).then( ( { username, password }: { username: string; password: string } ) => {
8+
9+
cy.task( 'MwApi:Edit', {
10+
username: 'root',
11+
title: articleName,
12+
text: this.loremIpsumText,
13+
summary: 'GrowthExperiments Cypress browser test edit',
14+
} ).then( ( { pageid }: { pageid: number } ) => {
15+
cy.loginViaApi( username, password );
16+
17+
cy.intercept( {
18+
method: 'GET',
19+
pathname: '**/api.php',
20+
query: {
21+
action: 'query',
22+
list: 'linkrecommendations',
23+
lrpageid: `${ pageid }`,
24+
},
25+
}, { fixture: 'LoremIpsumSuggestions.json' } ).as( 'getLinkRecommendations' );
26+
} );
27+
} );
28+
29+
cy.viewport( 'samsung-s10' );
30+
cy.visit( 'index.php?title=' + articleName + '&mobileaction=toggle_view_mobile' );
31+
32+
cy.wait( '@getLinkRecommendations' );
33+
34+
cy.get( '.growth-surfaced-task-button' ).should( 'have.length', 3 );
35+
cy.get( '.growth-surfaced-task-button:first' ).click();
36+
cy.get( '.growth-surfaced-task-button:first' ).should( 'have.class', 'growth-surfaced-task-popup-visible' );
37+
} );
38+
} );

cypress/fixtures/LoremIpsum.txt

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam commodo, nunc sed placerat consequat, massa libero hendrerit nunc, sed ullamcorper magna arcu egestas sapien. Fusce non lacus non lorem rutrum cursus id vitae eros. Sed ut maximus mi. Donec in urna sit amet sapien tincidunt fermentum in in sem. Mauris volutpat sollicitudin ante, nec hendrerit turpis dignissim id. Vestibulum vel velit ac nisi semper feugiat. Sed ultrices magna nibh, nec luctus massa faucibus malesuada. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent feugiat nisl lacinia nisi dictum pulvinar. Donec ac facilisis lacus, vel interdum lorem. Duis nisl turpis, cursus a sagittis vitae, dapibus in ex. Quisque venenatis nulla non arcu sodales, ac sodales mauris sagittis.
2+
3+
Nunc sed ullamcorper urna. Donec eget malesuada nunc, a pharetra orci. Aenean lacinia vitae est vel tincidunt. Donec scelerisque malesuada vestibulum. Ut vulputate at nisi eget ultrices. Etiam ligula ex, sodales sit amet quam at, venenatis mollis nibh. Nam condimentum tortor ut vulputate ultrices. Nulla facilisis mattis tincidunt. Nunc mi dui, facilisis et mi id, porttitor gravida lorem. In nunc tellus, pulvinar eleifend justo nec, volutpat sodales eros. Nullam tristique tincidunt lectus. Cras vitae lectus orci.
4+
5+
Donec sodales pharetra augue maximus aliquet. Curabitur ut convallis neque. Curabitur lobortis nunc porta, vulputate nunc eu, varius lectus. Aliquam aliquam, nunc sed dictum efficitur, purus lacus tristique purus, at tincidunt nulla urna a justo. Aenean non ultrices lorem. In imperdiet sed sem a hendrerit. Nam id quam lorem. Maecenas iaculis augue at tincidunt ornare. Nam sed ultrices augue. Fusce sodales risus id efficitur fringilla. Praesent quis sem turpis. Nullam posuere purus id accumsan aliquet.
6+
7+
Curabitur rhoncus non sapien non eleifend. Nam eu lectus ut tellus facilisis ultricies in vel mauris. Proin porttitor purus eget mauris consectetur, vel dapibus arcu bibendum. Pellentesque rhoncus tellus eros, eget fringilla lacus suscipit ac. Fusce sollicitudin finibus nulla at vulputate. Pellentesque venenatis posuere nisi, ac hendrerit risus semper a. Duis eros urna, ullamcorper eu lobortis eget, molestie luctus felis.
8+
9+
Vestibulum tristique ipsum in quam vehicula ornare. Nulla laoreet efficitur eleifend. Ut lacinia molestie ex nec molestie. Nam vitae velit porttitor, consequat sem in, consectetur enim. Vivamus a erat vitae lectus scelerisque consequat sit amet eget nunc. Phasellus et lectus sit amet est tristique scelerisque eget eu eros. Fusce pulvinar, tortor in laoreet eleifend, ante dui rutrum nibh, eget malesuada erat est venenatis nibh. Etiam viverra tempor purus sed malesuada. Donec pulvinar hendrerit lectus, non consequat mauris tristique sed. Duis id elit enim. Morbi finibus, neque a fringilla tempus, ante purus tristique dolor, id hendrerit mi tellus at velit. Etiam nunc est, rhoncus nec ex ac, tristique ultrices neque. Mauris sed interdum nibh. Duis non felis nisi. Suspendisse porttitor, sapien id convallis tempor, nibh augue tristique odio, ut blandit augue nulla in sem.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"batchcomplete": "",
3+
"query": {
4+
"linkrecommendations": {
5+
"recommendations": [
6+
{
7+
"context_before": "\\n\\n",
8+
"context_after": " dolor sit",
9+
"link_text": "Lorem ipsum",
10+
"link_target": "Lorem ipsum",
11+
"link_index": 0,
12+
"score": 0.6815791726112366,
13+
"wikitext_offset": 2
14+
},
15+
{
16+
"context_before": "em rutrum ",
17+
"context_after": " id vitae ",
18+
"link_text": "cursus",
19+
"link_target": "Cursus",
20+
"link_index": 1,
21+
"score": 0.6178194463253021,
22+
"wikitext_offset": 210
23+
},
24+
{
25+
"context_before": "lentesque ",
26+
"context_after": " morbi tri",
27+
"link_text": "habitant",
28+
"link_target": "Habitants",
29+
"link_index": 2,
30+
"score": 0.6209898352622986,
31+
"wikitext_offset": 502
32+
},
33+
{
34+
"context_before": "venenatis ",
35+
"context_after": " nibh. Nam",
36+
"link_text": "mollis",
37+
"link_target": "Mollis",
38+
"link_index": 3,
39+
"score": 0.3724355399608612,
40+
"wikitext_offset": 1050
41+
},
42+
{
43+
"context_before": "ie luctus ",
44+
"context_after": ".\\n\\nVestibu",
45+
"link_text": "felis",
46+
"link_target": "Felis",
47+
"link_index": 4,
48+
"score": 0.3840920627117157,
49+
"wikitext_offset": 2256
50+
},
51+
{
52+
"context_before": "bh. Etiam ",
53+
"context_after": " tempor pu",
54+
"link_text": "viverra",
55+
"link_target": "Viverra",
56+
"link_index": 5,
57+
"score": 0.6527170181274414,
58+
"wikitext_offset": 2701
59+
}
60+
],
61+
"taskURL": "http://default.mediawiki.mwdd.localhost:8080/wiki/Special:Homepage/newcomertask/277?genewcomertasktoken=dba9a02dd34e1b7aced1b509052501da&geclickid=29a5996a01c8ebc0df1e1bda5c2fcdb0&gesuggestededit=1&getasktype=link-recommendation"
62+
}
63+
}
64+
}

cypress/support/LocalSettingsSetup.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as childProcess from 'child_process';
2+
import * as process from 'process';
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
6+
const phpVersion = process.env.PHP_VERSION;
7+
const phpFpmService = 'php' + phpVersion + '-fpm';
8+
const ip = path.resolve( __dirname + '/../../../../' );
9+
const localSettingsPath = path.resolve( ip + '/LocalSettings.php' );
10+
const localSettingsContents = fs.readFileSync( localSettingsPath, 'utf-8' );
11+
12+
/**
13+
* This is needed in Quibble + Apache (T225218) because we use supervisord to control
14+
* the php-fpm service, and with supervisord you need to restart the php-fpm service
15+
* in order to load updated php code.
16+
*/
17+
async function restartPhpFpmService(): Promise<void> {
18+
if ( !process.env.QUIBBLE_APACHE ) {
19+
return;
20+
}
21+
console.log( 'Restarting ' + phpFpmService );
22+
childProcess.spawnSync(
23+
'service',
24+
[ phpFpmService, 'restart' ],
25+
);
26+
// Ugly hack: Run this twice because sometimes the first invocation hangs.
27+
childProcess.spawnSync(
28+
'service',
29+
[ phpFpmService, 'restart' ],
30+
);
31+
}
32+
33+
/**
34+
* Require the GrowthExperiments.LocalSettings.php in the main LocalSettings.php. Note that you
35+
* need to call restartPhpFpmService for this take effect in a Quibble environment.
36+
*
37+
* @return {true}
38+
*/
39+
function overrideLocalSettings(): true {
40+
console.log( 'Setting up modified ' + localSettingsPath );
41+
fs.writeFileSync( localSettingsPath,
42+
localSettingsContents + `
43+
// Cypress test code (is supposed to be removed after the test suite, safe to delete)
44+
if ( file_exists( "$IP/extensions/GrowthExperiments/cypress/support/setupFixtures/GrowthExperiments.LocalSettings.php" ) ) {
45+
require_once "$IP/extensions/GrowthExperiments/cypress/support/setupFixtures/GrowthExperiments.LocalSettings.php";
46+
}
47+
` );
48+
return true;
49+
}
50+
51+
/**
52+
* Restore the original, unmodified LocalSettings.php.
53+
*
54+
* Note that you need to call restartPhpFpmService for this to take effect in a
55+
* Quibble environment.
56+
*
57+
* @return {true}
58+
*/
59+
function restoreLocalSettings(): true {
60+
console.log( 'Restoring original ' + localSettingsPath );
61+
fs.writeFileSync( localSettingsPath, localSettingsContents );
62+
return true;
63+
}
64+
65+
export default { restartPhpFpmService, overrideLocalSettings, restoreLocalSettings };

cypress/support/MwApiPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function mwApiCommands( cypressConfig: Cypress.PluginConfigOptions ): {
8585
return Promise.reject( new Error( 'edit failed: ' + editResult.edit.result ) );
8686
}
8787

88-
return Promise.resolve( null );
88+
return Promise.resolve( editResult.edit );
8989
},
9090
};
9191
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
$wgGEUseCommunityConfigurationExtension = true;
4+
$wgGENewcomerTasksLinkRecommendationsEnabled = true;
5+
$wgGELinkRecommendationsFrontendEnabled = true;
6+
$wgGESurfacingStructuredTasksEnabled = true;

extension.json

+23-1
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,13 @@
342342
"GrowthExperimentsMentorStatusManager"
343343
]
344344
},
345+
"surfacingStructuredTasksOnBeforePageDisplay": {
346+
"class": "GrowthExperiments\\NewcomerTasks\\SurfacingStructuredTasks\\BeforePageDisplayHookHandler",
347+
"services": [
348+
"MainConfig",
349+
"GrowthExperimentsNewcomerTasksConfigurationLoader"
350+
]
351+
},
345352
"homepage": {
346353
"class": "GrowthExperiments\\HomepageHooks",
347354
"services": [
@@ -513,7 +520,8 @@
513520
"mentordashboarddiscovery",
514521
"tour",
515522
"welcomeSurvey",
516-
"mentor"
523+
"mentor",
524+
"surfacingStructuredTasksOnBeforePageDisplay"
517525
],
518526
"CentralAuthPostLoginRedirect": [
519527
"variant",
@@ -1648,6 +1656,20 @@
16481656
"ext.growthExperiments.DataStore"
16491657
]
16501658
},
1659+
"ext.growthExperiments.StructuredTask.Surfacing": {
1660+
"class": "MediaWiki\\ResourceLoader\\CodexModule",
1661+
"packageFiles": [
1662+
"ext.growthExperiments.StructuredTask.Surfacing/index.js",
1663+
"ext.growthExperiments.StructuredTask.Surfacing/ArticleTextManipulator.js"
1664+
],
1665+
"styles": [
1666+
"ext.growthExperiments.StructuredTask.Surfacing/styles.less"
1667+
],
1668+
"codexStyleOnly": "true",
1669+
"codexComponents": [
1670+
"CdxButton"
1671+
]
1672+
},
16511673
"ext.growthExperiments.Help": {
16521674
"styles": [
16531675
"ext.growthExperiments.Help/HelpPanelProcessDialog.less",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace GrowthExperiments\NewcomerTasks\SurfacingStructuredTasks;
4+
5+
use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
6+
use GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskTypeHandler;
7+
use GrowthExperiments\Util;
8+
use MediaWiki\Config\Config;
9+
use MediaWiki\Output\Hook\BeforePageDisplayHook;
10+
11+
class BeforePageDisplayHookHandler implements BeforePageDisplayHook {
12+
13+
private Config $config;
14+
private ConfigurationLoader $configurationLoader;
15+
16+
public function __construct(
17+
Config $config,
18+
ConfigurationLoader $configurationLoader
19+
) {
20+
$this->config = $config;
21+
$this->configurationLoader = $configurationLoader;
22+
}
23+
24+
/**
25+
* @inheritDoc
26+
*/
27+
public function onBeforePageDisplay( $out, $skin ): void {
28+
if ( !$this->config->get( 'GESurfacingStructuredTasksEnabled' ) ) {
29+
return;
30+
}
31+
32+
$user = $out->getUser();
33+
if ( !$user->isNamed() ) {
34+
return;
35+
}
36+
37+
$page = $out->getTitle();
38+
if ( !$page || $page->getNamespace() !== NS_MAIN ) {
39+
return;
40+
}
41+
42+
$action = $out->getRequest()->getVal( 'action', 'view' );
43+
if ( $action !== 'view' ) {
44+
return;
45+
}
46+
47+
$veaction = $out->getRequest()->getVal( 'veaction', null );
48+
if ( $veaction !== null ) {
49+
return;
50+
}
51+
52+
if ( !Util::isMobile( $skin ) ) {
53+
return;
54+
}
55+
56+
if ( $user->getEditCount() !== 0 ) {
57+
return;
58+
}
59+
60+
$taskTypes = $this->configurationLoader->getTaskTypes();
61+
if ( !isset( $taskTypes[LinkRecommendationTaskTypeHandler::TASK_TYPE_ID] ) ) {
62+
return;
63+
}
64+
65+
$linkRecommendationTaskType = $taskTypes[LinkRecommendationTaskTypeHandler::TASK_TYPE_ID];
66+
'@phan-var \GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskType $linkRecommendationTaskType';
67+
68+
$maxLinks = $linkRecommendationTaskType->getMaximumLinksToShowPerTask();
69+
$minScore = $linkRecommendationTaskType->getMinimumLinkScore();
70+
$out->addJsConfigVars( 'wgGrowthExperimentsLinkRecommendationTask', [
71+
'maxLinks' => $maxLinks,
72+
'minScore' => $minScore,
73+
] );
74+
$out->addModules( 'ext.growthExperiments.StructuredTask.Surfacing' );
75+
}
76+
}

jest.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module.exports = {
3030
collectCoverageFrom: [
3131
'modules/ext.growthExperiments.MentorDashboard/**/*.(js|vue)',
3232
'modules/ext.growthExperiments.Homepage.Impact/**/*.(js|vue)',
33+
'modules/ext.growthExperiments.StructuredTask.Surfacing/**/*.(js|vue)',
3334
'modules/vue-components/**/*.(js|vue)'
3435
],
3536
// The directory where Jest should output its coverage files
@@ -49,6 +50,7 @@ module.exports = {
4950
'./modules/ext.growthExperiments.DataStore',
5051
'./modules/ext.growthExperiments.MentorDashboard',
5152
'./modules/ext.growthExperiments.Homepage.Impact',
53+
'./modules/ext.growthExperiments.StructuredTask.Surfacing',
5254
'./modules/vue-components'
5355
],
5456
setupFiles: [

modules/.eslintrc.json

+20
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"env": {
1313
"commonjs": true
1414
},
15+
"parserOptions": {
16+
"ecmaVersion": 8
17+
},
1518
"rules": {
1619
"max-len": "off"
1720
},
@@ -41,6 +44,23 @@
4144
"env": {
4245
"jest": true
4346
}
47+
},
48+
{
49+
"files": [ "ext.growthExperiments.StructuredTask.Surfacing/**/*.js" ],
50+
"extends": [
51+
"wikimedia/language/es2018"
52+
],
53+
"rules": {
54+
"jsdoc/valid-types": "off",
55+
"es-x/no-async-functions": "off",
56+
"es-x/no-array-prototype-includes": "off",
57+
"es-x/no-trailing-function-commas": "off",
58+
"no-cond-assign": "off",
59+
"comma-dangle": [
60+
"error",
61+
"always-multiline"
62+
]
63+
}
4464
}
4565
]
4666
}

0 commit comments

Comments
 (0)