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