diff --git a/tests/Entity/EntitySearchTest.php b/tests/Search/EntitySearchTest.php similarity index 74% rename from tests/Entity/EntitySearchTest.php rename to tests/Search/EntitySearchTest.php index 5ace70e3a..9c76d0f71 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Search/EntitySearchTest.php @@ -1,12 +1,9 @@ <?php -namespace Tests\Entity; +namespace Search; use BookStack\Activity\Models\Tag; use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; -use Illuminate\Support\Str; use Tests\TestCase; class EntitySearchTest extends TestCase @@ -312,113 +309,6 @@ class EntitySearchTest extends TestCase $defaultListTest->assertDontSee($templatePage->name); } - public function test_sibling_search_for_pages() - { - $chapter = $this->entities->chapterHasPages(); - $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling'); - $page = $chapter->pages->first(); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); - $search->assertSuccessful(); - foreach ($chapter->pages as $page) { - $search->assertSee($page->name); - } - - $search->assertDontSee($chapter->name); - } - - public function test_sibling_search_for_pages_without_chapter() - { - $page = $this->entities->pageNotWithinChapter(); - $bookChildren = $page->book->getDirectVisibleChildren(); - $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); - $search->assertSuccessful(); - foreach ($bookChildren as $child) { - $search->assertSee($child->name); - } - - $search->assertDontSee($page->book->name); - } - - public function test_sibling_search_for_chapters() - { - $chapter = $this->entities->chapter(); - $bookChildren = $chapter->book->getDirectVisibleChildren(); - $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter"); - $search->assertSuccessful(); - foreach ($bookChildren as $child) { - $search->assertSee($child->name); - } - - $search->assertDontSee($chapter->book->name); - } - - public function test_sibling_search_for_books() - { - $books = Book::query()->take(10)->get(); - $book = $books->first(); - $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling'); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book"); - $search->assertSuccessful(); - foreach ($books as $expectedBook) { - $search->assertSee($expectedBook->name); - } - } - - public function test_sibling_search_for_shelves() - { - $shelves = Bookshelf::query()->take(10)->get(); - $shelf = $shelves->first(); - $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling'); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf"); - $search->assertSuccessful(); - foreach ($shelves as $expectedShelf) { - $search->assertSee($expectedShelf->name); - } - } - - public function test_sibling_search_for_books_provides_results_in_alphabetical_order() - { - $contextBook = $this->entities->book(); - $searchBook = $this->entities->book(); - - $searchBook->name = 'Zebras'; - $searchBook->save(); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book"); - $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras'); - - $searchBook->name = '1AAAAAAArdvarks'; - $searchBook->save(); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book"); - $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks'); - } - - public function test_sibling_search_for_shelves_provides_results_in_alphabetical_order() - { - $contextShelf = $this->entities->shelf(); - $searchShelf = $this->entities->shelf(); - - $searchShelf->name = 'Zebras'; - $searchShelf->save(); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf"); - $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras'); - - $searchShelf->name = '1AAAAAAArdvarks'; - $searchShelf->save(); - - $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf"); - $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks'); - } - public function test_search_works_on_updated_page_content() { $page = $this->entities->page(); @@ -453,75 +343,6 @@ class EntitySearchTest extends TestCase $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page A'); } - public function test_terms_in_headers_have_an_adjusted_index_score() - { - $page = $this->entities->newPage(['name' => 'Test page A', 'html' => ' - <p>TermA</p> - <h1>TermB <strong>TermNested</strong></h1> - <h2>TermC</h2> - <h3>TermD</h3> - <h4>TermE</h4> - <h5>TermF</h5> - <h6>TermG</h6> - ']); - - $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); - - $this->assertEquals(1, $scoreByTerm->get('TermA')); - $this->assertEquals(10, $scoreByTerm->get('TermB')); - $this->assertEquals(10, $scoreByTerm->get('TermNested')); - $this->assertEquals(5, $scoreByTerm->get('TermC')); - $this->assertEquals(4, $scoreByTerm->get('TermD')); - $this->assertEquals(3, $scoreByTerm->get('TermE')); - $this->assertEquals(2, $scoreByTerm->get('TermF')); - // Is 1.5 but stored as integer, rounding up - $this->assertEquals(2, $scoreByTerm->get('TermG')); - } - - public function test_indexing_works_as_expected_for_page_with_lots_of_terms() - { - $this->markTestSkipped('Time consuming test'); - - $count = 100000; - $text = ''; - $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_#'; - for ($i = 0; $i < $count; $i++) { - $text .= substr(str_shuffle($chars), 0, 5) . ' '; - } - - $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>' . $text . '</p>']); - - $termCount = $page->searchTerms()->count(); - - // Expect at least 90% unique rate - $this->assertGreaterThan($count * 0.9, $termCount); - } - - public function test_name_and_content_terms_are_merged_to_single_score() - { - $page = $this->entities->newPage(['name' => 'TermA', 'html' => ' - <p>TermA</p> - ']); - - $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); - - // Scores 40 for being in the name then 1 for being in the content - $this->assertEquals(41, $scoreByTerm->get('TermA')); - } - - public function test_tag_names_and_values_are_indexed_for_search() - { - $page = $this->entities->newPage(['name' => 'PageA', 'html' => '<p>content</p>', 'tags' => [ - ['name' => 'Animal', 'value' => 'MeowieCat'], - ['name' => 'SuperImportant'], - ]]); - - $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); - $this->assertEquals(5, $scoreByTerm->get('MeowieCat')); - $this->assertEquals(3, $scoreByTerm->get('Animal')); - $this->assertEquals(3, $scoreByTerm->get('SuperImportant')); - } - public function test_matching_terms_in_search_results_are_highlighted() { $this->entities->newPage(['name' => 'My Meowie Cat', 'html' => '<p>A superimportant page about meowieable animals</p>', 'tags' => [ diff --git a/tests/Search/SearchIndexingTest.php b/tests/Search/SearchIndexingTest.php new file mode 100644 index 000000000..43219a4ed --- /dev/null +++ b/tests/Search/SearchIndexingTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Search; + +use Tests\TestCase; + +class SearchIndexingTest extends TestCase +{ + public function test_terms_in_headers_have_an_adjusted_index_score() + { + $page = $this->entities->newPage(['name' => 'Test page A', 'html' => ' + <p>TermA</p> + <h1>TermB <strong>TermNested</strong></h1> + <h2>TermC</h2> + <h3>TermD</h3> + <h4>TermE</h4> + <h5>TermF</h5> + <h6>TermG</h6> + ']); + + $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); + + $this->assertEquals(1, $scoreByTerm->get('TermA')); + $this->assertEquals(10, $scoreByTerm->get('TermB')); + $this->assertEquals(10, $scoreByTerm->get('TermNested')); + $this->assertEquals(5, $scoreByTerm->get('TermC')); + $this->assertEquals(4, $scoreByTerm->get('TermD')); + $this->assertEquals(3, $scoreByTerm->get('TermE')); + $this->assertEquals(2, $scoreByTerm->get('TermF')); + // Is 1.5 but stored as integer, rounding up + $this->assertEquals(2, $scoreByTerm->get('TermG')); + } + + public function test_indexing_works_as_expected_for_page_with_lots_of_terms() + { + $this->markTestSkipped('Time consuming test'); + + $count = 100000; + $text = ''; + $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_#'; + for ($i = 0; $i < $count; $i++) { + $text .= substr(str_shuffle($chars), 0, 5) . ' '; + } + + $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '<p>' . $text . '</p>']); + + $termCount = $page->searchTerms()->count(); + + // Expect at least 90% unique rate + $this->assertGreaterThan($count * 0.9, $termCount); + } + + public function test_name_and_content_terms_are_merged_to_single_score() + { + $page = $this->entities->newPage(['name' => 'TermA', 'html' => ' + <p>TermA</p> + ']); + + $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); + + // Scores 40 for being in the name then 1 for being in the content + $this->assertEquals(41, $scoreByTerm->get('TermA')); + } + + public function test_tag_names_and_values_are_indexed_for_search() + { + $page = $this->entities->newPage(['name' => 'PageA', 'html' => '<p>content</p>', 'tags' => [ + ['name' => 'Animal', 'value' => 'MeowieCat'], + ['name' => 'SuperImportant'], + ]]); + + $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); + $this->assertEquals(5, $scoreByTerm->get('MeowieCat')); + $this->assertEquals(3, $scoreByTerm->get('Animal')); + $this->assertEquals(3, $scoreByTerm->get('SuperImportant')); + } +} diff --git a/tests/Entity/SearchOptionsTest.php b/tests/Search/SearchOptionsTest.php similarity index 99% rename from tests/Entity/SearchOptionsTest.php rename to tests/Search/SearchOptionsTest.php index 0c2ad271c..39c20c195 100644 --- a/tests/Entity/SearchOptionsTest.php +++ b/tests/Search/SearchOptionsTest.php @@ -1,6 +1,6 @@ <?php -namespace Tests\Entity; +namespace Search; use BookStack\Search\Options\ExactSearchOption; use BookStack\Search\Options\FilterSearchOption; diff --git a/tests/Search/SiblingSearchTest.php b/tests/Search/SiblingSearchTest.php new file mode 100644 index 000000000..63865af8a --- /dev/null +++ b/tests/Search/SiblingSearchTest.php @@ -0,0 +1,117 @@ +<?php + +namespace Search; + +use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Bookshelf; +use Tests\TestCase; + +class SiblingSearchTest extends TestCase +{ + public function test_sibling_search_for_pages() + { + $chapter = $this->entities->chapterHasPages(); + $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling'); + $page = $chapter->pages->first(); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); + $search->assertSuccessful(); + foreach ($chapter->pages as $page) { + $search->assertSee($page->name); + } + + $search->assertDontSee($chapter->name); + } + + public function test_sibling_search_for_pages_without_chapter() + { + $page = $this->entities->pageNotWithinChapter(); + $bookChildren = $page->book->getDirectVisibleChildren(); + $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); + $search->assertSuccessful(); + foreach ($bookChildren as $child) { + $search->assertSee($child->name); + } + + $search->assertDontSee($page->book->name); + } + + public function test_sibling_search_for_chapters() + { + $chapter = $this->entities->chapter(); + $bookChildren = $chapter->book->getDirectVisibleChildren(); + $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter"); + $search->assertSuccessful(); + foreach ($bookChildren as $child) { + $search->assertSee($child->name); + } + + $search->assertDontSee($chapter->book->name); + } + + public function test_sibling_search_for_books() + { + $books = Book::query()->take(10)->get(); + $book = $books->first(); + $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book"); + $search->assertSuccessful(); + foreach ($books as $expectedBook) { + $search->assertSee($expectedBook->name); + } + } + + public function test_sibling_search_for_shelves() + { + $shelves = Bookshelf::query()->take(10)->get(); + $shelf = $shelves->first(); + $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf"); + $search->assertSuccessful(); + foreach ($shelves as $expectedShelf) { + $search->assertSee($expectedShelf->name); + } + } + + public function test_sibling_search_for_books_provides_results_in_alphabetical_order() + { + $contextBook = $this->entities->book(); + $searchBook = $this->entities->book(); + + $searchBook->name = 'Zebras'; + $searchBook->save(); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book"); + $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras'); + + $searchBook->name = '1AAAAAAArdvarks'; + $searchBook->save(); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextBook->id}&entity_type=book"); + $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks'); + } + + public function test_sibling_search_for_shelves_provides_results_in_alphabetical_order() + { + $contextShelf = $this->entities->shelf(); + $searchShelf = $this->entities->shelf(); + + $searchShelf->name = 'Zebras'; + $searchShelf->save(); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf"); + $this->withHtml($search)->assertElementNotContains('a:first-child', 'Zebras'); + + $searchShelf->name = '1AAAAAAArdvarks'; + $searchShelf->save(); + + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$contextShelf->id}&entity_type=bookshelf"); + $this->withHtml($search)->assertElementContains('a:first-child', '1AAAAAAArdvarks'); + } +}