3 use Wikimedia\TestingAccessWrapper
;
6 * Test class for ChangesListSpecialPage class
8 * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen
10 * @author Antoine Musso
11 * @author Stephane Bisson
12 * @author Matthew Flaschen
15 * @covers ChangesListSpecialPage
17 class ChangesListSpecialPageTest
extends AbstractChangesListSpecialPageTestCase
{
18 public function setUp() {
20 $this->setMwGlobals( [
21 'wgStructuredChangeFiltersShowPreference' => true,
25 protected function getPage() {
26 $mock = $this->getMockBuilder( ChangesListSpecialPage
::class )
29 'ChangesListSpecialPage',
33 ->setMethods( [ 'getPageTitle' ] )
34 ->getMockForAbstractClass();
36 $mock->method( 'getPageTitle' )->willReturn(
37 Title
::makeTitle( NS_SPECIAL
, 'ChangesListSpecialPage' )
40 $mock = TestingAccessWrapper
::newFromObject(
47 private function buildQuery(
48 $requestOptions = null,
51 $context = new RequestContext
;
52 $context->setRequest( new FauxRequest( $requestOptions ) );
54 $context->setUser( $user );
57 $this->changesListSpecialPage
->setContext( $context );
58 $this->changesListSpecialPage
->filterGroups
= [];
59 $formOptions = $this->changesListSpecialPage
->setup( null );
61 # Filter out rc_timestamp conditions which depends on the test runtime
62 # This condition is not needed as of march 2, 2011 -- hashar
63 # @todo FIXME: Find a way to generate the correct rc_timestamp
67 $queryConditions = [];
72 [ $this->changesListSpecialPage
, 'buildQuery' ],
83 $queryConditions = array_filter(
85 'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
88 return $queryConditions;
91 /** helper to test SpecialRecentchanges::buildQuery() */
92 private function assertConditions(
94 $requestOptions = null,
98 $queryConditions = $this->buildQuery( $requestOptions, $user );
101 self
::normalizeCondition( $expected ),
102 self
::normalizeCondition( $queryConditions ),
107 private static function normalizeCondition( $conds ) {
108 $dbr = wfGetDB( DB_REPLICA
);
109 $normalized = array_map(
110 function ( $k, $v ) use ( $dbr ) {
111 if ( is_array( $v ) ) {
114 // (Ab)use makeList() to format only this entry
115 return $dbr->makeList( [ $k => $v ], Database
::LIST_AND
);
117 array_keys( $conds ),
124 /** return false if condition begins with 'rc_timestamp ' */
125 private static function filterOutRcTimestampCondition( $var ) {
126 return ( is_array( $var ) ||
false === strpos( $var, 'rc_timestamp ' ) );
129 public function testRcNsFilter() {
130 $this->assertConditions(
132 "rc_namespace = '0'",
135 'namespace' => NS_MAIN
,
137 "rc conditions with one namespace"
141 public function testRcNsFilterInversion() {
142 $this->assertConditions(
144 "rc_namespace != '0'",
147 'namespace' => NS_MAIN
,
150 "rc conditions with namespace inverted"
154 public function testRcNsFilterMultiple() {
155 $this->assertConditions(
157 "rc_namespace IN ('1','2','3')",
160 'namespace' => '1;2;3',
162 "rc conditions with multiple namespaces"
166 public function testRcNsFilterMultipleAssociated() {
167 $this->assertConditions(
169 "rc_namespace IN ('0','1','4','5','6','7')",
172 'namespace' => '1;4;7',
175 "rc conditions with multiple namespaces and associated"
179 public function testRcNsFilterMultipleAssociatedInvert() {
180 $this->assertConditions(
182 "rc_namespace NOT IN ('2','3','8','9')",
185 'namespace' => '2;3;9',
189 "rc conditions with multiple namespaces, associated and inverted"
193 public function testRcNsFilterMultipleInvert() {
194 $this->assertConditions(
196 "rc_namespace NOT IN ('1','2','3')",
199 'namespace' => '1;2;3',
202 "rc conditions with multiple namespaces inverted"
206 public function testRcHidemyselfFilter() {
207 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
208 $this->overrideMwServices();
210 $user = $this->getTestUser()->getUser();
211 $user->getActorId( wfGetDB( DB_MASTER
) );
212 $this->assertConditions(
214 "NOT((rc_actor = '{$user->getActorId()}') OR "
215 . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))",
220 "rc conditions: hidemyself=1 (logged in)",
224 $user = User
::newFromName( '10.11.12.13', false );
225 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
226 $this->assertConditions(
228 "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))",
233 "rc conditions: hidemyself=1 (anon)",
238 public function testRcHidebyothersFilter() {
239 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
240 $this->overrideMwServices();
242 $user = $this->getTestUser()->getUser();
243 $user->getActorId( wfGetDB( DB_MASTER
) );
244 $this->assertConditions(
246 "(rc_actor = '{$user->getActorId()}') OR "
247 . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')",
252 "rc conditions: hidebyothers=1 (logged in)",
256 $user = User
::newFromName( '10.11.12.13', false );
257 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
258 $this->assertConditions(
260 "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')",
265 "rc conditions: hidebyothers=1 (anon)",
270 public function testRcHidepageedits() {
271 $this->assertConditions(
276 'hidepageedits' => 1,
278 "rc conditions: hidepageedits=1"
282 public function testRcHidenewpages() {
283 $this->assertConditions(
290 "rc conditions: hidenewpages=1"
294 public function testRcHidelog() {
295 $this->assertConditions(
302 "rc conditions: hidelog=1"
306 public function testRcHidehumans() {
307 $this->assertConditions(
315 "rc conditions: hidebots=0 hidehumans=1"
319 public function testRcHidepatrolledDisabledFilter() {
320 $this->setMwGlobals( 'wgUseRCPatrol', false );
321 $user = $this->getTestUser()->getUser();
322 $this->assertConditions(
326 'hidepatrolled' => 1,
328 "rc conditions: hidepatrolled=1 (user not allowed)",
333 public function testRcHideunpatrolledDisabledFilter() {
334 $this->setMwGlobals( 'wgUseRCPatrol', false );
335 $user = $this->getTestUser()->getUser();
336 $this->assertConditions(
340 'hideunpatrolled' => 1,
342 "rc conditions: hideunpatrolled=1 (user not allowed)",
346 public function testRcHidepatrolledFilter() {
347 $user = $this->getTestSysop()->getUser();
348 $this->assertConditions(
353 'hidepatrolled' => 1,
355 "rc conditions: hidepatrolled=1",
360 public function testRcHideunpatrolledFilter() {
361 $user = $this->getTestSysop()->getUser();
362 $this->assertConditions(
364 'rc_patrolled' => [ 1, 2 ],
367 'hideunpatrolled' => 1,
369 "rc conditions: hideunpatrolled=1",
374 public function testRcReviewStatusFilter() {
375 $user = $this->getTestSysop()->getUser();
376 $this->assertConditions(
381 'reviewStatus' => 'manual'
383 "rc conditions: reviewStatus=manual",
386 $this->assertConditions(
388 'rc_patrolled' => [ 0, 2 ],
391 'reviewStatus' => 'unpatrolled;auto'
393 "rc conditions: reviewStatus=unpatrolled;auto",
398 public function testRcHideminorFilter() {
399 $this->assertConditions(
406 "rc conditions: hideminor=1"
410 public function testRcHidemajorFilter() {
411 $this->assertConditions(
418 "rc conditions: hidemajor=1"
422 public function testHideCategorization() {
423 $this->assertConditions(
429 'hidecategorization' => 1
431 "rc conditions: hidecategorization=1"
435 public function testFilterUserExpLevelAll() {
436 $this->assertConditions(
441 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
443 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
447 public function testFilterUserExpLevelRegisteredUnregistered() {
448 $this->assertConditions(
453 'userExpLevel' => 'registered;unregistered',
455 "rc conditions: userExpLevel=registered;unregistered"
459 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
460 $this->assertConditions(
465 'userExpLevel' => 'registered;unregistered;learner',
467 "rc conditions: userExpLevel=registered;unregistered;learner"
471 public function testFilterUserExpLevelAllExperienceLevels() {
472 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
473 $this->overrideMwServices();
475 $this->assertConditions(
478 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
481 'userExpLevel' => 'newcomer;learner;experienced',
483 "rc conditions: userExpLevel=newcomer;learner;experienced"
487 public function testFilterUserExpLevelRegistrered() {
488 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
489 $this->overrideMwServices();
491 $this->assertConditions(
494 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
497 'userExpLevel' => 'registered',
499 "rc conditions: userExpLevel=registered"
503 public function testFilterUserExpLevelUnregistrered() {
504 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
505 $this->overrideMwServices();
507 $this->assertConditions(
510 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
513 'userExpLevel' => 'unregistered',
515 "rc conditions: userExpLevel=unregistered"
519 public function testFilterUserExpLevelRegistreredOrLearner() {
520 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
521 $this->overrideMwServices();
523 $this->assertConditions(
526 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
529 'userExpLevel' => 'registered;learner',
531 "rc conditions: userExpLevel=registered;learner"
535 public function testFilterUserExpLevelUnregistreredOrExperienced() {
536 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
537 $this->overrideMwServices();
539 $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
542 '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
543 . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
545 "rc conditions: userExpLevel=unregistered;experienced"
549 public function testFilterUserExpLevel() {
551 $this->setMwGlobals( [
552 'wgLearnerEdits' => 10,
553 'wgLearnerMemberSince' => 4,
554 'wgExperiencedUserEdits' => 500,
555 'wgExperiencedUserMemberSince' => 30,
558 $this->createUsers( [
559 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
560 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
561 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
562 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
563 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
564 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
565 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
566 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
570 $this->assertArrayEquals(
571 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
572 $this->fetchUsers( [ 'newcomer' ], $now )
575 // newcomers and learner
576 $this->assertArrayEquals(
578 'Newcomer1', 'Newcomer2', 'Newcomer3',
579 'Learner1', 'Learner2', 'Learner3', 'Learner4',
581 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
584 // newcomers and more learner
585 $this->assertArrayEquals(
587 'Newcomer1', 'Newcomer2', 'Newcomer3',
590 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
594 $this->assertArrayEquals(
595 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
596 $this->fetchUsers( [ 'learner' ], $now )
599 // more experienced only
600 $this->assertArrayEquals(
602 $this->fetchUsers( [ 'experienced' ], $now )
605 // learner and more experienced
606 $this->assertArrayEquals(
608 'Learner1', 'Learner2', 'Learner3', 'Learner4',
611 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
612 'Learner and more experienced'
616 private function createUsers( $specs, $now ) {
617 $dbw = wfGetDB( DB_MASTER
);
618 foreach ( $specs as $name => $spec ) {
622 'editcount' => $spec['edits'],
623 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
630 private function fetchUsers( $filters, $now ) {
639 call_user_func_array(
640 [ $this->changesListSpecialPage
, 'filterOnUserExperienceLevel' ],
642 get_class( $this->changesListSpecialPage
),
643 $this->changesListSpecialPage
->getContext(),
644 $this->changesListSpecialPage
->getDB(),
655 // @todo: This is not at all safe or sane. It just blindly assumes
656 // nothing in $conds depends on any other tables.
657 $result = wfGetDB( DB_MASTER
)->select(
660 array_filter( $conds ) +
[ 'user_email' => 'ut' ]
664 foreach ( $result as $row ) {
665 $usernames[] = $row->user_name
;
671 private function daysAgo( $days, $now ) {
672 $secondsPerDay = 86400;
673 return $now - $days * $secondsPerDay;
676 public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {
679 'msg' => 'showhidefoo',
684 'msg' => 'showhidebar',
691 'name' => 'unstructured',
692 'class' => ChangesListBooleanFilterGroup
::class,
697 'showHide' => 'showhidefoo',
702 'showHide' => 'showhidebar',
707 $this->changesListSpecialPage
->getFilterGroupDefinitionFromLegacyCustomFilters(
713 public function testGetStructuredFilterJsData() {
714 $this->changesListSpecialPage
->filterGroups
= [];
718 'name' => 'gub-group',
719 'title' => 'gub-group-title',
720 'class' => ChangesListBooleanFilterGroup
::class,
724 'label' => 'foo-label',
725 'description' => 'foo-description',
727 'showHide' => 'showhidefoo',
732 'label' => 'bar-label',
733 'description' => 'bar-description',
741 'name' => 'des-group',
742 'title' => 'des-group-title',
743 'class' => ChangesListStringOptionsFilterGroup
::class,
744 'isFullCoverage' => true,
748 'label' => 'grault-label',
749 'description' => 'grault-description',
753 'label' => 'garply-label',
754 'description' => 'garply-description',
757 'queryCallable' => function () {
759 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
763 'name' => 'unstructured',
764 'class' => ChangesListBooleanFilterGroup
::class,
767 'name' => 'hidethud',
768 'showHide' => 'showhidethud',
774 'showHide' => 'showhidemos',
782 $this->changesListSpecialPage
->registerFiltersFromDefinitions( $definition );
784 $this->assertArrayEquals(
786 // Filters that only display in the unstructured UI are
787 // are not included, and neither are groups that would
788 // be empty due to the above.
791 'name' => 'gub-group',
792 'title' => 'gub-group-title',
793 'type' => ChangesListBooleanFilterGroup
::TYPE
,
798 'label' => 'bar-label',
799 'description' => 'bar-description',
805 'defaultHighlightColor' => null
809 'label' => 'foo-label',
810 'description' => 'foo-description',
816 'defaultHighlightColor' => null
819 'fullCoverage' => true,
824 'name' => 'des-group',
825 'title' => 'des-group-title',
826 'type' => ChangesListStringOptionsFilterGroup
::TYPE
,
828 'fullCoverage' => true,
832 'label' => 'grault-label',
833 'description' => 'grault-description',
838 'defaultHighlightColor' => null
842 'label' => 'garply-label',
843 'description' => 'garply-description',
848 'defaultHighlightColor' => null
853 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
864 'grault-description',
866 'garply-description',
869 $this->changesListSpecialPage
->getStructuredFilterJsData(),
870 /** ordered= */ false,
875 public function provideParseParameters() {
877 [ 'hidebots', [ 'hidebots' => true ] ],
879 [ 'bots', [ 'hidebots' => false ] ],
881 [ 'hideminor', [ 'hideminor' => true ] ],
883 [ 'minor', [ 'hideminor' => false ] ],
885 [ 'hidemajor', [ 'hidemajor' => true ] ],
887 [ 'hideliu', [ 'hideliu' => true ] ],
889 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
891 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
893 [ 'hideanons', [ 'hideanons' => true ] ],
895 [ 'hidemyself', [ 'hidemyself' => true ] ],
897 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
899 [ 'hidehumans', [ 'hidehumans' => true ] ],
901 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
903 [ 'pagedits', [ 'hidepageedits' => false ] ],
905 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
907 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
909 [ 'hidelog', [ 'hidelog' => true ] ],
912 'userExpLevel=learner;experienced',
914 'userExpLevel' => 'learner;experienced'
918 // A few random combos
920 'bots,hideliu,hidemyself',
924 'hidemyself' => true,
929 'minor,hideanons,categorization',
931 'hideminor' => false,
933 'hidecategorization' => false,
938 'hidehumans,bots,hidecategorization',
940 'hidehumans' => true,
942 'hidecategorization' => true,
947 'hidemyself,userExpLevel=newcomer;learner,hideminor',
949 'hidemyself' => true,
951 'userExpLevel' => 'newcomer;learner',
957 public function provideGetFilterConflicts() {
961 "expectedConflicts" => false,
966 "userExpLevel" => "newcomer",
968 "expectedConflicts" => false,
973 "userExpLevel" => "learner",
975 "expectedConflicts" => false,
980 "hidenewpages" => true,
981 "hidepageedits" => true,
982 "hidecategorization" => false,
984 "hideWikidata" => true,
986 "expectedConflicts" => true,
991 "hidenewpages" => false,
992 "hidepageedits" => true,
993 "hidecategorization" => false,
995 "hideWikidata" => true,
997 "expectedConflicts" => true,
1001 "hidemajor" => true,
1002 "hidenewpages" => false,
1003 "hidepageedits" => false,
1004 "hidecategorization" => true,
1006 "hideWikidata" => true,
1008 "expectedConflicts" => false,
1012 "hideminor" => true,
1013 "hidenewpages" => true,
1014 "hidepageedits" => true,
1015 "hidecategorization" => false,
1017 "hideWikidata" => true,
1019 "expectedConflicts" => false,
1025 * @dataProvider provideGetFilterConflicts
1027 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
1028 $context = new RequestContext
;
1029 $context->setRequest( new FauxRequest( $parameters ) );
1030 $this->changesListSpecialPage
->setContext( $context );
1032 $this->assertEquals(
1034 $this->changesListSpecialPage
->areFiltersInConflict()
1038 public function validateOptionsProvider() {
1041 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
1043 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1046 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1048 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1051 [ 'hideanons' => 1 ],
1053 [ 'userExpLevel' => 'registered' ]
1058 [ 'userExpLevel' => 'unregistered' ]
1061 [ 'hideanons' => 1, 'hidebots' => 1 ],
1063 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ]
1066 [ 'hideliu' => 1, 'hidebots' => 0 ],
1068 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ]
1071 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1076 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1081 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1086 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1092 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],