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 $normalized = array_map(
109 function ( $k, $v ) {
110 return is_numeric( $k ) ?
$v : "$k = $v";
112 array_keys( $conds ),
119 /** return false if condition begin with 'rc_timestamp ' */
120 private static function filterOutRcTimestampCondition( $var ) {
121 return ( false === strpos( $var, 'rc_timestamp ' ) );
124 public function testRcNsFilter() {
125 $this->assertConditions(
127 "rc_namespace = '0'",
130 'namespace' => NS_MAIN
,
132 "rc conditions with one namespace"
136 public function testRcNsFilterInversion() {
137 $this->assertConditions(
139 "rc_namespace != '0'",
142 'namespace' => NS_MAIN
,
145 "rc conditions with namespace inverted"
149 public function testRcNsFilterMultiple() {
150 $this->assertConditions(
152 "rc_namespace IN ('1','2','3')",
155 'namespace' => '1;2;3',
157 "rc conditions with multiple namespaces"
161 public function testRcNsFilterMultipleAssociated() {
162 $this->assertConditions(
164 "rc_namespace IN ('0','1','4','5','6','7')",
167 'namespace' => '1;4;7',
170 "rc conditions with multiple namespaces and associated"
174 public function testRcNsFilterMultipleAssociatedInvert() {
175 $this->assertConditions(
177 "rc_namespace NOT IN ('2','3','8','9')",
180 'namespace' => '2;3;9',
184 "rc conditions with multiple namespaces, associated and inverted"
188 public function testRcNsFilterMultipleInvert() {
189 $this->assertConditions(
191 "rc_namespace NOT IN ('1','2','3')",
194 'namespace' => '1;2;3',
197 "rc conditions with multiple namespaces inverted"
201 public function testRcHidemyselfFilter() {
202 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
203 $this->overrideMwServices();
205 $user = $this->getTestUser()->getUser();
206 $user->getActorId( wfGetDB( DB_MASTER
) );
207 $this->assertConditions(
209 "NOT((rc_actor = '{$user->getActorId()}') OR "
210 . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))",
215 "rc conditions: hidemyself=1 (logged in)",
219 $user = User
::newFromName( '10.11.12.13', false );
220 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
221 $this->assertConditions(
223 "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))",
228 "rc conditions: hidemyself=1 (anon)",
233 public function testRcHidebyothersFilter() {
234 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
235 $this->overrideMwServices();
237 $user = $this->getTestUser()->getUser();
238 $user->getActorId( wfGetDB( DB_MASTER
) );
239 $this->assertConditions(
241 "(rc_actor = '{$user->getActorId()}') OR "
242 . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')",
247 "rc conditions: hidebyothers=1 (logged in)",
251 $user = User
::newFromName( '10.11.12.13', false );
252 $id = $user->getActorId( wfGetDB( DB_MASTER
) );
253 $this->assertConditions(
255 "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')",
260 "rc conditions: hidebyothers=1 (anon)",
265 public function testRcHidepageedits() {
266 $this->assertConditions(
271 'hidepageedits' => 1,
273 "rc conditions: hidepageedits=1"
277 public function testRcHidenewpages() {
278 $this->assertConditions(
285 "rc conditions: hidenewpages=1"
289 public function testRcHidelog() {
290 $this->assertConditions(
297 "rc conditions: hidelog=1"
301 public function testRcHidehumans() {
302 $this->assertConditions(
310 "rc conditions: hidebots=0 hidehumans=1"
314 public function testRcHidepatrolledDisabledFilter() {
315 $this->setMwGlobals( 'wgUseRCPatrol', false );
316 $user = $this->getTestUser()->getUser();
317 $this->assertConditions(
321 'hidepatrolled' => 1,
323 "rc conditions: hidepatrolled=1 (user not allowed)",
328 public function testRcHideunpatrolledDisabledFilter() {
329 $this->setMwGlobals( 'wgUseRCPatrol', false );
330 $user = $this->getTestUser()->getUser();
331 $this->assertConditions(
335 'hideunpatrolled' => 1,
337 "rc conditions: hideunpatrolled=1 (user not allowed)",
341 public function testRcHidepatrolledFilter() {
342 $user = $this->getTestSysop()->getUser();
343 $this->assertConditions(
348 'hidepatrolled' => 1,
350 "rc conditions: hidepatrolled=1",
355 public function testRcHideunpatrolledFilter() {
356 $user = $this->getTestSysop()->getUser();
357 $this->assertConditions(
362 'hideunpatrolled' => 1,
364 "rc conditions: hideunpatrolled=1",
369 public function testRcHideminorFilter() {
370 $this->assertConditions(
377 "rc conditions: hideminor=1"
381 public function testRcHidemajorFilter() {
382 $this->assertConditions(
389 "rc conditions: hidemajor=1"
393 public function testHideCategorization() {
394 $this->assertConditions(
400 'hidecategorization' => 1
402 "rc conditions: hidecategorization=1"
406 public function testFilterUserExpLevelAll() {
407 $this->assertConditions(
412 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
414 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
418 public function testFilterUserExpLevelRegisteredUnregistered() {
419 $this->assertConditions(
424 'userExpLevel' => 'registered;unregistered',
426 "rc conditions: userExpLevel=registered;unregistered"
430 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
431 $this->assertConditions(
436 'userExpLevel' => 'registered;unregistered;learner',
438 "rc conditions: userExpLevel=registered;unregistered;learner"
442 public function testFilterUserExpLevelAllExperienceLevels() {
443 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
444 $this->overrideMwServices();
446 $this->assertConditions(
449 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
452 'userExpLevel' => 'newcomer;learner;experienced',
454 "rc conditions: userExpLevel=newcomer;learner;experienced"
458 public function testFilterUserExpLevelRegistrered() {
459 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
460 $this->overrideMwServices();
462 $this->assertConditions(
465 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
468 'userExpLevel' => 'registered',
470 "rc conditions: userExpLevel=registered"
474 public function testFilterUserExpLevelUnregistrered() {
475 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
476 $this->overrideMwServices();
478 $this->assertConditions(
481 'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
484 'userExpLevel' => 'unregistered',
486 "rc conditions: userExpLevel=unregistered"
490 public function testFilterUserExpLevelRegistreredOrLearner() {
491 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
492 $this->overrideMwServices();
494 $this->assertConditions(
497 'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
500 'userExpLevel' => 'registered;learner',
502 "rc conditions: userExpLevel=registered;learner"
506 public function testFilterUserExpLevelUnregistreredOrExperienced() {
507 $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
508 $this->overrideMwServices();
510 $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
513 '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
514 . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
516 "rc conditions: userExpLevel=unregistered;experienced"
520 public function testFilterUserExpLevel() {
522 $this->setMwGlobals( [
523 'wgLearnerEdits' => 10,
524 'wgLearnerMemberSince' => 4,
525 'wgExperiencedUserEdits' => 500,
526 'wgExperiencedUserMemberSince' => 30,
529 $this->createUsers( [
530 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
531 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
532 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
533 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
534 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
535 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
536 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
537 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
541 $this->assertArrayEquals(
542 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
543 $this->fetchUsers( [ 'newcomer' ], $now )
546 // newcomers and learner
547 $this->assertArrayEquals(
549 'Newcomer1', 'Newcomer2', 'Newcomer3',
550 'Learner1', 'Learner2', 'Learner3', 'Learner4',
552 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
555 // newcomers and more learner
556 $this->assertArrayEquals(
558 'Newcomer1', 'Newcomer2', 'Newcomer3',
561 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
565 $this->assertArrayEquals(
566 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
567 $this->fetchUsers( [ 'learner' ], $now )
570 // more experienced only
571 $this->assertArrayEquals(
573 $this->fetchUsers( [ 'experienced' ], $now )
576 // learner and more experienced
577 $this->assertArrayEquals(
579 'Learner1', 'Learner2', 'Learner3', 'Learner4',
582 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
583 'Learner and more experienced'
587 private function createUsers( $specs, $now ) {
588 $dbw = wfGetDB( DB_MASTER
);
589 foreach ( $specs as $name => $spec ) {
593 'editcount' => $spec['edits'],
594 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
601 private function fetchUsers( $filters, $now ) {
610 call_user_func_array(
611 [ $this->changesListSpecialPage
, 'filterOnUserExperienceLevel' ],
613 get_class( $this->changesListSpecialPage
),
614 $this->changesListSpecialPage
->getContext(),
615 $this->changesListSpecialPage
->getDB(),
626 // @todo: This is not at all safe or sane. It just blindly assumes
627 // nothing in $conds depends on any other tables.
628 $result = wfGetDB( DB_MASTER
)->select(
631 array_filter( $conds ) +
[ 'user_email' => 'ut' ]
635 foreach ( $result as $row ) {
636 $usernames[] = $row->user_name
;
642 private function daysAgo( $days, $now ) {
643 $secondsPerDay = 86400;
644 return $now - $days * $secondsPerDay;
647 public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {
650 'msg' => 'showhidefoo',
655 'msg' => 'showhidebar',
662 'name' => 'unstructured',
663 'class' => ChangesListBooleanFilterGroup
::class,
668 'showHide' => 'showhidefoo',
673 'showHide' => 'showhidebar',
678 $this->changesListSpecialPage
->getFilterGroupDefinitionFromLegacyCustomFilters(
684 public function testGetStructuredFilterJsData() {
685 $this->changesListSpecialPage
->filterGroups
= [];
689 'name' => 'gub-group',
690 'title' => 'gub-group-title',
691 'class' => ChangesListBooleanFilterGroup
::class,
695 'label' => 'foo-label',
696 'description' => 'foo-description',
698 'showHide' => 'showhidefoo',
703 'label' => 'bar-label',
704 'description' => 'bar-description',
712 'name' => 'des-group',
713 'title' => 'des-group-title',
714 'class' => ChangesListStringOptionsFilterGroup
::class,
715 'isFullCoverage' => true,
719 'label' => 'grault-label',
720 'description' => 'grault-description',
724 'label' => 'garply-label',
725 'description' => 'garply-description',
728 'queryCallable' => function () {
730 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
734 'name' => 'unstructured',
735 'class' => ChangesListBooleanFilterGroup
::class,
738 'name' => 'hidethud',
739 'showHide' => 'showhidethud',
745 'showHide' => 'showhidemos',
753 $this->changesListSpecialPage
->registerFiltersFromDefinitions( $definition );
755 $this->assertArrayEquals(
757 // Filters that only display in the unstructured UI are
758 // are not included, and neither are groups that would
759 // be empty due to the above.
762 'name' => 'gub-group',
763 'title' => 'gub-group-title',
764 'type' => ChangesListBooleanFilterGroup
::TYPE
,
769 'label' => 'bar-label',
770 'description' => 'bar-description',
776 'defaultHighlightColor' => null
780 'label' => 'foo-label',
781 'description' => 'foo-description',
787 'defaultHighlightColor' => null
790 'fullCoverage' => true,
795 'name' => 'des-group',
796 'title' => 'des-group-title',
797 'type' => ChangesListStringOptionsFilterGroup
::TYPE
,
799 'fullCoverage' => true,
803 'label' => 'grault-label',
804 'description' => 'grault-description',
809 'defaultHighlightColor' => null
813 'label' => 'garply-label',
814 'description' => 'garply-description',
819 'defaultHighlightColor' => null
824 'default' => ChangesListStringOptionsFilterGroup
::NONE
,
835 'grault-description',
837 'garply-description',
840 $this->changesListSpecialPage
->getStructuredFilterJsData(),
841 /** ordered= */ false,
846 public function provideParseParameters() {
848 [ 'hidebots', [ 'hidebots' => true ] ],
850 [ 'bots', [ 'hidebots' => false ] ],
852 [ 'hideminor', [ 'hideminor' => true ] ],
854 [ 'minor', [ 'hideminor' => false ] ],
856 [ 'hidemajor', [ 'hidemajor' => true ] ],
858 [ 'hideliu', [ 'hideliu' => true ] ],
860 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
862 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
864 [ 'hideanons', [ 'hideanons' => true ] ],
866 [ 'hidemyself', [ 'hidemyself' => true ] ],
868 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
870 [ 'hidehumans', [ 'hidehumans' => true ] ],
872 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
874 [ 'pagedits', [ 'hidepageedits' => false ] ],
876 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
878 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
880 [ 'hidelog', [ 'hidelog' => true ] ],
883 'userExpLevel=learner;experienced',
885 'userExpLevel' => 'learner;experienced'
889 // A few random combos
891 'bots,hideliu,hidemyself',
895 'hidemyself' => true,
900 'minor,hideanons,categorization',
902 'hideminor' => false,
904 'hidecategorization' => false,
909 'hidehumans,bots,hidecategorization',
911 'hidehumans' => true,
913 'hidecategorization' => true,
918 'hidemyself,userExpLevel=newcomer;learner,hideminor',
920 'hidemyself' => true,
922 'userExpLevel' => 'newcomer;learner',
928 public function provideGetFilterConflicts() {
932 "expectedConflicts" => false,
937 "userExpLevel" => "newcomer",
939 "expectedConflicts" => false,
944 "userExpLevel" => "learner",
946 "expectedConflicts" => false,
951 "hidenewpages" => true,
952 "hidepageedits" => true,
953 "hidecategorization" => false,
955 "hideWikidata" => true,
957 "expectedConflicts" => true,
962 "hidenewpages" => false,
963 "hidepageedits" => true,
964 "hidecategorization" => false,
966 "hideWikidata" => true,
968 "expectedConflicts" => true,
973 "hidenewpages" => false,
974 "hidepageedits" => false,
975 "hidecategorization" => true,
977 "hideWikidata" => true,
979 "expectedConflicts" => false,
984 "hidenewpages" => true,
985 "hidepageedits" => true,
986 "hidecategorization" => false,
988 "hideWikidata" => true,
990 "expectedConflicts" => false,
996 * @dataProvider provideGetFilterConflicts
998 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
999 $context = new RequestContext
;
1000 $context->setRequest( new FauxRequest( $parameters ) );
1001 $this->changesListSpecialPage
->setContext( $context );
1003 $this->assertEquals(
1005 $this->changesListSpecialPage
->areFiltersInConflict()
1009 public function validateOptionsProvider() {
1012 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
1014 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1017 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1019 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1022 [ 'hideanons' => 1 ],
1024 [ 'userExpLevel' => 'registered' ]
1029 [ 'userExpLevel' => 'unregistered' ]
1032 [ 'hideanons' => 1, 'hidebots' => 1 ],
1034 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ]
1037 [ 'hideliu' => 1, 'hidebots' => 0 ],
1039 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ]
1042 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1047 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1052 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1057 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1063 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],