Merge "Revised styling of sister-search sidebar."
[lhc/web/wiklou.git] / tests / phpunit / includes / specialpage / ChangesListSpecialPageTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
6 * Test class for ChangesListSpecialPage class
7 *
8 * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen
9 *
10 * @author Antoine Musso
11 * @author Stephane Bisson
12 * @author Matthew Flaschen
13 * @group Database
14 *
15 * @covers ChangesListSpecialPage
16 */
17 class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase {
18 protected function getPage() {
19 $mock = $this->getMockBuilder( ChangesListSpecialPage::class )
20 ->setConstructorArgs(
21 [
22 'ChangesListSpecialPage',
23 ''
24 ]
25 )
26 ->setMethods( [ 'getPageTitle' ] )
27 ->getMockForAbstractClass();
28
29 $mock->method( 'getPageTitle' )->willReturn(
30 Title::makeTitle( NS_SPECIAL, 'ChangesListSpecialPage' )
31 );
32
33 $mock = TestingAccessWrapper::newFromObject(
34 $mock
35 );
36
37 return $mock;
38 }
39
40 /** helper to test SpecialRecentchanges::buildMainQueryConds() */
41 private function assertConditions(
42 $expected,
43 $requestOptions = null,
44 $message = '',
45 $user = null
46 ) {
47 $context = new RequestContext;
48 $context->setRequest( new FauxRequest( $requestOptions ) );
49 if ( $user ) {
50 $context->setUser( $user );
51 }
52
53 $this->changesListSpecialPage->setContext( $context );
54 $this->changesListSpecialPage->filterGroups = [];
55 $formOptions = $this->changesListSpecialPage->setup( null );
56
57 #  Filter out rc_timestamp conditions which depends on the test runtime
58 # This condition is not needed as of march 2, 2011 -- hashar
59 # @todo FIXME: Find a way to generate the correct rc_timestamp
60
61 $tables = [];
62 $fields = [];
63 $queryConditions = [];
64 $query_options = [];
65 $join_conds = [];
66
67 call_user_func_array(
68 [ $this->changesListSpecialPage, 'buildQuery' ],
69 [
70 &$tables,
71 &$fields,
72 &$queryConditions,
73 &$query_options,
74 &$join_conds,
75 $formOptions
76 ]
77 );
78
79 $queryConditions = array_filter(
80 $queryConditions,
81 'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
82 );
83
84 $this->assertEquals(
85 self::normalizeCondition( $expected ),
86 self::normalizeCondition( $queryConditions ),
87 $message
88 );
89 }
90
91 private static function normalizeCondition( $conds ) {
92 $normalized = array_map(
93 function ( $k, $v ) {
94 return is_numeric( $k ) ? $v : "$k = $v";
95 },
96 array_keys( $conds ),
97 $conds
98 );
99 sort( $normalized );
100 return $normalized;
101 }
102
103 /** return false if condition begin with 'rc_timestamp ' */
104 private static function filterOutRcTimestampCondition( $var ) {
105 return ( false === strpos( $var, 'rc_timestamp ' ) );
106 }
107
108 public function testRcNsFilter() {
109 $this->assertConditions(
110 [ # expected
111 "rc_namespace = '0'",
112 ],
113 [
114 'namespace' => NS_MAIN,
115 ],
116 "rc conditions with one namespace"
117 );
118 }
119
120 public function testRcNsFilterInversion() {
121 $this->assertConditions(
122 [ # expected
123 "rc_namespace != '0'",
124 ],
125 [
126 'namespace' => NS_MAIN,
127 'invert' => 1,
128 ],
129 "rc conditions with namespace inverted"
130 );
131 }
132
133 public function testRcNsFilterMultiple() {
134 $this->assertConditions(
135 [ # expected
136 "rc_namespace IN ('1','2','3')",
137 ],
138 [
139 'namespace' => '1;2;3',
140 ],
141 "rc conditions with multiple namespaces"
142 );
143 }
144
145 public function testRcNsFilterMultipleAssociated() {
146 $this->assertConditions(
147 [ # expected
148 "rc_namespace IN ('0','1','4','5','6','7')",
149 ],
150 [
151 'namespace' => '1;4;7',
152 'associated' => 1,
153 ],
154 "rc conditions with multiple namespaces and associated"
155 );
156 }
157
158 public function testRcNsFilterMultipleAssociatedInvert() {
159 $this->assertConditions(
160 [ # expected
161 "rc_namespace NOT IN ('2','3','8','9')",
162 ],
163 [
164 'namespace' => '2;3;9',
165 'associated' => 1,
166 'invert' => 1
167 ],
168 "rc conditions with multiple namespaces, associated and inverted"
169 );
170 }
171
172 public function testRcNsFilterMultipleInvert() {
173 $this->assertConditions(
174 [ # expected
175 "rc_namespace NOT IN ('1','2','3')",
176 ],
177 [
178 'namespace' => '1;2;3',
179 'invert' => 1,
180 ],
181 "rc conditions with multiple namespaces inverted"
182 );
183 }
184
185 public function testRcHidemyselfFilter() {
186 $user = $this->getTestUser()->getUser();
187 $this->assertConditions(
188 [ # expected
189 "rc_user_text != '{$user->getName()}'",
190 ],
191 [
192 'hidemyself' => 1,
193 ],
194 "rc conditions: hidemyself=1 (logged in)",
195 $user
196 );
197
198 $user = User::newFromName( '10.11.12.13', false );
199 $this->assertConditions(
200 [ # expected
201 "rc_user_text != '10.11.12.13'",
202 ],
203 [
204 'hidemyself' => 1,
205 ],
206 "rc conditions: hidemyself=1 (anon)",
207 $user
208 );
209 }
210
211 public function testRcHidebyothersFilter() {
212 $user = $this->getTestUser()->getUser();
213 $this->assertConditions(
214 [ # expected
215 "rc_user_text = '{$user->getName()}'",
216 ],
217 [
218 'hidebyothers' => 1,
219 ],
220 "rc conditions: hidebyothers=1 (logged in)",
221 $user
222 );
223
224 $user = User::newFromName( '10.11.12.13', false );
225 $this->assertConditions(
226 [ # expected
227 "rc_user_text = '10.11.12.13'",
228 ],
229 [
230 'hidebyothers' => 1,
231 ],
232 "rc conditions: hidebyothers=1 (anon)",
233 $user
234 );
235 }
236
237 public function testRcHidepageedits() {
238 $this->assertConditions(
239 [ # expected
240 "rc_type != '0'",
241 ],
242 [
243 'hidepageedits' => 1,
244 ],
245 "rc conditions: hidepageedits=1"
246 );
247 }
248
249 public function testRcHidenewpages() {
250 $this->assertConditions(
251 [ # expected
252 "rc_type != '1'",
253 ],
254 [
255 'hidenewpages' => 1,
256 ],
257 "rc conditions: hidenewpages=1"
258 );
259 }
260
261 public function testRcHidelog() {
262 $this->assertConditions(
263 [ # expected
264 "rc_type != '3'",
265 ],
266 [
267 'hidelog' => 1,
268 ],
269 "rc conditions: hidelog=1"
270 );
271 }
272
273 public function testRcHidehumans() {
274 $this->assertConditions(
275 [ # expected
276 'rc_bot' => 1,
277 ],
278 [
279 'hidebots' => 0,
280 'hidehumans' => 1,
281 ],
282 "rc conditions: hidebots=0 hidehumans=1"
283 );
284 }
285
286 public function testRcHidepatrolledDisabledFilter() {
287 $user = $this->getTestUser()->getUser();
288 $this->assertConditions(
289 [ # expected
290 ],
291 [
292 'hidepatrolled' => 1,
293 ],
294 "rc conditions: hidepatrolled=1 (user not allowed)",
295 $user
296 );
297 }
298
299 public function testRcHideunpatrolledDisabledFilter() {
300 $user = $this->getTestUser()->getUser();
301 $this->assertConditions(
302 [ # expected
303 ],
304 [
305 'hideunpatrolled' => 1,
306 ],
307 "rc conditions: hideunpatrolled=1 (user not allowed)",
308 $user
309 );
310 }
311 public function testRcHidepatrolledFilter() {
312 $user = $this->getTestSysop()->getUser();
313 $this->assertConditions(
314 [ # expected
315 "rc_patrolled = 0",
316 ],
317 [
318 'hidepatrolled' => 1,
319 ],
320 "rc conditions: hidepatrolled=1",
321 $user
322 );
323 }
324
325 public function testRcHideunpatrolledFilter() {
326 $user = $this->getTestSysop()->getUser();
327 $this->assertConditions(
328 [ # expected
329 "rc_patrolled = 1",
330 ],
331 [
332 'hideunpatrolled' => 1,
333 ],
334 "rc conditions: hideunpatrolled=1",
335 $user
336 );
337 }
338
339 public function testRcHideminorFilter() {
340 $this->assertConditions(
341 [ # expected
342 "rc_minor = 0",
343 ],
344 [
345 'hideminor' => 1,
346 ],
347 "rc conditions: hideminor=1"
348 );
349 }
350
351 public function testRcHidemajorFilter() {
352 $this->assertConditions(
353 [ # expected
354 "rc_minor = 1",
355 ],
356 [
357 'hidemajor' => 1,
358 ],
359 "rc conditions: hidemajor=1"
360 );
361 }
362
363 public function testHideCategorization() {
364 $this->assertConditions(
365 [
366 # expected
367 "rc_type != '6'"
368 ],
369 [
370 'hidecategorization' => 1
371 ],
372 "rc conditions: hidecategorization=1"
373 );
374 }
375
376 public function testFilterUserExpLevel() {
377 $now = time();
378 $this->setMwGlobals( [
379 'wgLearnerEdits' => 10,
380 'wgLearnerMemberSince' => 4,
381 'wgExperiencedUserEdits' => 500,
382 'wgExperiencedUserMemberSince' => 30,
383 ] );
384
385 $this->createUsers( [
386 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
387 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
388 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
389 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
390 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
391 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
392 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
393 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
394 ], $now );
395
396 // newcomers only
397 $this->assertArrayEquals(
398 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
399 $this->fetchUsers( [ 'newcomer' ], $now )
400 );
401
402 // newcomers and learner
403 $this->assertArrayEquals(
404 [
405 'Newcomer1', 'Newcomer2', 'Newcomer3',
406 'Learner1', 'Learner2', 'Learner3', 'Learner4',
407 ],
408 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
409 );
410
411 // newcomers and more learner
412 $this->assertArrayEquals(
413 [
414 'Newcomer1', 'Newcomer2', 'Newcomer3',
415 'Experienced1',
416 ],
417 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
418 );
419
420 // learner only
421 $this->assertArrayEquals(
422 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
423 $this->fetchUsers( [ 'learner' ], $now )
424 );
425
426 // more experienced only
427 $this->assertArrayEquals(
428 [ 'Experienced1' ],
429 $this->fetchUsers( [ 'experienced' ], $now )
430 );
431
432 // learner and more experienced
433 $this->assertArrayEquals(
434 [
435 'Learner1', 'Learner2', 'Learner3', 'Learner4',
436 'Experienced1',
437 ],
438 $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
439 'Learner and more experienced'
440 );
441
442 // newcomers, learner, and more experienced
443 // TOOD: Fix test. This needs to test that anons are excluded,
444 // and right now the join fails.
445 /* $this->assertArrayEquals( */
446 /* [ */
447 /* 'Newcomer1', 'Newcomer2', 'Newcomer3', */
448 /* 'Learner1', 'Learner2', 'Learner3', 'Learner4', */
449 /* 'Experienced1', */
450 /* ], */
451 /* $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ], $now ) */
452 /* ); */
453 }
454
455 private function createUsers( $specs, $now ) {
456 $dbw = wfGetDB( DB_MASTER );
457 foreach ( $specs as $name => $spec ) {
458 User::createNew(
459 $name,
460 [
461 'editcount' => $spec['edits'],
462 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
463 'email' => 'ut',
464 ]
465 );
466 }
467 }
468
469 private function fetchUsers( $filters, $now ) {
470 $tables = [];
471 $conds = [];
472 $fields = [];
473 $query_options = [];
474 $join_conds = [];
475
476 sort( $filters );
477
478 call_user_func_array(
479 [ $this->changesListSpecialPage, 'filterOnUserExperienceLevel' ],
480 [
481 get_class( $this->changesListSpecialPage ),
482 $this->changesListSpecialPage->getContext(),
483 $this->changesListSpecialPage->getDB(),
484 &$tables,
485 &$fields,
486 &$conds,
487 &$query_options,
488 &$join_conds,
489 $filters,
490 $now
491 ]
492 );
493
494 $result = wfGetDB( DB_MASTER )->select(
495 $tables,
496 'user_name',
497 array_filter( $conds ) + [ 'user_email' => 'ut' ]
498 );
499
500 $usernames = [];
501 foreach ( $result as $row ) {
502 $usernames[] = $row->user_name;
503 }
504
505 return $usernames;
506 }
507
508 private function daysAgo( $days, $now ) {
509 $secondsPerDay = 86400;
510 return $now - $days * $secondsPerDay;
511 }
512
513 public function testGetFilterGroupDefinitionFromLegacyCustomFilters() {
514 $customFilters = [
515 'hidefoo' => [
516 'msg' => 'showhidefoo',
517 'default' => true,
518 ],
519
520 'hidebar' => [
521 'msg' => 'showhidebar',
522 'default' => false,
523 ],
524 ];
525
526 $this->assertEquals(
527 [
528 'name' => 'unstructured',
529 'class' => ChangesListBooleanFilterGroup::class,
530 'priority' => -1,
531 'filters' => [
532 [
533 'name' => 'hidefoo',
534 'showHide' => 'showhidefoo',
535 'default' => true,
536 ],
537 [
538 'name' => 'hidebar',
539 'showHide' => 'showhidebar',
540 'default' => false,
541 ]
542 ],
543 ],
544 $this->changesListSpecialPage->getFilterGroupDefinitionFromLegacyCustomFilters(
545 $customFilters
546 )
547 );
548 }
549
550 public function testGetStructuredFilterJsData() {
551 $this->changesListSpecialPage->filterGroups = [];
552
553 $definition = [
554 [
555 'name' => 'gub-group',
556 'title' => 'gub-group-title',
557 'class' => ChangesListBooleanFilterGroup::class,
558 'filters' => [
559 [
560 'name' => 'hidefoo',
561 'label' => 'foo-label',
562 'description' => 'foo-description',
563 'default' => true,
564 'showHide' => 'showhidefoo',
565 'priority' => 2,
566 ],
567 [
568 'name' => 'hidebar',
569 'label' => 'bar-label',
570 'description' => 'bar-description',
571 'default' => false,
572 'priority' => 4,
573 ]
574 ],
575 ],
576
577 [
578 'name' => 'des-group',
579 'title' => 'des-group-title',
580 'class' => ChangesListStringOptionsFilterGroup::class,
581 'isFullCoverage' => true,
582 'filters' => [
583 [
584 'name' => 'grault',
585 'label' => 'grault-label',
586 'description' => 'grault-description',
587 ],
588 [
589 'name' => 'garply',
590 'label' => 'garply-label',
591 'description' => 'garply-description',
592 ],
593 ],
594 'queryCallable' => function () {
595 },
596 'default' => ChangesListStringOptionsFilterGroup::NONE,
597 ],
598
599 [
600 'name' => 'unstructured',
601 'class' => ChangesListBooleanFilterGroup::class,
602 'filters' => [
603 [
604 'name' => 'hidethud',
605 'showHide' => 'showhidethud',
606 'default' => true,
607 ],
608
609 [
610 'name' => 'hidemos',
611 'showHide' => 'showhidemos',
612 'default' => false,
613 ],
614 ],
615 ],
616
617 ];
618
619 $this->changesListSpecialPage->registerFiltersFromDefinitions( $definition );
620
621 $this->assertArrayEquals(
622 [
623 // Filters that only display in the unstructured UI are
624 // are not included, and neither are groups that would
625 // be empty due to the above.
626 'groups' => [
627 [
628 'name' => 'gub-group',
629 'title' => 'gub-group-title',
630 'type' => ChangesListBooleanFilterGroup::TYPE,
631 'priority' => -1,
632 'filters' => [
633 [
634 'name' => 'hidebar',
635 'label' => 'bar-label',
636 'description' => 'bar-description',
637 'default' => false,
638 'priority' => 4,
639 'cssClass' => null,
640 'conflicts' => [],
641 'subset' => [],
642 ],
643 [
644 'name' => 'hidefoo',
645 'label' => 'foo-label',
646 'description' => 'foo-description',
647 'default' => true,
648 'priority' => 2,
649 'cssClass' => null,
650 'conflicts' => [],
651 'subset' => [],
652 ],
653 ],
654 'fullCoverage' => true,
655 'conflicts' => [],
656 ],
657
658 [
659 'name' => 'des-group',
660 'title' => 'des-group-title',
661 'type' => ChangesListStringOptionsFilterGroup::TYPE,
662 'priority' => -2,
663 'fullCoverage' => true,
664 'filters' => [
665 [
666 'name' => 'grault',
667 'label' => 'grault-label',
668 'description' => 'grault-description',
669 'cssClass' => null,
670 'priority' => -2,
671 'conflicts' => [],
672 'subset' => [],
673 ],
674 [
675 'name' => 'garply',
676 'label' => 'garply-label',
677 'description' => 'garply-description',
678 'cssClass' => null,
679 'priority' => -3,
680 'conflicts' => [],
681 'subset' => [],
682 ],
683 ],
684 'conflicts' => [],
685 'separator' => ';',
686 'default' => ChangesListStringOptionsFilterGroup::NONE,
687 ],
688 ],
689 'messageKeys' => [
690 'gub-group-title',
691 'bar-label',
692 'bar-description',
693 'foo-label',
694 'foo-description',
695 'des-group-title',
696 'grault-label',
697 'grault-description',
698 'garply-label',
699 'garply-description',
700 ],
701 ],
702 $this->changesListSpecialPage->getStructuredFilterJsData(),
703 /** ordered= */ false,
704 /** named= */ true
705 );
706 }
707
708 public function provideParseParameters() {
709 return [
710 [ 'hidebots', [ 'hidebots' => true ] ],
711
712 [ 'bots', [ 'hidebots' => false ] ],
713
714 [ 'hideminor', [ 'hideminor' => true ] ],
715
716 [ 'minor', [ 'hideminor' => false ] ],
717
718 [ 'hidemajor', [ 'hidemajor' => true ] ],
719
720 [ 'hideliu', [ 'hideliu' => true ] ],
721
722 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
723
724 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
725
726 [ 'hideanons', [ 'hideanons' => true ] ],
727
728 [ 'hidemyself', [ 'hidemyself' => true ] ],
729
730 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
731
732 [ 'hidehumans', [ 'hidehumans' => true ] ],
733
734 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
735
736 [ 'pagedits', [ 'hidepageedits' => false ] ],
737
738 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
739
740 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
741
742 [ 'hidelog', [ 'hidelog' => true ] ],
743
744 [
745 'userExpLevel=learner;experienced',
746 [
747 'userExpLevel' => 'learner;experienced'
748 ],
749 ],
750
751 // A few random combos
752 [
753 'bots,hideliu,hidemyself',
754 [
755 'hidebots' => false,
756 'hideliu' => true,
757 'hidemyself' => true,
758 ],
759 ],
760
761 [
762 'minor,hideanons,categorization',
763 [
764 'hideminor' => false,
765 'hideanons' => true,
766 'hidecategorization' => false,
767 ]
768 ],
769
770 [
771 'hidehumans,bots,hidecategorization',
772 [
773 'hidehumans' => true,
774 'hidebots' => false,
775 'hidecategorization' => true,
776 ],
777 ],
778
779 [
780 'hidemyself,userExpLevel=newcomer;learner,hideminor',
781 [
782 'hidemyself' => true,
783 'hideminor' => true,
784 'userExpLevel' => 'newcomer;learner',
785 ],
786 ],
787 ];
788 }
789
790 public function provideGetFilterConflicts() {
791 return [
792 [
793 "parameters" => [],
794 "expectedConflicts" => false,
795 ],
796 [
797 "parameters" => [
798 "hideliu" => true,
799 "userExpLevel" => "newcomer",
800 ],
801 "expectedConflicts" => true,
802 ],
803 [
804 "parameters" => [
805 "hideanons" => true,
806 "userExpLevel" => "learner",
807 ],
808 "expectedConflicts" => false,
809 ],
810 [
811 "parameters" => [
812 "hidemajor" => true,
813 "hidenewpages" => true,
814 "hidepageedits" => true,
815 "hidecategorization" => false,
816 "hidelog" => true,
817 "hideWikidata" => true,
818 ],
819 "expectedConflicts" => true,
820 ],
821 [
822 "parameters" => [
823 "hidemajor" => true,
824 "hidenewpages" => false,
825 "hidepageedits" => true,
826 "hidecategorization" => false,
827 "hidelog" => false,
828 "hideWikidata" => true,
829 ],
830 "expectedConflicts" => true,
831 ],
832 [
833 "parameters" => [
834 "hidemajor" => true,
835 "hidenewpages" => false,
836 "hidepageedits" => false,
837 "hidecategorization" => true,
838 "hidelog" => true,
839 "hideWikidata" => true,
840 ],
841 "expectedConflicts" => false,
842 ],
843 [
844 "parameters" => [
845 "hideminor" => true,
846 "hidenewpages" => true,
847 "hidepageedits" => true,
848 "hidecategorization" => false,
849 "hidelog" => true,
850 "hideWikidata" => true,
851 ],
852 "expectedConflicts" => false,
853 ],
854 ];
855 }
856
857 /**
858 * @dataProvider provideGetFilterConflicts
859 */
860 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
861 $context = new RequestContext;
862 $context->setRequest( new FauxRequest( $parameters ) );
863 $this->changesListSpecialPage->setContext( $context );
864
865 $this->assertEquals(
866 $expectedConflicts,
867 $this->changesListSpecialPage->areFiltersInConflict()
868 );
869 }
870
871 public function validateOptionsProvider() {
872 return [
873 [
874 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
875 true,
876 [ 'hideliu' => 1, 'hidebots' => 1, ],
877 ],
878
879 [
880 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
881 true,
882 [ 'hidebots' => 0, 'hidehumans' => 1 ],
883 ],
884
885 [
886 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
887 true,
888 [],
889 ],
890 [
891 [ 'hidebots' => 1, 'hidehumans' => 1 ],
892 true,
893 [],
894 ],
895 [
896 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
897 true,
898 [],
899 ],
900 [
901 [ 'hideminor' => 1, 'hidemajor' => 1 ],
902 true,
903 [],
904 ],
905 [
906 // changeType
907 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, ],
908 true,
909 [],
910 ],
911 ];
912 }
913 }