3 use MediaWiki\MediaWikiServices
;
4 use Wikimedia\TestingAccessWrapper
;
7 * Test class for ChangesListSpecialPage class
9 * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen
11 * @author Antoine Musso
12 * @author Stephane Bisson
13 * @author Matthew Flaschen
16 * @covers ChangesListSpecialPage
18 class ChangesListSpecialPageTest
extends AbstractChangesListSpecialPageTestCase
{
19 protected function getPage() {
20 $mock = $this->getMockBuilder( ChangesListSpecialPage
::class )
23 'ChangesListSpecialPage',
27 ->setMethods( [ 'getPageTitle' ] )
28 ->getMockForAbstractClass();
30 $mock->method( 'getPageTitle' )->willReturn(
31 Title
::makeTitle( NS_SPECIAL
, 'ChangesListSpecialPage' )
34 $mock = TestingAccessWrapper
::newFromObject(
41 private function buildQuery(
42 $requestOptions = null,
45 $context = new RequestContext
;
46 $context->setRequest( new FauxRequest( $requestOptions ) );
48 $context->setUser( $user );
51 $this->changesListSpecialPage
->setContext( $context );
52 $this->changesListSpecialPage
->filterGroups
= [];
53 $formOptions = $this->changesListSpecialPage
->setup( null );
55 # Filter out rc_timestamp conditions which depends on the test runtime
56 # This condition is not needed as of march 2, 2011 -- hashar
57 # @todo FIXME: Find a way to generate the correct rc_timestamp
61 $queryConditions = [];
66 [ $this->changesListSpecialPage
, 'buildQuery' ],
77 $queryConditions = array_filter(
79 'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
82 return $queryConditions;
85 /** helper to test SpecialRecentchanges::buildQuery() */
86 private function assertConditions(
88 $requestOptions = null,
92 $queryConditions = $this->buildQuery( $requestOptions, $user );
95 self
::normalizeCondition( $expected ),
96 self
::normalizeCondition( $queryConditions ),
101 private static function normalizeCondition( $conds ) {
102 $dbr = wfGetDB( DB_REPLICA
);
103 $normalized = array_map(
104 function ( $k, $v ) use ( $dbr ) {
105 if ( is_array( $v ) ) {
108 // (Ab)use makeList() to format only this entry
109 return $dbr->makeList( [ $k => $v ], Database
::LIST_AND
);
111 array_keys( $conds ),
118 /** return false if condition begins with 'rc_timestamp ' */
119 private static function filterOutRcTimestampCondition( $var ) {
120 return ( is_array( $var ) ||
strpos( $var, 'rc_timestamp ' ) === false );
123 public function testRcNsFilter() {
124 $this->assertConditions(
126 "rc_namespace = '0'",
129 'namespace' => NS_MAIN
,
131 "rc conditions with one namespace"
135 public function testRcNsFilterInversion() {
136 $this->assertConditions(
138 "rc_namespace != '0'",
141 'namespace' => NS_MAIN
,
144 "rc conditions with namespace inverted"
148 public function testRcNsFilterMultiple() {
149 $this->assertConditions(
151 "rc_namespace IN ('1','2','3')",
154 'namespace' => '1;2;3',
156 "rc conditions with multiple namespaces"
160 public function testRcNsFilterMultipleAssociated() {
161 $this->assertConditions(
163 "rc_namespace IN ('0','1','4','5','6','7')",
166 'namespace' => '1;4;7',
169 "rc conditions with multiple namespaces and associated"
173 public function testRcNsFilterAssociatedSpecial() {
174 $this->assertConditions(
176 "rc_namespace IN ('-1','0','1')",
179 'namespace' => '1;-1',
182 "rc conditions with associated and special namespace"
186 public function testRcNsFilterMultipleAssociatedInvert() {
187 $this->assertConditions(
189 "rc_namespace NOT IN ('2','3','8','9')",
192 'namespace' => '2;3;9',
196 "rc conditions with multiple namespaces, associated and inverted"
200 public function testRcNsFilterMultipleInvert() {
201 $this->assertConditions(
203 "rc_namespace NOT IN ('1','2','3')",
206 'namespace' => '1;2;3',
209 "rc conditions with multiple namespaces inverted"
213 public function testRcNsFilterAllContents() {
214 $namespaces = MediaWikiServices
::getInstance()->getNamespaceInfo()->getSubjectNamespaces();
215 $this->assertConditions(
217 'rc_namespace IN (' . $this->db
->makeList( $namespaces ) . ')',
220 'namespace' => 'all-contents',
222 "rc conditions with all-contents"
226 public function testRcHidemyselfFilter() {
227 $user = $this->getTestUser()->getUser();
228 $user->getActorId( wfGetDB( DB_MASTER
) );
229 $this->assertConditions(
231 "NOT((rc_actor = '{$user->getActorId()}'))",
236 "rc conditions: hidemyself=1 (logged in)",
240 $user = User
::newFromName( '10.11.12.13', false );
241 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
242 $this->assertConditions(
244 "NOT((rc_actor = '{$user->getActorId()}'))",
249 "rc conditions: hidemyself=1 (anon)",
254 public function testRcHidebyothersFilter() {
255 $user = $this->getTestUser()->getUser();
256 $user->getActorId( wfGetDB( DB_MASTER
) );
257 $this->assertConditions(
259 "(rc_actor = '{$user->getActorId()}')",
264 "rc conditions: hidebyothers=1 (logged in)",
268 $user = User
::newFromName( '10.11.12.13', false );
269 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
270 $this->assertConditions(
272 "(rc_actor = '{$user->getActorId()}')",
277 "rc conditions: hidebyothers=1 (anon)",
282 public function testRcHidepageedits() {
283 $this->assertConditions(
288 'hidepageedits' => 1,
290 "rc conditions: hidepageedits=1"
294 public function testRcHidenewpages() {
295 $this->assertConditions(
302 "rc conditions: hidenewpages=1"
306 public function testRcHidelog() {
307 $this->assertConditions(
314 "rc conditions: hidelog=1"
318 public function testRcHidehumans() {
319 $this->assertConditions(
327 "rc conditions: hidebots=0 hidehumans=1"
331 public function testRcHidepatrolledDisabledFilter() {
332 $this->setMwGlobals( 'wgUseRCPatrol', false );
333 $user = $this->getTestUser()->getUser();
334 $this->assertConditions(
338 'hidepatrolled' => 1,
340 "rc conditions: hidepatrolled=1 (user not allowed)",
345 public function testRcHideunpatrolledDisabledFilter() {
346 $this->setMwGlobals( 'wgUseRCPatrol', false );
347 $user = $this->getTestUser()->getUser();
348 $this->assertConditions(
352 'hideunpatrolled' => 1,
354 "rc conditions: hideunpatrolled=1 (user not allowed)",
359 public function testRcHidepatrolledFilter() {
360 $user = $this->getTestSysop()->getUser();
361 $this->assertConditions(
366 'hidepatrolled' => 1,
368 "rc conditions: hidepatrolled=1",
373 public function testRcHideunpatrolledFilter() {
374 $user = $this->getTestSysop()->getUser();
375 $this->assertConditions(
377 'rc_patrolled' => [ 1, 2 ],
380 'hideunpatrolled' => 1,
382 "rc conditions: hideunpatrolled=1",
387 public function testRcReviewStatusFilter() {
388 $user = $this->getTestSysop()->getUser();
389 $this->assertConditions(
394 'reviewStatus' => 'manual'
396 "rc conditions: reviewStatus=manual",
399 $this->assertConditions(
401 'rc_patrolled' => [ 0, 2 ],
404 'reviewStatus' => 'unpatrolled;auto'
406 "rc conditions: reviewStatus=unpatrolled;auto",
411 public function testRcHideminorFilter() {
412 $this->assertConditions(
419 "rc conditions: hideminor=1"
423 public function testRcHidemajorFilter() {
424 $this->assertConditions(
431 "rc conditions: hidemajor=1"
435 public function testHideCategorization() {
436 $this->assertConditions(
442 'hidecategorization' => 1
444 "rc conditions: hidecategorization=1"
448 public function testFilterUserExpLevelAll() {
449 $this->assertConditions(
454 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
456 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
460 public function testFilterUserExpLevelRegisteredUnregistered() {
461 $this->assertConditions(
466 'userExpLevel' => 'registered;unregistered',
468 "rc conditions: userExpLevel=registered;unregistered"
472 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
473 $this->assertConditions(
478 'userExpLevel' => 'registered;unregistered;learner',
480 "rc conditions: userExpLevel=registered;unregistered;learner"
484 public function testFilterUserExpLevelAllExperienceLevels() {
485 $this->assertConditions(
488 'actor_rc_user.actor_user IS NOT NULL',
491 'userExpLevel' => 'newcomer;learner;experienced',
493 "rc conditions: userExpLevel=newcomer;learner;experienced"
497 public function testFilterUserExpLevelRegistrered() {
498 $this->assertConditions(
501 'actor_rc_user.actor_user IS NOT NULL',
504 'userExpLevel' => 'registered',
506 "rc conditions: userExpLevel=registered"
510 public function testFilterUserExpLevelUnregistrered() {
511 $this->assertConditions(
514 'actor_rc_user.actor_user IS NULL',
517 'userExpLevel' => 'unregistered',
519 "rc conditions: userExpLevel=unregistered"
523 public function testFilterUserExpLevelRegistreredOrLearner() {
524 $this->assertConditions(
527 'actor_rc_user.actor_user IS NOT NULL',
530 'userExpLevel' => 'registered;learner',
532 "rc conditions: userExpLevel=registered;learner"
536 public function testFilterUserExpLevelUnregistreredOrExperienced() {
537 $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
540 '/\(actor_rc_user\.actor_user IS NULL\) OR '
541 . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
543 "rc conditions: userExpLevel=unregistered;experienced"
547 public function testFilterUserExpLevel() {
549 $this->setMwGlobals( [
550 'wgLearnerEdits' => 10,
551 'wgLearnerMemberSince' => 4,
552 'wgExperiencedUserEdits' => 500,
553 'wgExperiencedUserMemberSince' => 30,
556 $this->createUsers( [
557 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
558 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
559 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
560 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
561 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
562 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
563 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
564 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
568 $this->assertArrayEquals(
569 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
570 $this->fetchUsers( [ 'newcomer' ], $now )
573 // newcomers and learner
574 $this->assertArrayEquals(
576 'Newcomer1', 'Newcomer2', 'Newcomer3',
577 'Learner1', 'Learner2', 'Learner3', 'Learner4',
579 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
582 // newcomers and more learner
583 $this->assertArrayEquals(
585 'Newcomer1', 'Newcomer2', 'Newcomer3',
588 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
592 $this->assertArrayEquals(
593 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
594 $this->fetchUsers( [ 'learner' ], $now )
597 // more experienced only
598 $this->assertArrayEquals(
600 $this->fetchUsers( [ 'experienced' ], $now )
603 // learner and more experienced
604 $this->assertArrayEquals(
606 'Learner1', 'Learner2', 'Learner3', 'Learner4',
609 $this->fetchUsers( [ 'learner', 'experienced' ], $now )
613 private function createUsers( $specs, $now ) {
614 $dbw = wfGetDB( DB_MASTER
);
615 foreach ( $specs as $name => $spec ) {
619 'editcount' => $spec['edits'],
620 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
627 private function fetchUsers( $filters, $now ) {
636 call_user_func_array(
637 [ $this->changesListSpecialPage
, 'filterOnUserExperienceLevel' ],
639 get_class( $this->changesListSpecialPage
),
640 $this->changesListSpecialPage
->getContext(),
641 $this->changesListSpecialPage
->getDB(),
652 // @todo: This is not at all safe or sane. It just blindly assumes
653 // nothing in $conds depends on any other tables.
654 $result = wfGetDB( DB_MASTER
)->select(
657 array_filter( $conds ) +
[ 'user_email' => 'ut' ]
661 foreach ( $result as $row ) {
662 $usernames[] = $row->user_name
;
668 private function daysAgo( $days, $now ) {
669 $secondsPerDay = 86400;
670 return $now - $days * $secondsPerDay;
673 public function testGetStructuredFilterJsData() {
674 $this->changesListSpecialPage
->filterGroups
= [];
678 'name' => 'gub-group',
679 'title' => 'gub-group-title',
680 'class' => ChangesListBooleanFilterGroup
::class,
684 'label' => 'foo-label',
685 'description' => 'foo-description',
687 'showHide' => 'showhidefoo',
692 'label' => 'bar-label',
693 'description' => 'bar-description',
701 'name' => 'des-group',
702 'title' => 'des-group-title',
703 'class' => ChangesListStringOptionsFilterGroup
::class,
704 'isFullCoverage' => true,
708 'label' => 'grault-label',
709 'description' => 'grault-description',
713 'label' => 'garply-label',
714 'description' => 'garply-description',
717 'queryCallable' => function () {
719 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
723 'name' => 'unstructured',
724 'class' => ChangesListBooleanFilterGroup
::class,
727 'name' => 'hidethud',
728 'showHide' => 'showhidethud',
734 'showHide' => 'showhidemos',
742 $this->changesListSpecialPage
->registerFiltersFromDefinitions( $definition );
744 $this->assertArrayEquals(
746 // Filters that only display in the unstructured UI are
747 // are not included, and neither are groups that would
748 // be empty due to the above.
751 'name' => 'gub-group',
752 'title' => 'gub-group-title',
753 'type' => ChangesListBooleanFilterGroup
::TYPE
,
758 'label' => 'bar-label',
759 'description' => 'bar-description',
765 'defaultHighlightColor' => null
769 'label' => 'foo-label',
770 'description' => 'foo-description',
776 'defaultHighlightColor' => null
779 'fullCoverage' => true,
784 'name' => 'des-group',
785 'title' => 'des-group-title',
786 'type' => ChangesListStringOptionsFilterGroup
::TYPE
,
788 'fullCoverage' => true,
792 'label' => 'grault-label',
793 'description' => 'grault-description',
798 'defaultHighlightColor' => null
802 'label' => 'garply-label',
803 'description' => 'garply-description',
808 'defaultHighlightColor' => null
813 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
824 'grault-description',
826 'garply-description',
829 $this->changesListSpecialPage
->getStructuredFilterJsData(),
830 /** ordered= */ false,
835 public function provideParseParameters() {
837 [ 'hidebots', [ 'hidebots' => true ] ],
839 [ 'bots', [ 'hidebots' => false ] ],
841 [ 'hideminor', [ 'hideminor' => true ] ],
843 [ 'minor', [ 'hideminor' => false ] ],
845 [ 'hidemajor', [ 'hidemajor' => true ] ],
847 [ 'hideliu', [ 'hideliu' => true ] ],
849 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
851 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
853 [ 'hideanons', [ 'hideanons' => true ] ],
855 [ 'hidemyself', [ 'hidemyself' => true ] ],
857 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
859 [ 'hidehumans', [ 'hidehumans' => true ] ],
861 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
863 [ 'pagedits', [ 'hidepageedits' => false ] ],
865 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
867 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
869 [ 'hidelog', [ 'hidelog' => true ] ],
872 'userExpLevel=learner;experienced',
874 'userExpLevel' => 'learner;experienced'
878 // A few random combos
880 'bots,hideliu,hidemyself',
884 'hidemyself' => true,
889 'minor,hideanons,categorization',
891 'hideminor' => false,
893 'hidecategorization' => false,
898 'hidehumans,bots,hidecategorization',
900 'hidehumans' => true,
902 'hidecategorization' => true,
907 'hidemyself,userExpLevel=newcomer;learner,hideminor',
909 'hidemyself' => true,
911 'userExpLevel' => 'newcomer;learner',
917 public function provideGetFilterConflicts() {
921 "expectedConflicts" => false,
926 "userExpLevel" => "newcomer",
928 "expectedConflicts" => false,
933 "userExpLevel" => "learner",
935 "expectedConflicts" => false,
940 "hidenewpages" => true,
941 "hidepageedits" => true,
942 "hidecategorization" => false,
944 "hideWikidata" => true,
946 "expectedConflicts" => true,
951 "hidenewpages" => false,
952 "hidepageedits" => true,
953 "hidecategorization" => false,
955 "hideWikidata" => true,
957 "expectedConflicts" => true,
962 "hidenewpages" => false,
963 "hidepageedits" => false,
964 "hidecategorization" => true,
966 "hideWikidata" => true,
968 "expectedConflicts" => false,
973 "hidenewpages" => true,
974 "hidepageedits" => true,
975 "hidecategorization" => false,
977 "hideWikidata" => true,
979 "expectedConflicts" => false,
985 * @dataProvider provideGetFilterConflicts
987 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
988 $context = new RequestContext
;
989 $context->setRequest( new FauxRequest( $parameters ) );
990 $this->changesListSpecialPage
->setContext( $context );
994 $this->changesListSpecialPage
->areFiltersInConflict()
998 public function validateOptionsProvider() {
1001 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
1003 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1007 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1009 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1013 [ 'hideanons' => 1 ],
1015 [ 'userExpLevel' => 'registered' ],
1021 [ 'userExpLevel' => 'unregistered' ],
1025 [ 'hideanons' => 1, 'hidebots' => 1 ],
1027 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ],
1031 [ 'hideliu' => 1, 'hidebots' => 0 ],
1033 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ],
1037 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1043 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1049 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1055 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1062 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],