Skip to content

Commit 3d0e917

Browse files
committed
tests(Cypress): migrate-selenium test for suggested edits navigation on homepage
This is laying the groundwork for migrating the more complex tests link-suggestions and image-suggestions to Cypress (and CC2.0). After considering it more, I decided to keep the pattern of a PageObjectModel for the homepage because this page is sufficiently complex and the identifiers we use are often so obscure that this makes sense to me. This is partly a departure from a (very opinionated) blog article by Cypress: https://www.cypress.io/blog/stop-using-page-objects-and-start-using-app-actions However, that article is mainly about _setting up_ the environment for test, less about the actual test. We still use stable interfaces where possible for setting up the test-specific environment (maintenance scripts, mw.Api for login and setting user options, etc.). Also, the discussion here articulated well some of the concerns I had about that overall approach: https://old.reddit.com/r/softwaretesting/comments/y1j8rq/should_i_use_page_objects_with_cypress/ The pages are extracted from tests/selenium/fixtures/SuggestedEditsContent.xml The `DecoratingTaskSuggesterFactory` has been added, because what is currently in the selenium-version of the thing is causing an error in the javascript console in the browser. Selenium doesn't care about that, but we are intentionally failing Cypress tests on errors in the console. Thus setting it up in a way that is a bit closer to production and thus fulfilling the expectations of other code. Bug: T380581 Change-Id: I608484c5d0565fafea14d4f777e820d0f72d53ad
1 parent 55771a1 commit 3d0e917

File tree

13 files changed

+333
-23
lines changed

13 files changed

+333
-23
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
/cypress/screenshots
1212
/cypress/.cache
1313
.DS_Store
14+
.env

cypress.config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { mwApiCommands } from './cypress/support/MwApiPlugin';
44
// eslint-disable-next-line n/no-missing-import
55
import LocalSettingsSetup from './cypress/support/LocalSettingsSetup';
66
import * as installCypressLogsPrinter from 'cypress-terminal-report/src/installLogsPrinter';
7+
import * as childProcess from 'child_process';
78

89
const envLogDir = process.env.LOG_DIR ? process.env.LOG_DIR + '/GrowthExperiments' : null;
910

@@ -30,6 +31,25 @@ export default defineConfig( {
3031
on( 'before:run', async () => {
3132
LocalSettingsSetup.overrideLocalSettings();
3233
await LocalSettingsSetup.restartPhpFpmService();
34+
35+
const ip = process.env.MW_INSTALL_PATH || '../..';
36+
const command = process.env.MW_MAINTENANCE_COMMAND || 'php';
37+
const maintScriptRunnerArgs = process.env.MW_MAINTENANCE_ARGS ? process.env.MW_MAINTENANCE_ARGS.split( ' ' ) : [ 'maintenance/run.php' ];
38+
const maintScriptResult = childProcess.spawnSync(
39+
command,
40+
[
41+
...maintScriptRunnerArgs,
42+
'GrowthExperiments:PrepareBrowserTests',
43+
],
44+
{ cwd: ip },
45+
);
46+
console.log( 'stdout:' );
47+
console.log( String( maintScriptResult.stdout ) );
48+
console.log( 'stderr:' );
49+
console.log( String( maintScriptResult.stderr ) );
50+
if ( maintScriptResult.status !== 0 ) {
51+
throw new Error( 'unable to run the maintenance script setting up the browser tests' );
52+
}
3353
} );
3454
on( 'after:run', async () => {
3555
LocalSettingsSetup.restoreLocalSettings();

cypress/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@
55
],
66
"parserOptions": {
77
"sourceType": "module"
8+
},
9+
"rules": {
10+
"n/no-missing-import": "off"
811
}
912
}

cypress/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Cypress tests
2+
3+
## Setup
4+
To run Cypress browser tests against a local mediawiki install, set these environment variables. Depending on your local
5+
setup, these might be different.
6+
```bash
7+
export MW_SERVER=http://default.mediawiki.mwdd.localhost:8080/
8+
export MW_SCRIPT_PATH=w/
9+
export MEDIAWIKI_USER=an_admin_username
10+
export MEDIAWIKI_PASSWORD=the_password_for_that_user
11+
```
12+
13+
In running the tests in headless mode requires executing a maintenance script.
14+
If that works for your local setup in a non-standard way (i.e., not using `php` and `maintenance/run.php`),
15+
then you can configure your custom setup with further variables:
16+
```bash
17+
export MW_MAINTENANCE_COMMAND=mw
18+
export MW_MAINTENANCE_ARGS='docker mediawiki mwscript --'
19+
```
20+
21+
## Run the tests
22+
### Headless like in CI
23+
Use this command to run the tests in a terminal:
24+
```bash
25+
npm run cypress:run
26+
```
27+
28+
### With a GUI
29+
First prepare your local system by executing the `cypress/support/PrepareBrowserTestss.php` maintenance script.
30+
This will import some pages and recommendations into the system.
31+
32+
Then open Cypress's GUI with this command:
33+
```bash
34+
npm run cypress:open
35+
```
36+
Some tests complete suggestions and edit pages, so it might be necessary to run the above script again before running
37+
those test again.

cypress/e2e/Homepage.cy.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Homepage from '../pageObjects/SpecialHomepage.page';
2+
3+
const homepage = new Homepage();
4+
5+
describe( 'Special:Homepage', () => {
6+
it( 'Shows a suggested edits card and allows navigation forwards and backwards through queue', () => {
7+
cy.task( 'MwApi:CreateUser', { usernamePrefix: 'Alice' } ).then( ( { username, password }: {
8+
username: string;
9+
password: string;
10+
} ) => {
11+
cy.loginViaApi( username, password );
12+
} );
13+
cy.setUserOptions( {
14+
'growthexperiments-homepage-se-filters': JSON.stringify( [ 'copyedit' ] ),
15+
'growthexperiments-tour-homepage-welcome': '1',
16+
'growthexperiments-tour-homepage-discovery': '1',
17+
} );
18+
19+
cy.visit( 'index.php?title=Special:Homepage' );
20+
21+
homepage.suggestedEditsCardTitle.should( 'have.text', 'Classical kemençe' );
22+
homepage.suggestedEditsPreviousButton.should( 'have.attr', 'aria-disabled', 'true' );
23+
homepage.suggestedEditsNextButton.should( 'not.have.attr', 'aria-disabled' );
24+
homepage.suggestedEditsNextButton.click();
25+
homepage.suggestedEditsCardTitle.should( 'have.text', 'Cretan lyra' );
26+
homepage.suggestedEditsPreviousButton.click();
27+
homepage.suggestedEditsPreviousButton.should( 'have.attr', 'aria-disabled', 'true' );
28+
29+
// Go to the end of queue card.
30+
homepage.suggestedEditsNextButton.click();
31+
homepage.suggestedEditsNextButton.click();
32+
homepage.suggestedEditsCardTitle.should( 'have.text', 'No more suggestions' );
33+
homepage.suggestedEditsNextButton.should( 'have.attr', 'aria-disabled', 'true' );
34+
} );
35+
} );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class SpecialHomepage {
2+
public get suggestedEditsCardTitle(): ReturnType<typeof cy.get> {
3+
return cy.get( '.se-card-title' );
4+
}
5+
6+
public get suggestedEditsPreviousButton(): ReturnType<typeof cy.get> {
7+
return cy.get( '.suggested-edits-previous .oo-ui-buttonElement-button' );
8+
}
9+
10+
public get suggestedEditsNextButton(): ReturnType<typeof cy.get> {
11+
return cy.get( '.suggested-edits-next .oo-ui-buttonElement-button' );
12+
}
13+
}
14+
15+
export default SpecialHomepage;

cypress/support/commands.ts

+10
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ Cypress.Commands.add( 'saveCommunityConfigurationForm', ( editSummary: string ):
4848
cy.contains( 'Your changes were saved' ).should( 'be.visible' );
4949
} );
5050

51+
Cypress.Commands.add( 'setUserOptions', ( options ): void => {
52+
cy.visit( '/index.php' );
53+
cy.window().its( 'mw.Api' ).should( 'exist' );
54+
cy.window().then( async ( { mw }: Cypress.AUTWindow & { mw: MediaWiki } ): Promise<void> => {
55+
const api = new mw.Api();
56+
await api.saveOptions( options );
57+
} );
58+
} );
59+
5160
/* eslint-disable @typescript-eslint/no-namespace */
5261
declare global {
5362
namespace Cypress {
@@ -56,6 +65,7 @@ declare global {
5665
loginAsAdmin(): Chainable<JQuery<HTMLElement>>;
5766
logout(): Chainable<JQuery<HTMLElement>>;
5867
saveCommunityConfigurationForm( editSummary: string ): Chainable<JQuery<HTMLElement>>;
68+
setUserOptions( options: Record<string, string> ): Chainable<JQuery<HTMLElement>>;
5969
}
6070
}
6171
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,65 @@
11
<?php
22

3+
use GrowthExperiments\GrowthExperimentsServices;
4+
use GrowthExperiments\HomepageModules\SuggestedEdits;
5+
use GrowthExperiments\NewcomerTasks\Task\Task;
6+
use GrowthExperiments\NewcomerTasks\TaskSuggester\DecoratingTaskSuggesterFactory;
7+
use GrowthExperiments\NewcomerTasks\TaskSuggester\QualityGateDecorator;
38
use GrowthExperiments\NewcomerTasks\TaskSuggester\StaticTaskSuggesterFactory;
49
use GrowthExperiments\NewcomerTasks\TaskSuggester\TaskSuggesterFactory;
10+
use GrowthExperiments\NewcomerTasks\TaskType\TemplateBasedTaskType;
511
use MediaWiki\MediaWikiServices;
12+
use MediaWiki\Title\TitleValue;
613

714
$wgGEUseCommunityConfigurationExtension = true;
815
$wgGENewcomerTasksLinkRecommendationsEnabled = true;
916
$wgGELinkRecommendationsFrontendEnabled = true;
1017
$wgGESurfacingStructuredTasksEnabled = true;
1118

1219
$wgHooks['MediaWikiServices'][] = static function ( MediaWikiServices $services ) {
20+
$copyEditTaskType = new TemplateBasedTaskType(
21+
'copyedit',
22+
GrowthExperiments\NewcomerTasks\TaskType\TaskType::DIFFICULTY_EASY,
23+
[],
24+
[ new TitleValue( NS_MAIN, 'Awkward' ) ]
25+
);
26+
1327
# Mock the task suggester to specify what article(s) will be suggested.
1428
$services->redefineService(
1529
'GrowthExperimentsTaskSuggesterFactory',
16-
static function () use ( $services ): TaskSuggesterFactory {
17-
return new StaticTaskSuggesterFactory( [], $services->getTitleFactory() );
30+
static function () use (
31+
$copyEditTaskType,
32+
$services
33+
): TaskSuggesterFactory {
34+
$staticSuggesterFactory = new StaticTaskSuggesterFactory( [
35+
new Task( $copyEditTaskType, new TitleValue( NS_MAIN, 'Classical kemençe' ) ),
36+
new Task( $copyEditTaskType, new TitleValue( NS_MAIN, 'Cretan lyra' ) )
37+
], $services->getTitleFactory() );
38+
39+
$growthServices = GrowthExperimentsServices::wrap( $services );
40+
$taskSuggesterFactory = new DecoratingTaskSuggesterFactory(
41+
$staticSuggesterFactory,
42+
$services->getObjectFactory(),
43+
[
44+
[
45+
'class' => QualityGateDecorator::class,
46+
'args' => [
47+
$growthServices->getNewcomerTasksConfigurationLoader(),
48+
$growthServices->getImageRecommendationSubmissionLogFactory(),
49+
$growthServices->getSectionImageRecommendationSubmissionLogFactory(),
50+
$growthServices->getLinkRecommendationSubmissionLogFactory(),
51+
$growthServices->getGrowthExperimentsCampaignConfig()
52+
]
53+
],
54+
]
55+
);
56+
57+
return $taskSuggesterFactory;
1858
}
1959
);
2060
};
61+
62+
// Activate suggested edits for new users, complete various tours.
63+
$wgHooks['UserGetDefaultOptions'][] = static function ( &$defaultOptions ) {
64+
$defaultOptions[SuggestedEdits::ACTIVATED_PREF] = true;
65+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{{For|instruments also called Kemençe|Kemenche}}
2+
{{DISPLAYTITLE:Classical ''kemençe''}}
3+
{{Template:Awkward}}
4+
{{Infobox instrument
5+
| name =
6+
| names =
7+
| image = Kemence Sezgin (1).jpg
8+
| image_capt =
9+
| background = string
10+
| classification = stringed
11+
| hornbostel_sachs = 321.321
12+
| hornbostel_sachs_desc = Necked bowl lute
13+
| inventors =
14+
| developed =
15+
| range =
16+
| related = [[Byzantine lyra]], [[Gadulka]], [[Calabrian Lira]], [[Cretan lyra]], [[Lijerica]]
17+
| musicians =
18+
| builders =
19+
| articles =
20+
}}
21+
22+
The '''classical''' '''''kemenche''''' ({{lang-tr|Klasik kemençe}}), '''{{lang|tr|Armudî kemençe}}''' ('pear-shaped kemenche') or '''{{lang|el-Latn|Politiki lyra}}''' ({{lang-el|πολίτικη λύρα}}, 'Constantinopolitan lyre') is a pear-shaped bowed instrument that derived from the medieval Greek byzantine lyre.
23+
24+
It was mainly used by [[Greeks|Greek]] immigrants from [[Asia Minor]] and in classical [[Ottoman Empire|Ottoman]] music. The instrument was also used earlier for popular music, such as early "Smyrna-Style" [[Rebetiko]] and played till nowadays. It has become the main bowed instrument of [[Ottoman classical music]] since the mid 19th century.
25+
26+
==Etymology==
27+
28+
The name ''Kemençe'' derives from the [[Persian language|Persian]] [[Kamancheh]], and means merely "small bow".&lt;ref&gt;{{cite journal|title=Middle East Focus |url=http://www.thestrad.com/downloads/Kamancheh.pdf |work=The Strad |date=July 2007 |pages=50–2 |quote=The Persian word for bow is kaman, and kamancheh is the diminutive form. }}{{dead link|date=August 2020|bot=medic}}{{cbignore|bot=medic}}&lt;/ref&gt;
29+
30+
The name ''lyra'' derives from the name of the ancient Greek [[lyre]] and was used in medieval times, see [[Byzantine lyra]].
31+
32+
== Playing ==
33+
34+
It is played in the downright position, by resting it between both knees or on one knee when sitting. It is always played "braccio", that is, with the tuning head uppermost. The kemenche bow is called the yay ({{lang-tr|Yay}}) and the ''doksar'' ({{lang-el|δοξάρι}}), the Greek term for bow.
35+
36+
The strings are stopped by touching them by the side with the nails, like for many folk fiddles from Southeastern Europe to the Indian sub-continent, including the Indian [[sarangi]].
37+
38+
== Construction ==
39+
40+
[[File:Smyrna Trio.jpg|thumb|right|alt="Photo of Smyrna Style Trio (1930)|Smyrna style trio: K.&amp;nbsp;Lambros, [[Roza Eskenazi|R.&amp;nbsp;Eskenazi]], A.&amp;nbsp;Tomboulis (Athens, 1930)]]
41+
42+
Its pear-shaped body, elliptical pegbox and neck are fashioned from a single piece of wood. Its sound-board has two D-shaped soundholes of some 4x3 cm, approximately 25&amp;nbsp;mm apart, the rounded side facing outwards. The bridge is placed between, one side resting on the face of the instrument and the other on the sound post. A small hole 3–4&amp;nbsp;mm in diameter is bored in the back, directly below the bridge, and a ‘back channel’ (‘sırt oluğu’) begins from a triangular raised area (‘mihrap’) which is an extension of the neck, widens in the middle, and ends in a point near the tailpiece (“kuyruk takozu”) to which the gut or metal strings are attached. There is no nut to equalize the vibrating lengths of the strings.
43+
44+
The pegs, which are 14–15&amp;nbsp;cm long, form a triangle on the head, the middle string being 37–40&amp;nbsp;mm longer than the strings to either side of it. The vibrating lengths of the short strings are 25.5–26&amp;nbsp;cm. All the strings are of gut but the yegâh string is silver-wound. Today players may use synthetic racquet strings, aluminium-wound gut, synthetic silk or chromed steel violin strings.
45+
46+
Formerly the head, neck and back channel might be inlaid with ivory, mother-of-pearl or tortoise shell. Some kemençes made for the palace or mansions by great makers such as Büyük İzmitli or Baron had their backs and even the edges of the sound holes completely covered with such inlays with engraved and inlaid motifs.
47+
48+
==Related instruments==
49+
The [[Byzantine lyra]] (Latin: ''lira'') was a pear-shaped bowed string instrument. The [[Persian people|Persian]] geographer [[Ibn Khordadbeh|Ibn Khurradadhbih]] (d. 911) was the first to describe the Byzantine lyra as a typical Byzantine instrument (Margaret J. Kartomi, 1990).
50+
51+
Variations of the instrument (sharing the same form and method of playing) exist through a vast area of the [[Mediterranean]] and the [[Balkans]]. Examples are the Bulgarian [[Gadulka]], the [[Calabrian Lira]] in Italy, the [[Cretan lyra|lyra]] of [[Crete]] and the [[Dodecanese]], the [[Lijerica]] of the Croatian Adriatic.
52+
53+
== Notable kemençe and lyra virtuosi ==
54+
* [[Fahire Fersan]] (1900–1997)
55+
* [[Tamburi Cemil Bey]] (1873–1916)
56+
* Derya Türkan
57+
* Sokratis Sinopoulos
58+
* Labros Leontaridis
59+
* İhsan Özgen
60+
* Cüneyd Orhon
61+
* Ruşen Ferit Kam
62+
63+
==See also==
64+
*[[Byzantine lyra]]
65+
*[[Cretan lyra]]
66+
*[[Gadulka]]
67+
*[[Gudok]]
68+
*[[Ghaychak]]
69+
*[[Gusle]]
70+
*[[Rebab]]
71+
*[[Kamancheh]]
72+
*[[Kobyz]]
73+
*[[Rebec]]
74+
*[[Igil]]
75+
*[[Byzaanchy]]
76+
*[[Huqin]]
77+
*[[Violin family]]
78+
79+
== References ==
80+
{{Reflist}}
81+
82+
== Bibliography ==
83+
84+
* Margaret J. Kartomi: On Concepts and Classifications of Musical Instruments. Chicago Studies in Ethnomusicology, University of Chicago Press, 1990.
85+
* The New Grove Dictionary of Musical Instruments: Londra, 1984.
86+
* M. Nazmi Özalp: Türk Sanat Mûsikîsi Sazlarından Kemençe, Ankara, tarihsiz (1985’ten önce).
87+
* Laurence Picken: Folk Musical Instruments of Turkey, Londra, 1975.
88+
* Rauf Yekta: Türk Musikisi (çev: Orhan Nasuhioğlu), İstanbul, 1986.
89+
90+
==External links==
91+
{{Commons category|Klasik kemençe/Polítiki lýra}}
92+
* [http://www.turkishmusicportal.org/en/instruments/turkish-classical-music-kemence Kemençe]
93+
94+
===Video===
95+
*[https://www.youtube.com/watch?v=HSEYQhJZNuI Classical kemençe video]
96+
97+
{{Greek musical instruments}}
98+
{{Turkish musical instruments}}
99+
{{Authority control}}
100+
101+
{{DEFAULTSORT:Classical kemence}}
102+
[[Category:Necked bowl lutes]]
103+
[[Category:Bowed instruments]]
104+
[[Category:Greek musical instruments]]
105+
[[Category:Turkish musical instruments]]
106+
[[Category:Instruments of Ottoman classical music]]
107+
[[Category:Instruments of Turkish makam music]]

0 commit comments

Comments
 (0)