Merge "Hide TOC with CSS instead of JavaScript"
[lhc/web/wiklou.git] / tests / phpunit / includes / search / SearchEnginePrefixTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use Wikimedia\TestingAccessWrapper;
5
6 /**
7 * @group Search
8 * @group Database
9 */
10 class SearchEnginePrefixTest extends MediaWikiLangTestCase {
11 private $originalHandlers;
12
13 /**
14 * @var SearchEngine
15 */
16 private $search;
17
18 public function addDBDataOnce() {
19 if ( !$this->isWikitextNS( NS_MAIN ) ) {
20 // tests are skipped if NS_MAIN is not wikitext
21 return;
22 }
23
24 $this->insertPage( 'Sandbox' );
25 $this->insertPage( 'Bar' );
26 $this->insertPage( 'Example' );
27 $this->insertPage( 'Example Bar' );
28 $this->insertPage( 'Example Foo' );
29 $this->insertPage( 'Example Foo/Bar' );
30 $this->insertPage( 'Example/Baz' );
31 $this->insertPage( 'Sample' );
32 $this->insertPage( 'Sample Ban' );
33 $this->insertPage( 'Sample Eat' );
34 $this->insertPage( 'Sample Who' );
35 $this->insertPage( 'Sample Zoo' );
36 $this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
37 $this->insertPage( 'Redirect Test' );
38 $this->insertPage( 'Redirect Test Worse Result' );
39 $this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
40 $this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
41 $this->insertPage( 'Redirect Test2' );
42 $this->insertPage( 'Redirect Test2 Worse Result' );
43
44 $this->insertPage( 'Talk:Sandbox' );
45 $this->insertPage( 'Talk:Example' );
46
47 $this->insertPage( 'User:Example' );
48 $this->insertPage( 'Barcelona' );
49 $this->insertPage( 'Barbara' );
50 $this->insertPage( 'External' );
51 }
52
53 protected function setUp() {
54 parent::setUp();
55
56 if ( !$this->isWikitextNS( NS_MAIN ) ) {
57 $this->markTestSkipped( 'Main namespace does not support wikitext.' );
58 }
59
60 // Avoid special pages from extensions interferring with the tests
61 $this->setMwGlobals( [
62 'wgSpecialPages' => [],
63 'wgHooks' => [],
64 ] );
65
66 $this->search = MediaWikiServices::getInstance()->newSearchEngine();
67 $this->search->setNamespaces( [] );
68
69 $this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers;
70 TestingAccessWrapper::newFromClass( Hooks::class )->handlers = [];
71
72 SpecialPageFactory::resetList();
73 }
74
75 public function tearDown() {
76 parent::tearDown();
77
78 TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers;
79
80 SpecialPageFactory::resetList();
81 }
82
83 protected function searchProvision( array $results = null ) {
84 if ( $results === null ) {
85 $this->setMwGlobals( 'wgHooks', [] );
86 } else {
87 $this->setMwGlobals( 'wgHooks', [
88 'PrefixSearchBackend' => [
89 function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
90 $srchres = $results;
91 return false;
92 }
93 ],
94 ] );
95 }
96 }
97
98 public static function provideSearch() {
99 return [
100 [ [
101 'Empty string',
102 'query' => '',
103 'results' => [],
104 ] ],
105 [ [
106 'Main namespace with title prefix',
107 'query' => 'Sa',
108 'results' => [
109 'Sample',
110 'Sample Ban',
111 'Sample Eat',
112 ],
113 // Third result when testing offset
114 'offsetresult' => [
115 'Sample Who',
116 ],
117 ] ],
118 [ [
119 'Talk namespace prefix',
120 'query' => 'Talk:',
121 'results' => [
122 'Talk:Example',
123 'Talk:Sandbox',
124 ],
125 ] ],
126 [ [
127 'User namespace prefix',
128 'query' => 'User:',
129 'results' => [
130 'User:Example',
131 ],
132 ] ],
133 [ [
134 'Special namespace prefix',
135 'query' => 'Special:',
136 'results' => [
137 'Special:ActiveUsers',
138 'Special:AllMessages',
139 'Special:AllMyUploads',
140 ],
141 // Third result when testing offset
142 'offsetresult' => [
143 'Special:AllPages',
144 ],
145 ] ],
146 [ [
147 'Special namespace with prefix',
148 'query' => 'Special:Un',
149 'results' => [
150 'Special:Unblock',
151 'Special:UncategorizedCategories',
152 'Special:UncategorizedFiles',
153 ],
154 // Third result when testing offset
155 'offsetresult' => [
156 'Special:UncategorizedPages',
157 ],
158 ] ],
159 [ [
160 'Special page name',
161 'query' => 'Special:EditWatchlist',
162 'results' => [
163 'Special:EditWatchlist',
164 ],
165 ] ],
166 [ [
167 'Special page subpages',
168 'query' => 'Special:EditWatchlist/',
169 'results' => [
170 'Special:EditWatchlist/clear',
171 'Special:EditWatchlist/raw',
172 ],
173 ] ],
174 [ [
175 'Special page subpages with prefix',
176 'query' => 'Special:EditWatchlist/cl',
177 'results' => [
178 'Special:EditWatchlist/clear',
179 ],
180 ] ],
181 ];
182 }
183
184 /**
185 * @dataProvider provideSearch
186 * @covers SearchEngine::defaultPrefixSearch
187 */
188 public function testSearch( array $case ) {
189 $this->search->setLimitOffset( 3 );
190 $results = $this->search->defaultPrefixSearch( $case['query'] );
191 $results = array_map( function ( Title $t ) {
192 return $t->getPrefixedText();
193 }, $results );
194
195 $this->assertEquals(
196 $case['results'],
197 $results,
198 $case[0]
199 );
200 }
201
202 /**
203 * @dataProvider provideSearch
204 * @covers SearchEngine::defaultPrefixSearch
205 */
206 public function testSearchWithOffset( array $case ) {
207 $this->search->setLimitOffset( 3, 1 );
208 $results = $this->search->defaultPrefixSearch( $case['query'] );
209 $results = array_map( function ( Title $t ) {
210 return $t->getPrefixedText();
211 }, $results );
212
213 // We don't expect the first result when offsetting
214 array_shift( $case['results'] );
215 // And sometimes we expect a different last result
216 $expected = isset( $case['offsetresult'] ) ?
217 array_merge( $case['results'], $case['offsetresult'] ) :
218 $case['results'];
219
220 $this->assertEquals(
221 $expected,
222 $results,
223 $case[0]
224 );
225 }
226
227 public static function provideSearchBackend() {
228 return [
229 [ [
230 'Simple case',
231 'provision' => [
232 'Bar',
233 'Barcelona',
234 'Barbara',
235 ],
236 'query' => 'Bar',
237 'results' => [
238 'Bar',
239 'Barcelona',
240 'Barbara',
241 ],
242 ] ],
243 [ [
244 'Exact match not in first result should be moved to the first result (T72958)',
245 'provision' => [
246 'Barcelona',
247 'Bar',
248 'Barbara',
249 ],
250 'query' => 'Bar',
251 'results' => [
252 'Bar',
253 'Barcelona',
254 'Barbara',
255 ],
256 ] ],
257 [ [
258 'Exact match missing from results should be added as first result (T72958)',
259 'provision' => [
260 'Barcelona',
261 'Barbara',
262 'Bart',
263 ],
264 'query' => 'Bar',
265 'results' => [
266 'Bar',
267 'Barcelona',
268 'Barbara',
269 ],
270 ] ],
271 [ [
272 'Exact match missing and not existing pages should be dropped',
273 'provision' => [
274 'Exile',
275 'Exist',
276 'External',
277 ],
278 'query' => 'Ex',
279 'results' => [
280 'External',
281 ],
282 ] ],
283 [ [
284 "Exact match shouldn't override already found match if " .
285 "exact is redirect and found isn't",
286 'provision' => [
287 // Target of the exact match is low in the list
288 'Redirect Test Worse Result',
289 'Redirect Test',
290 ],
291 'query' => 'redirect test',
292 'results' => [
293 // Redirect target is pulled up and exact match isn't added
294 'Redirect Test',
295 'Redirect Test Worse Result',
296 ],
297 ] ],
298 [ [
299 "Exact match shouldn't override already found match if " .
300 "both exact match and found match are redirect",
301 'provision' => [
302 // Another redirect to the same target as the exact match
303 // is low in the list
304 'Redirect Test2 Worse Result',
305 'Redirect test2',
306 ],
307 'query' => 'redirect TEST2',
308 'results' => [
309 // Found redirect is pulled to the top and exact match isn't
310 // added
311 'Redirect test2',
312 'Redirect Test2 Worse Result',
313 ],
314 ] ],
315 [ [
316 "Exact match should override any already found matches that " .
317 "are redirects to it",
318 'provision' => [
319 // Another redirect to the same target as the exact match
320 // is low in the list
321 'Redirect Test Worse Result',
322 'Redirect test',
323 ],
324 'query' => 'Redirect Test',
325 'results' => [
326 // Found redirect is pulled to the top and exact match isn't
327 // added
328 'Redirect Test',
329 'Redirect Test Worse Result',
330 'Redirect test',
331 ],
332 ] ],
333 [ [
334 "Extra results must not be returned",
335 'provision' => [
336 'Example',
337 'Example Bar',
338 'Example Foo',
339 'Example Foo/Bar'
340 ],
341 'query' => 'foo',
342 'results' => [
343 'Example',
344 'Example Bar',
345 'Example Foo',
346 ],
347 ] ],
348 ];
349 }
350
351 /**
352 * @dataProvider provideSearchBackend
353 * @covers PrefixSearch::searchBackend
354 */
355 public function testSearchBackend( array $case ) {
356 $search = $this->mockSearchWithResults( $case['provision'] );
357 $results = $search->completionSearch( $case['query'] );
358
359 $results = $results->map( function ( SearchSuggestion $s ) {
360 return $s->getText();
361 } );
362
363 $this->assertEquals(
364 $case['results'],
365 $results,
366 $case[0]
367 );
368 }
369
370 public function paginationProvider() {
371 $res = [ 'Example', 'Example Bar', 'Example Foo', 'Example Foo/Bar' ];
372 return [
373 'With less than requested results no pagination' => [
374 false, array_slice( $res, 0, 2 ),
375 ],
376 'With same as requested results no pagination' => [
377 false, array_slice( $res, 0, 3 ),
378 ],
379 'With extra result returned offer pagination' => [
380 true, $res,
381 ],
382 ];
383 }
384
385 /**
386 * @dataProvider paginationProvider
387 */
388 public function testPagination( $hasMoreResults, $provision ) {
389 $search = $this->mockSearchWithResults( $provision );
390 $results = $search->completionSearch( 'irrelevant' );
391
392 $this->assertEquals( $hasMoreResults, $results->hasMoreResults() );
393 }
394
395 private function mockSearchWithResults( $titleStrings, $limit = 3 ) {
396 $search = $stub = $this->getMockBuilder( SearchEngine::class )
397 ->setMethods( [ 'completionSearchBackend' ] )->getMock();
398
399 $return = SearchSuggestionSet::fromStrings( $titleStrings );
400
401 $search->expects( $this->any() )
402 ->method( 'completionSearchBackend' )
403 ->will( $this->returnValue( $return ) );
404
405 $search->setLimitOffset( $limit );
406 return $search;
407 }
408 }