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