Files
BookStack/tests/Entity/BookTest.php
Dan Brown 18364d1e6e WYSIWYG: Added inline code support to minimal editor
Used for comments and descriptions.
Also updated shortcut handling that we're not registering shortcuts for
edits which can't use the related formatting types.

For #6003
2026-04-16 11:11:06 +01:00

303 lines
11 KiB
PHP

<?php
namespace Tests\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookRepo;
use Tests\TestCase;
class BookTest extends TestCase
{
public function test_create()
{
$book = Book::factory()->make([
'name' => 'My First Book',
]);
$resp = $this->asEditor()->get('/books');
$this->withHtml($resp)->assertElementContains('a[href="' . url('/create-book') . '"]', 'Create New Book');
$resp = $this->get('/create-book');
$this->withHtml($resp)->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book');
$resp = $this->post('/books', $book->only('name', 'description_html'));
$resp->assertRedirect('/books/my-first-book');
$resp = $this->get('/books/my-first-book');
$resp->assertSee($book->name);
$resp->assertSee($book->descriptionInfo()->getPlain());
}
public function test_create_uses_different_slugs_when_name_reused()
{
$book = Book::factory()->make([
'name' => 'My First Book',
]);
$this->asEditor()->post('/books', $book->only('name', 'description_html'));
$this->asEditor()->post('/books', $book->only('name', 'description_html'));
$books = Book::query()->where('name', '=', $book->name)
->orderBy('id', 'desc')
->take(2)
->get();
$this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug);
$this->assertEquals('my-first-book', $books[1]->slug);
}
public function test_create_sets_tags()
{
// Cheeky initial update to refresh slug
$this->asEditor()->post('books', [
'name' => 'My book with tags',
'description_html' => '<p>A book with tags</p>',
'tags' => [
[
'name' => 'Category',
'value' => 'Donkey Content',
],
[
'name' => 'Level',
'value' => '5',
],
],
]);
/** @var Book $book */
$book = Book::query()->where('name', '=', 'My book with tags')->firstOrFail();
$tags = $book->tags()->get();
$this->assertEquals(2, $tags->count());
$this->assertEquals('Donkey Content', $tags[0]->value);
$this->assertEquals('Level', $tags[1]->name);
}
public function test_update()
{
$book = $this->entities->book();
// Cheeky initial update to refresh slug
$this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description_html' => $book->description_html]);
$book->refresh();
$newName = $book->name . ' Updated';
$newDesc = $book->description_html . '<p>with more content</p>';
$resp = $this->get($book->getUrl('/edit'));
$resp->assertSee($book->name);
$resp->assertSee($book->description_html);
$this->withHtml($resp)->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book');
$resp = $this->put($book->getUrl(), ['name' => $newName, 'description_html' => $newDesc]);
$resp->assertRedirect($book->getUrl() . '-updated');
$resp = $this->get($book->getUrl() . '-updated');
$resp->assertSee($newName);
$resp->assertSee($newDesc, false);
}
public function test_update_sets_tags()
{
$book = $this->entities->book();
$this->assertEquals(0, $book->tags()->count());
// Cheeky initial update to refresh slug
$this->asEditor()->put($book->getUrl(), [
'name' => $book->name,
'tags' => [
[
'name' => 'Category',
'value' => 'Dolphin Content',
],
[
'name' => 'Level',
'value' => '5',
],
],
]);
$book->refresh();
$tags = $book->tags()->get();
$this->assertEquals(2, $tags->count());
$this->assertEquals('Dolphin Content', $tags[0]->value);
$this->assertEquals('Level', $tags[1]->name);
}
public function test_delete()
{
$book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
$this->assertNull($book->deleted_at);
$pageCount = $book->pages()->count();
$chapterCount = $book->chapters()->count();
$deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
$deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
$deleteReq = $this->delete($book->getUrl());
$deleteReq->assertRedirect(url('/books'));
$this->assertActivityExists('book_delete', $book);
$book->refresh();
$this->assertNotNull($book->deleted_at);
$this->assertTrue($book->pages()->count() === 0);
$this->assertTrue($book->chapters()->count() === 0);
$this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
$this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
$this->assertTrue($book->deletions()->count() === 1);
$redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
$this->assertNotificationContains($redirectReq, 'Book Successfully Deleted');
}
public function test_delete_with_shelf_context_returns_to_shelf_view_after_delete()
{
$shelf = $this->entities->shelfHasBooks();
/** @var Book $book */
$book = $shelf->books()->first();
$this->asEditor()->get($shelf->getUrl());
$this->get($book->getUrl());
$this->get($book->getUrl('/delete'));
$resp = $this->delete($book->getUrl());
$resp->assertRedirect($shelf->getUrl());
}
public function test_cancel_on_create_page_leads_back_to_books_listing()
{
$resp = $this->asEditor()->get('/create-book');
$this->withHtml($resp)->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel');
}
public function test_cancel_on_edit_book_page_leads_back_to_book()
{
$book = $this->entities->book();
$resp = $this->asEditor()->get($book->getUrl('/edit'));
$this->withHtml($resp)->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel');
}
public function test_next_previous_navigation_controls_show_within_book_content()
{
$book = $this->entities->book();
$chapter = $book->chapters->first();
$resp = $this->asEditor()->get($chapter->getUrl());
$this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Next');
$this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[0]->name, 0, 20));
$resp = $this->get($chapter->pages[0]->getUrl());
$this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[1]->name, 0, 20));
$this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Previous');
$this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));
}
public function test_recently_viewed_books_updates_as_expected()
{
$books = Book::take(2)->get();
$resp = $this->asAdmin()->get('/books');
$this->withHtml($resp)->assertElementNotContains('#recents', $books[0]->name)
->assertElementNotContains('#recents', $books[1]->name);
$this->get($books[0]->getUrl());
$this->get($books[1]->getUrl());
$resp = $this->get('/books');
$this->withHtml($resp)->assertElementContains('#recents', $books[0]->name)
->assertElementContains('#recents', $books[1]->name);
}
public function test_popular_books_updates_upon_visits()
{
$books = Book::take(2)->get();
$resp = $this->asAdmin()->get('/books');
$this->withHtml($resp)->assertElementNotContains('#popular', $books[0]->name)
->assertElementNotContains('#popular', $books[1]->name);
$this->get($books[0]->getUrl());
$this->get($books[1]->getUrl());
$this->get($books[0]->getUrl());
$resp = $this->get('/books');
$this->withHtml($resp)->assertElementContains('#popular .book:nth-child(1)', $books[0]->name)
->assertElementContains('#popular .book:nth-child(2)', $books[1]->name);
}
public function test_books_view_shows_view_toggle_option()
{
/** @var Book $book */
$editor = $this->users->editor();
setting()->putUser($editor, 'books_view_type', 'list');
$resp = $this->actingAs($editor)->get('/books');
$this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'Grid View');
$this->withHtml($resp)->assertElementExists('button[name="view"][value="grid"]');
$resp = $this->patch("/preferences/change-view/books", ['view' => 'grid']);
$resp->assertRedirect();
$this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
$resp = $this->actingAs($editor)->get('/books');
$this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'List View');
$this->withHtml($resp)->assertElementExists('button[name="view"][value="list"]');
$resp = $this->patch("/preferences/change-view/books", ['view_type' => 'list']);
$resp->assertRedirect();
$this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
}
public function test_description_limited_to_specific_html()
{
$book = $this->entities->book();
$input = '<h1>Test</h1><p id="abc" href="beans">Content<a href="#cat" target="_blank" data-a="b">a</a><section>Hello</section><code id="abc">code</code></p>';
$expected = '<p>Content<a href="#cat" target="_blank">a</a><code>code</code></p>';
$this->asEditor()->put($book->getUrl(), [
'name' => $book->name,
'description_html' => $input
]);
$book->refresh();
$this->assertEquals($expected, $book->description_html);
}
public function test_show_view_displays_description_if_no_description_html_set()
{
$book = $this->entities->book();
$book->description_html = '';
$book->description = "My great\ndescription\n\nwith newlines";
$book->save();
$resp = $this->asEditor()->get($book->getUrl());
$resp->assertSee("<p>My great<br>\ndescription<br>\n<br>\nwith newlines</p>", false);
}
public function test_description_with_only_br_tags_results_in_empty_p_tag_used_on_show()
{
$descriptions = [
'<p><br></p>',
'<p><br><br><br><br></p>',
'<p><br><br><br></p><h1><br><br><br><br><br></h1>',
];
$book = $this->entities->book();
$this->asEditor();
foreach ($descriptions as $descriptionTestCase) {
$book->description_html = $descriptionTestCase;
$book->save();
$resp = $this->get($book->getUrl());
$html = $this->withHtml($resp);
$descriptionHtml = $html->getInnerHtml('.book-content > div.text-muted:first-child');
$this->assertEquals('<p></p>', $descriptionHtml);
}
}
}