Merge "mediawiki.content.json: Remove file and author annotations"
[mediawiki.git] / tests / phpunit / includes / search / SearchEnginePrefixTest.php
bloba756881a512b54732801ec4d8fe2452b11769a6b
1 <?php
3 use MediaWiki\MainConfigNames;
4 use MediaWiki\Title\Title;
6 /**
7 * @group Search
8 * @group Database
9 */
10 class SearchEnginePrefixTest extends MediaWikiLangTestCase {
11 private SearchEngine $search;
13 public function addDBDataOnce() {
14 if ( !$this->isWikitextNS( NS_MAIN ) ) {
15 // tests are skipped if NS_MAIN is not wikitext
16 return;
19 $this->insertPage( 'Sandbox' );
20 $this->insertPage( 'Bar' );
21 $this->insertPage( 'Example' );
22 $this->insertPage( 'Example Bar' );
23 $this->insertPage( 'Example Foo' );
24 $this->insertPage( 'Example Foo/Bar' );
25 $this->insertPage( 'Example/Baz' );
26 $this->insertPage( 'Sample' );
27 $this->insertPage( 'Sample Ban' );
28 $this->insertPage( 'Sample Eat' );
29 $this->insertPage( 'Sample Who' );
30 $this->insertPage( 'Sample Zoo' );
31 $this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
32 $this->insertPage( 'Redirect Test' );
33 $this->insertPage( 'Redirect Test Worse Result' );
34 $this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
35 $this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
36 $this->insertPage( 'Redirect Test2' );
37 $this->insertPage( 'Redirect Test2 Worse Result' );
39 $this->insertPage( 'Talk:Sandbox' );
40 $this->insertPage( 'Talk:Example' );
42 $this->insertPage( 'User:Example' );
43 $this->insertPage( 'Barcelona' );
44 $this->insertPage( 'Barbara' );
45 $this->insertPage( 'External' );
48 protected function setUp(): void {
49 parent::setUp();
51 if ( !$this->isWikitextNS( NS_MAIN ) ) {
52 $this->markTestSkipped( 'Main namespace does not support wikitext.' );
55 // Avoid special pages from extensions interferring with the tests
56 $this->overrideConfigValues( [
57 MainConfigNames::SpecialPages => [],
58 MainConfigNames::Hooks => [],
59 ] );
61 $this->search = $this->getServiceContainer()->newSearchEngine();
62 $this->search->setNamespaces( [] );
65 protected function searchProvision( ?array $results = null ) {
66 if ( $results === null ) {
67 $this->overrideConfigValue( MainConfigNames::Hooks, [] );
68 } else {
69 $this->setTemporaryHook(
70 'PrefixSearchBackend',
71 static function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
72 $srchres = $results;
73 return false;
79 public static function provideSearch() {
80 return [
81 [ [
82 'Empty string',
83 'query' => '',
84 'results' => [],
85 ] ],
86 [ [
87 'All invalid characters, effectively empty',
88 'query' => '[',
89 'results' => [],
90 ] ],
91 [ [
92 'Main namespace with title prefix',
93 'query' => 'Sa',
94 'results' => [
95 'Sample',
96 'Sample Ban',
97 'Sample Eat',
99 // Third result when testing offset
100 'offsetresult' => [
101 'Sample Who',
103 ] ],
105 'Some invalid characters',
106 'query' => '[[Sa]]',
107 'results' => [
108 'Sample',
109 'Sample Ban',
110 'Sample Eat',
112 'offsetresult' => [ 'Sample Who' ],
113 ] ],
115 'Talk namespace prefix',
116 'query' => 'Talk:',
117 'results' => [
118 'Talk:Example',
119 'Talk:Sandbox',
121 ] ],
123 'User namespace prefix',
124 'query' => 'User:',
125 'results' => [
126 'User:Example',
128 ] ],
130 'Special namespace prefix',
131 'query' => 'Special:',
132 'results' => [
133 'Special:ActiveUsers',
134 'Special:AllMessages',
135 'Special:AllPages',
137 // Third result when testing offset
138 'offsetresult' => [
139 'Special:AncientPages',
141 ] ],
143 'Special namespace with prefix',
144 'query' => 'Special:Un',
145 'results' => [
146 'Special:Unblock',
147 'Special:UncategorizedCategories',
148 'Special:UncategorizedFiles',
150 // Third result when testing offset
151 'offsetresult' => [
152 'Special:UncategorizedPages',
154 ] ],
156 'Special page name',
157 'query' => 'Special:EditWatchlist',
158 'results' => [
160 ] ],
162 'Special page subpages',
163 'query' => 'Special:EditWatchlist/',
164 'results' => [
165 'Special:EditWatchlist/clear',
166 'Special:EditWatchlist/raw',
168 ] ],
170 'Special page subpages with prefix',
171 'query' => 'Special:EditWatchlist/cl',
172 'results' => [
173 'Special:EditWatchlist/clear',
175 ] ],
180 * @dataProvider provideSearch
181 * @covers \SearchEngine::defaultPrefixSearch
183 public function testSearch( array $case ) {
184 $this->search->setLimitOffset( 3 );
185 $results = $this->search->defaultPrefixSearch( $case['query'] );
186 $results = array_map( static function ( Title $t ) {
187 return $t->getPrefixedText();
188 }, $results );
190 $this->assertEquals(
191 $case['results'],
192 $results,
193 $case[0]
198 * @dataProvider provideSearch
199 * @covers \SearchEngine::defaultPrefixSearch
201 public function testSearchWithOffset( array $case ) {
202 $this->search->setLimitOffset( 3, 1 );
203 $results = $this->search->defaultPrefixSearch( $case['query'] );
204 $results = array_map( static function ( Title $t ) {
205 return $t->getPrefixedText();
206 }, $results );
208 // We don't expect the first result when offsetting
209 array_shift( $case['results'] );
210 // And sometimes we expect a different last result
211 $expected = isset( $case['offsetresult'] ) ?
212 array_merge( $case['results'], $case['offsetresult'] ) :
213 $case['results'];
215 $this->assertEquals(
216 $expected,
217 $results,
218 $case[0]
222 public static function provideSearchBackend() {
223 return [
225 'Simple case',
226 'provision' => [
227 'Bar',
228 'Barcelona',
229 'Barbara',
231 'query' => 'Bar',
232 'results' => [
233 'Bar',
234 'Barcelona',
235 'Barbara',
237 ] ],
239 'Exact match not in first result should be moved to the first result (T72958)',
240 'provision' => [
241 'Barcelona',
242 'Bar',
243 'Barbara',
245 'query' => 'Bar',
246 'results' => [
247 'Bar',
248 'Barcelona',
249 'Barbara',
251 ] ],
253 'Exact match missing from results should be added as first result (T72958)',
254 'provision' => [
255 'Barcelona',
256 'Barbara',
257 'Bart',
259 'query' => 'Bar',
260 'results' => [
261 'Bar',
262 'Barcelona',
263 'Barbara',
265 ] ],
267 'Exact match missing and not existing pages should be dropped',
268 'provision' => [
269 'Exile',
270 'Exist',
271 'External',
273 'query' => 'Ex',
274 'results' => [
275 'External',
277 ] ],
279 "Exact match shouldn't override already found match if " .
280 "exact is redirect and found isn't",
281 'provision' => [
282 // Target of the exact match is low in the list
283 'Redirect Test Worse Result',
284 'Redirect Test',
286 'query' => 'redirect test',
287 'results' => [
288 // Redirect target is pulled up and exact match isn't added
289 'Redirect Test',
290 'Redirect Test Worse Result',
292 ] ],
294 "Exact match should override already found match if " .
295 "both exact match and found match are redirect",
296 'provision' => [
297 // Another redirect to the same target as the exact match
298 // is low in the list
299 'Redirect Test2 Worse Result',
300 'Redirect test2',
302 'query' => 'redirect TEST2',
303 'results' => [
304 // Found redirect is pulled to the top and exact match isn't
305 // added
306 'Redirect TEST2',
307 'Redirect Test2 Worse Result',
309 ] ],
311 "Exact match should override any already found matches that " .
312 "are redirects to it",
313 'provision' => [
314 // Another redirect to the same target as the exact match
315 // is low in the list
316 'Redirect Test Worse Result',
317 'Redirect test',
319 'query' => 'Redirect Test',
320 'results' => [
321 // Found redirect is pulled to the top and exact match isn't
322 // added
323 'Redirect Test',
324 'Redirect Test Worse Result',
325 'Redirect test',
327 ] ],
329 "Extra results must not be returned",
330 'provision' => [
331 'Example',
332 'Example Bar',
333 'Example Foo',
334 'Example Foo/Bar'
336 'query' => 'foo',
337 'results' => [
338 'Example',
339 'Example Bar',
340 'Example Foo',
342 ] ],
347 * @dataProvider provideSearchBackend
348 * @covers \PrefixSearch::searchBackend
350 public function testSearchBackend( array $case ) {
351 $search = $this->mockSearchWithResults( $case['provision'] );
352 $results = $search->completionSearch( $case['query'] );
354 $results = $results->map( static function ( SearchSuggestion $s ) {
355 return $s->getText();
356 } );
358 $this->assertEquals(
359 $case['results'],
360 $results,
361 $case[0]
365 public static function paginationProvider() {
366 $res = [ 'Example', 'Example Bar', 'Example Foo', 'Example Foo/Bar' ];
367 return [
368 'With less than requested results no pagination' => [
369 false, array_slice( $res, 0, 2 ),
371 'With same as requested results no pagination' => [
372 false, array_slice( $res, 0, 3 ),
374 'With extra result returned offer pagination' => [
375 true, $res,
381 * @dataProvider paginationProvider
382 * @covers \SearchSuggestionSet::hasMoreResults
384 public function testPagination( $hasMoreResults, $provision ) {
385 $search = $this->mockSearchWithResults( $provision );
386 $results = $search->completionSearch( 'irrelevant' );
388 $this->assertEquals( $hasMoreResults, $results->hasMoreResults() );
391 private function mockSearchWithResults( $titleStrings, $limit = 3 ) {
392 $search = $this->getMockBuilder( SearchEngine::class )
393 ->onlyMethods( [ 'completionSearchBackend' ] )->getMock();
395 $return = SearchSuggestionSet::fromStrings( $titleStrings );
397 $search->method( 'completionSearchBackend' )
398 ->willReturn( $return );
400 $search->setLimitOffset( $limit );
401 return $search;