]> BookStack Code Mirror - bookstack/blob - tests/Entity/EntitySearchTest.php
Thumbnails: Fixed thumnail orientation
[bookstack] / tests / Entity / EntitySearchTest.php
1 <?php
2
3 namespace Tests\Entity;
4
5 use BookStack\Activity\Models\Tag;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Models\Chapter;
9 use Illuminate\Support\Str;
10 use Tests\TestCase;
11
12 class EntitySearchTest extends TestCase
13 {
14     public function test_page_search()
15     {
16         $book = $this->entities->book();
17         $page = $book->pages->first();
18
19         $search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
20         $search->assertSee('Search Results');
21         $search->assertSeeText($page->name, true);
22     }
23
24     public function test_bookshelf_search()
25     {
26         $shelf = $this->entities->shelf();
27
28         $search = $this->asEditor()->get('/search?term=' . urlencode($shelf->name) . '  {type:bookshelf}');
29         $search->assertSee('Search Results');
30         $search->assertSeeText($shelf->name, true);
31     }
32
33     public function test_invalid_page_search()
34     {
35         $resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
36         $resp->assertSee('Search Results');
37         $resp->assertStatus(200);
38         $this->get('/search?term=cat+-')->assertStatus(200);
39     }
40
41     public function test_empty_search_shows_search_page()
42     {
43         $res = $this->asEditor()->get('/search');
44         $res->assertStatus(200);
45     }
46
47     public function test_searching_accents_and_small_terms()
48     {
49         $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content a2 orange dog']);
50         $this->asEditor();
51
52         $accentSearch = $this->get('/search?term=' . urlencode('áéíí'));
53         $accentSearch->assertStatus(200)->assertSee($page->name);
54
55         $smallSearch = $this->get('/search?term=' . urlencode('a2'));
56         $smallSearch->assertStatus(200)->assertSee($page->name);
57     }
58
59     public function test_book_search()
60     {
61         $book = Book::first();
62         $page = $book->pages->last();
63         $chapter = $book->chapters->last();
64
65         $pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name));
66         $pageTestResp->assertSee($page->name);
67
68         $chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name));
69         $chapterTestResp->assertSee($chapter->name);
70     }
71
72     public function test_chapter_search()
73     {
74         $chapter = $this->entities->chapterHasPages();
75         $page = $chapter->pages[0];
76
77         $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));
78         $pageTestResp->assertSee($page->name);
79     }
80
81     public function test_tag_search()
82     {
83         $newTags = [
84             new Tag([
85                 'name'  => 'animal',
86                 'value' => 'cat',
87             ]),
88             new Tag([
89                 'name'  => 'color',
90                 'value' => 'red',
91             ]),
92         ];
93
94         $pageA = $this->entities->page();
95         $pageA->tags()->saveMany($newTags);
96
97         $pageB = $this->entities->page();
98         $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
99
100         $this->asEditor();
101         $tNameSearch = $this->get('/search?term=%5Banimal%5D');
102         $tNameSearch->assertSee($pageA->name)->assertSee($pageB->name);
103
104         $tNameSearch2 = $this->get('/search?term=%5Bcolor%5D');
105         $tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name);
106
107         $tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D');
108         $tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name);
109     }
110
111     public function test_exact_searches()
112     {
113         $page = $this->entities->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']);
114
115         $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"'));
116         $exactSearchA->assertStatus(200)->assertSee($page->name);
117
118         $exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('"story not about an orange"'));
119         $exactSearchB->assertStatus(200)->assertDontSee($page->name);
120     }
121
122     public function test_negated_searches()
123     {
124         $page = $this->entities->newPage(['name' => 'My new test negation page', 'html' => '<p>An angry tortoise wore trumpeted plimsoles</p>']);
125         $page->tags()->saveMany([new Tag(['name' => 'DonkCount', 'value' => '500'])]);
126         $page->created_by = $this->users->admin()->id;
127         $page->save();
128
129         $editor = $this->users->editor();
130         $this->actingAs($editor);
131
132         $exactSearch = $this->get('/search?term=' . urlencode('negation -"tortoise"'));
133         $exactSearch->assertStatus(200)->assertDontSeeText($page->name);
134
135         $tagSearchA = $this->get('/search?term=' . urlencode('negation [DonkCount=500]'));
136         $tagSearchA->assertStatus(200)->assertSeeText($page->name);
137         $tagSearchB = $this->get('/search?term=' . urlencode('negation -[DonkCount=500]'));
138         $tagSearchB->assertStatus(200)->assertDontSeeText($page->name);
139
140         $filterSearchA = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));
141         $filterSearchA->assertStatus(200)->assertSeeText($page->name);
142         $page->created_by = $editor->id;
143         $page->save();
144         $filterSearchB = $this->get('/search?term=' . urlencode('negation -{created_by:me}'));
145         $filterSearchB->assertStatus(200)->assertDontSeeText($page->name);
146     }
147
148     public function test_search_terms_with_delimiters_are_converted_to_exact_matches()
149     {
150         $this->asEditor();
151         $page = $this->entities->newPage(['name' => 'Delimiter test', 'html' => '<p>1.1 2,2 3?3 4:4 5;5 (8) &lt;9&gt; "10" \'11\' `12`</p>']);
152         $terms = explode(' ', '1.1 2,2 3?3 4:4 5;5 (8) <9> "10" \'11\' `12`');
153
154         foreach ($terms as $term) {
155             $search = $this->get('/search?term=' . urlencode($term));
156             $search->assertSee($page->name);
157         }
158     }
159
160     public function test_search_filters()
161     {
162         $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
163         $editor = $this->users->editor();
164         $this->actingAs($editor);
165
166         // Viewed filter searches
167         $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
168         $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name);
169         $this->get($page->getUrl());
170         $this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name);
171         $this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name);
172
173         // User filters
174         $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
175         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
176         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
177         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertDontSee($page->name);
178         $page->created_by = $editor->id;
179         $page->save();
180         $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
181         $this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editor->slug . '}'))->assertSee($page->name);
182         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
183         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
184         $page->updated_by = $editor->id;
185         $page->save();
186         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
187         $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertSee($page->name);
188         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
189         $page->owned_by = $editor->id;
190         $page->save();
191         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);
192         $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editor->slug . '}'))->assertSee($page->name);
193
194         // Content filters
195         $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
196         $this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name);
197         $this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name);
198         $this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name);
199
200         // Restricted filter
201         $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
202         $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles->first()]);
203         $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
204
205         // Date filters
206         $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name);
207         $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name);
208         $page->updated_at = '2037-02-01';
209         $page->save();
210         $this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name);
211         $this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name);
212
213         $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name);
214         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name);
215         $page->created_at = '2037-02-01';
216         $page->save();
217         $this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name);
218         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
219     }
220
221     public function test_entity_selector_search()
222     {
223         $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);
224         $notVisitedPage = $this->entities->page();
225
226         // Visit the page to make popular
227         $this->asEditor()->get($page->getUrl());
228
229         $normalSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
230         $normalSearch->assertSee($page->name);
231
232         $bookSearch = $this->get('/search/entity-selector?types=book&term=' . urlencode($page->name));
233         $bookSearch->assertDontSee($page->name);
234
235         $defaultListTest = $this->get('/search/entity-selector');
236         $defaultListTest->assertSee($page->name);
237         $defaultListTest->assertDontSee($notVisitedPage->name);
238     }
239
240     public function test_entity_selector_search_shows_breadcrumbs()
241     {
242         $chapter = $this->entities->chapter();
243         $page = $chapter->pages->first();
244         $this->asEditor();
245
246         $pageSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
247         $pageSearch->assertSee($page->name);
248         $pageSearch->assertSee($chapter->getShortName(42));
249         $pageSearch->assertSee($page->book->getShortName(42));
250
251         $chapterSearch = $this->get('/search/entity-selector?term=' . urlencode($chapter->name));
252         $chapterSearch->assertSee($chapter->name);
253         $chapterSearch->assertSee($chapter->book->getShortName(42));
254     }
255
256     public function test_entity_selector_shows_breadcrumbs_on_default_view()
257     {
258         $page = $this->entities->pageWithinChapter();
259         $this->asEditor()->get($page->chapter->getUrl());
260
261         $resp = $this->asEditor()->get('/search/entity-selector?types=book,chapter&permission=page-create');
262         $html = $this->withHtml($resp);
263         $html->assertElementContains('.chapter.entity-list-item', $page->chapter->name);
264         $html->assertElementContains('.chapter.entity-list-item .entity-item-snippet', $page->book->getShortName(42));
265     }
266
267     public function test_entity_selector_search_reflects_items_without_permission()
268     {
269         $page = $this->entities->page();
270         $baseSelector = 'a[data-entity-type="page"][data-entity-id="' . $page->id . '"]';
271         $searchUrl = '/search/entity-selector?permission=update&term=' . urlencode($page->name);
272
273         $resp = $this->asEditor()->get($searchUrl);
274         $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
275         $this->withHtml($resp)->assertElementNotContains($baseSelector, "You don't have the required permissions to select this item");
276
277         $resp = $this->actingAs($this->users->viewer())->get($searchUrl);
278         $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
279         $this->withHtml($resp)->assertElementContains($baseSelector, "You don't have the required permissions to select this item");
280     }
281
282     public function test_entity_template_selector_search()
283     {
284         $templatePage = $this->entities->newPage(['name' => 'Template search test', 'html' => 'template test']);
285         $templatePage->template = true;
286         $templatePage->save();
287
288         $nonTemplatePage = $this->entities->newPage(['name' => 'Nontemplate page', 'html' => 'nontemplate', 'template' => false]);
289
290         // Visit both to make popular
291         $this->asEditor()->get($templatePage->getUrl());
292         $this->get($nonTemplatePage->getUrl());
293
294         $normalSearch = $this->get('/search/entity-selector-templates?term=test');
295         $normalSearch->assertSee($templatePage->name);
296         $normalSearch->assertDontSee($nonTemplatePage->name);
297
298         $normalSearch = $this->get('/search/entity-selector-templates?term=beans');
299         $normalSearch->assertDontSee($templatePage->name);
300         $normalSearch->assertDontSee($nonTemplatePage->name);
301
302         $defaultListTest = $this->get('/search/entity-selector-templates');
303         $defaultListTest->assertSee($templatePage->name);
304         $defaultListTest->assertDontSee($nonTemplatePage->name);
305
306         $this->permissions->disableEntityInheritedPermissions($templatePage);
307
308         $normalSearch = $this->get('/search/entity-selector-templates?term=test');
309         $normalSearch->assertDontSee($templatePage->name);
310
311         $defaultListTest = $this->get('/search/entity-selector-templates');
312         $defaultListTest->assertDontSee($templatePage->name);
313     }
314
315     public function test_sibling_search_for_pages()
316     {
317         $chapter = $this->entities->chapterHasPages();
318         $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling');
319         $page = $chapter->pages->first();
320
321         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
322         $search->assertSuccessful();
323         foreach ($chapter->pages as $page) {
324             $search->assertSee($page->name);
325         }
326
327         $search->assertDontSee($chapter->name);
328     }
329
330     public function test_sibling_search_for_pages_without_chapter()
331     {
332         $page = $this->entities->pageNotWithinChapter();
333         $bookChildren = $page->book->getDirectVisibleChildren();
334         $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
335
336         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
337         $search->assertSuccessful();
338         foreach ($bookChildren as $child) {
339             $search->assertSee($child->name);
340         }
341
342         $search->assertDontSee($page->book->name);
343     }
344
345     public function test_sibling_search_for_chapters()
346     {
347         $chapter = $this->entities->chapter();
348         $bookChildren = $chapter->book->getDirectVisibleChildren();
349         $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
350
351         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter");
352         $search->assertSuccessful();
353         foreach ($bookChildren as $child) {
354             $search->assertSee($child->name);
355         }
356
357         $search->assertDontSee($chapter->book->name);
358     }
359
360     public function test_sibling_search_for_books()
361     {
362         $books = Book::query()->take(10)->get();
363         $book = $books->first();
364         $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling');
365
366         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book");
367         $search->assertSuccessful();
368         foreach ($books as $expectedBook) {
369             $search->assertSee($expectedBook->name);
370         }
371     }
372
373     public function test_sibling_search_for_shelves()
374     {
375         $shelves = Bookshelf::query()->take(10)->get();
376         $shelf = $shelves->first();
377         $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling');
378
379         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf");
380         $search->assertSuccessful();
381         foreach ($shelves as $expectedShelf) {
382             $search->assertSee($expectedShelf->name);
383         }
384     }
385
386     public function test_sibling_search_for_books_provides_results_in_alphabetical_order()
387     {
388         $contextBook = $this->entities->book();
389         $searchBook = $this->entities->book();
390
391         $searchBook->name = 'Zebras';
392         $searchBook->save();
393
394         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book");
395         $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras');
396
397         $searchBook->name = '1AAAAAAArdvarks';
398         $searchBook->save();
399
400         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book");
401         $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks');
402     }
403
404     public function test_sibling_search_for_shelves_provides_results_in_alphabetical_order()
405     {
406         $contextShelf = $this->entities->shelf();
407         $searchShelf = $this->entities->shelf();
408
409         $searchShelf->name = 'Zebras';
410         $searchShelf->save();
411
412         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf");
413         $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras');
414
415         $searchShelf->name = '1AAAAAAArdvarks';
416         $searchShelf->save();
417
418         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf");
419         $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks');
420     }
421
422     public function test_search_works_on_updated_page_content()
423     {
424         $page = $this->entities->page();
425         $this->asEditor();
426
427         $update = $this->put($page->getUrl(), [
428             'name' => $page->name,
429             'html' => '<p>dog pandabearmonster spaghetti</p>',
430         ]);
431
432         $search = $this->asEditor()->get('/search?term=pandabearmonster');
433         $search->assertStatus(200);
434         $search->assertSeeText($page->name);
435         $search->assertSee($page->getUrl());
436     }
437
438     public function test_search_ranks_common_words_lower()
439     {
440         $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>dog biscuit dog dog</p>']);
441         $this->entities->newPage(['name' => 'Test page B', 'html' => '<p>cat biscuit</p>']);
442
443         $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');
444         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page A');
445         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page B');
446
447         for ($i = 0; $i < 2; $i++) {
448             $this->entities->newPage(['name' => 'Test page ' . $i, 'html' => '<p>dog</p>']);
449         }
450
451         $search = $this->asEditor()->get('/search?term=cat+dog+biscuit');
452         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page B');
453         $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page A');
454     }
455
456     public function test_terms_in_headers_have_an_adjusted_index_score()
457     {
458         $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '
459             <p>TermA</p>
460             <h1>TermB <strong>TermNested</strong></h1>
461             <h2>TermC</h2>
462             <h3>TermD</h3>
463             <h4>TermE</h4>
464             <h5>TermF</h5>
465             <h6>TermG</h6>
466         ']);
467
468         $scoreByTerm = $page->searchTerms()->pluck('score', 'term');
469
470         $this->assertEquals(1, $scoreByTerm->get('TermA'));
471         $this->assertEquals(10, $scoreByTerm->get('TermB'));
472         $this->assertEquals(10, $scoreByTerm->get('TermNested'));
473         $this->assertEquals(5, $scoreByTerm->get('TermC'));
474         $this->assertEquals(4, $scoreByTerm->get('TermD'));
475         $this->assertEquals(3, $scoreByTerm->get('TermE'));
476         $this->assertEquals(2, $scoreByTerm->get('TermF'));
477         // Is 1.5 but stored as integer, rounding up
478         $this->assertEquals(2, $scoreByTerm->get('TermG'));
479     }
480
481     public function test_indexing_works_as_expected_for_page_with_lots_of_terms()
482     {
483         $this->markTestSkipped('Time consuming test');
484
485         $count = 100000;
486         $text = '';
487         $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_#';
488         for ($i = 0; $i < $count; $i++) {
489             $text .= substr(str_shuffle($chars), 0, 5) . ' ';
490         }
491
492         $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>' . $text . '</p>']);
493
494         $termCount = $page->searchTerms()->count();
495
496         // Expect at least 90% unique rate
497         $this->assertGreaterThan($count * 0.9, $termCount);
498     }
499
500     public function test_name_and_content_terms_are_merged_to_single_score()
501     {
502         $page = $this->entities->newPage(['name' => 'TermA', 'html' => '
503             <p>TermA</p>
504         ']);
505
506         $scoreByTerm = $page->searchTerms()->pluck('score', 'term');
507
508         // Scores 40 for being in the name then 1 for being in the content
509         $this->assertEquals(41, $scoreByTerm->get('TermA'));
510     }
511
512     public function test_tag_names_and_values_are_indexed_for_search()
513     {
514         $page = $this->entities->newPage(['name' => 'PageA', 'html' => '<p>content</p>', 'tags' => [
515             ['name' => 'Animal', 'value' => 'MeowieCat'],
516             ['name' => 'SuperImportant'],
517         ]]);
518
519         $scoreByTerm = $page->searchTerms()->pluck('score', 'term');
520         $this->assertEquals(5, $scoreByTerm->get('MeowieCat'));
521         $this->assertEquals(3, $scoreByTerm->get('Animal'));
522         $this->assertEquals(3, $scoreByTerm->get('SuperImportant'));
523     }
524
525     public function test_matching_terms_in_search_results_are_highlighted()
526     {
527         $this->entities->newPage(['name' => 'My Meowie Cat', 'html' => '<p>A superimportant page about meowieable animals</p>', 'tags' => [
528             ['name' => 'Animal', 'value' => 'MeowieCat'],
529             ['name' => 'SuperImportant'],
530         ]]);
531
532         $search = $this->asEditor()->get('/search?term=SuperImportant+Meowie');
533         // Title
534         $search->assertSee('My <strong>Meowie</strong> Cat', false);
535         // Content
536         $search->assertSee('A <strong>superimportant</strong> page about <strong>meowie</strong>able animals', false);
537         // Tag name
538         $this->withHtml($search)->assertElementContains('.tag-name.highlight', 'SuperImportant');
539         // Tag value
540         $this->withHtml($search)->assertElementContains('.tag-value.highlight', 'MeowieCat');
541     }
542
543     public function test_match_highlighting_works_with_multibyte_content()
544     {
545         $this->entities->newPage([
546             'name' => 'Test Page',
547             'html' => '<p>На мен ми трябва нещо добро test</p>',
548         ]);
549
550         $search = $this->asEditor()->get('/search?term=' . urlencode('На мен ми трябва нещо добро'));
551         $search->assertSee('<strong>На</strong> <strong>мен</strong> <strong>ми</strong> <strong>трябва</strong> <strong>нещо</strong> <strong>добро</strong> test', false);
552     }
553
554     public function test_html_entities_in_item_details_remains_escaped_in_search_results()
555     {
556         $this->entities->newPage(['name' => 'My <cool> TestPageContent', 'html' => '<p>My supercool &lt;great&gt; TestPageContent page</p>']);
557
558         $search = $this->asEditor()->get('/search?term=TestPageContent');
559         $search->assertSee('My &lt;cool&gt; <strong>TestPageContent</strong>', false);
560         $search->assertSee('My supercool &lt;great&gt; <strong>TestPageContent</strong> page', false);
561     }
562
563     public function test_words_adjacent_to_lines_breaks_can_be_matched_with_normal_terms()
564     {
565         $page = $this->entities->newPage(['name' => 'TermA', 'html' => '
566             <p>TermA<br>TermB<br>TermC</p>
567         ']);
568
569         $search = $this->asEditor()->get('/search?term=' . urlencode('TermB TermC'));
570
571         $search->assertSee($page->getUrl(), false);
572     }
573
574     public function test_backslashes_can_be_searched_upon()
575     {
576         $page = $this->entities->newPage(['name' => 'TermA', 'html' => '
577             <p>More info is at the path \\\\cat\\dog\\badger</p>
578         ']);
579         $page->tags()->save(new Tag(['name' => '\\Category', 'value' => '\\animals\\fluffy']));
580
581         $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog'));
582         $search->assertSee($page->getUrl(), false);
583
584         $search = $this->asEditor()->get('/search?term=' . urlencode('"\\dog\\\\"'));
585         $search->assertSee($page->getUrl(), false);
586
587         $search = $this->asEditor()->get('/search?term=' . urlencode('"\\badger\\\\"'));
588         $search->assertDontSee($page->getUrl(), false);
589
590         $search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]'));
591         $search->assertSee($page->getUrl(), false);
592     }
593
594     public function test_searches_with_terms_without_controls_includes_them_in_extras()
595     {
596         $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:dan} {created_by:dan} -{viewed_by_me} -[a=b] -"dog" {is_template} {sort_by:last_commented}'));
597         $this->withHtml($resp)->assertFieldHasValue('extras', '{updated_by:dan} {created_by:dan} {is_template} {sort_by:last_commented} -"dog" -[a=b] -{viewed_by_me}');
598     }
599
600     public function test_negated_searches_dont_show_in_inputs()
601     {
602         $resp = $this->asEditor()->get('/search?term=' . urlencode('-{created_by:me} -[a=b] -"dog"'));
603         $this->withHtml($resp)->assertElementNotExists('input[name="tags[]"][value="a=b"]');
604         $this->withHtml($resp)->assertElementNotExists('input[name="exact[]"][value="dog"]');
605         $this->withHtml($resp)->assertElementNotExists('input[name="filters[created_by]"][value="me"][checked="checked"]');
606     }
607
608     public function test_searches_with_user_filters_using_me_adds_them_into_advanced_search_form()
609     {
610         $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:me} {created_by:me}'));
611         $this->withHtml($resp)->assertElementExists('form input[name="filters[updated_by]"][value="me"][checked="checked"]');
612         $this->withHtml($resp)->assertElementExists('form input[name="filters[created_by]"][value="me"][checked="checked"]');
613     }
614
615     public function test_search_suggestion_endpoint()
616     {
617         $this->entities->newPage(['name' => 'My suggestion page', 'html' => '<p>My supercool suggestion page</p>']);
618
619         // Test specific search
620         $resp = $this->asEditor()->get('/search/suggest?term="supercool+suggestion"');
621         $resp->assertSee('My suggestion page');
622         $resp->assertDontSee('My supercool suggestion page');
623         $resp->assertDontSee('No items available');
624         $this->withHtml($resp)->assertElementCount('a', 1);
625
626         // Test search limit
627         $resp = $this->asEditor()->get('/search/suggest?term=et');
628         $this->withHtml($resp)->assertElementCount('a', 5);
629
630         // Test empty state
631         $resp = $this->asEditor()->get('/search/suggest?term=spaghettisaurusrex');
632         $this->withHtml($resp)->assertElementCount('a', 0);
633         $resp->assertSee('No items available');
634     }
635 }