book-2.0.x-dev/tests/src/Functional/BookTest.php

tests/src/Functional/BookTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\book\Functional;

use Drupal\Core\Cache\Cache;
use Drupal\node\Entity\Node;
use Drupal\user\RoleInterface;

/**
 * Create a book, add pages, and test book interface.
 *
 * @group book
 * @group #slow
 */
class BookTest extends BookTestBase {

  /**
   * Tests the book navigation cache context.
   *
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Drupal\Core\Entity\EntityMalformedException
   *
   * @see \Drupal\book\Cache\BookNavigationCacheContext
   */
  public function testBookNavigationCacheContext(): void {
    // Create a page node.
    $this->drupalCreateContentType(['type' => 'page']);
    $page = $this->drupalCreateNode();

    // Create a book, consisting of book nodes.
    $book_nodes = $this->createBook();

    // Enable the debug output.
    $this->container->get('state')->set('book_test.debug_book_navigation_cache_context', TRUE);
    Cache::invalidateTags(['book_test.debug_book_navigation_cache_context']);

    $this->drupalLogin($this->bookAuthor);

    // On non-node route.
    $this->drupalGet($this->adminUser->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=book.none');

    // On non-book node route.
    $this->drupalGet($page->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=book.none');

    // On book node route.
    $this->drupalGet($book_nodes[0]->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=0|2|3');
    $this->drupalGet($book_nodes[1]->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=0|2|3|4');
    $this->drupalGet($book_nodes[2]->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=0|2|3|5');
    $this->drupalGet($book_nodes[3]->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=0|2|6');
    $this->drupalGet($book_nodes[4]->toUrl());
    $this->assertSession()->responseContains('[route.book_navigation]=0|2|7');
  }

  /**
   * Tests saving the book outline on an empty book.
   *
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testEmptyBook(): void {
    // Create a new empty book.
    $this->drupalLogin($this->bookAuthor);
    $book = $this->createBookNode('new');
    $this->drupalLogout();

    // Log in as a user with access to the book outline and save the form.
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/structure/book/' . $book->id());
    $this->assertSession()->statusCodeEquals(200);
    $this->submitForm([], 'Save book pages');
    $this->assertSession()->pageTextContains('Updated book ' . $book->label() . '.');

    // Test book that does not exist.
    $this->drupalGet('admin/structure/book/9999');
    $this->assertSession()->statusCodeEquals(404);
  }

  /**
   * Tests book functionality through node interfaces.
   *
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function testBook(): void {
    // Create new book.
    $nodes = $this->createBook();
    $book = $this->book;

    $this->drupalLogin($this->webUser);

    // Check that book pages display along with the correct outlines and
    // previous/next links.
    $this->checkBookNode($book, [$nodes[0], $nodes[3], $nodes[4]], FALSE, FALSE, $nodes[0], []);
    $this->checkBookNode($nodes[0], [$nodes[1], $nodes[2]], $book, $book, $nodes[1], [$book]);
    $this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], [$book, $nodes[0]]);
    $this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], [$book, $nodes[0]]);
    $this->checkBookNode($nodes[3], NULL, $nodes[2], $book, $nodes[4], [$book]);
    $this->checkBookNode($nodes[4], NULL, $nodes[3], $book, FALSE, [$book]);

    $this->drupalLogout();
    $this->drupalLogin($this->bookAuthor);

    // Check the presence of expected cache tags.
    $this->drupalGet('node/add/book');
    $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:book.settings');

    /*
     * Add Node 5 under Node 3.
     * Book
     *  |- Node 0
     *   |- Node 1
     *   |- Node 2
     *  |- Node 3
     *   |- Node 5
     *  |- Node 4
     */
    // Node 5.
    $nodes[] = $this->createBookNode($book->id(), $nodes[3]->book['nid']);
    $this->drupalLogout();
    $this->drupalLogin($this->webUser);
    // Verify the new outline - make sure we don't get stale cached data.
    $this->checkBookNode($nodes[3], [$nodes[5]], $nodes[2], $book, $nodes[5], [$book]);
    $this->checkBookNode($nodes[4], NULL, $nodes[5], $book, FALSE, [$book]);
    $this->drupalLogout();
    // Create a second book, and move an existing book page into it.
    $this->drupalLogin($this->bookAuthor);
    $other_book = $this->createBookNode('new');
    $node = $this->createBookNode($book->id());

    $this->addNodeToBook(intval($other_book->id()), intval($node->id()));

    $this->drupalLogout();
    $this->drupalLogin($this->webUser);

    // Check that the nodes in the second book are displayed correctly.
    // First we must set $this->book to the second book, so that the
    // correct regex will be generated for testing the outline.
    $this->book = $other_book;
    $this->checkBookNode($other_book, [$node], FALSE, FALSE, $node, []);
    $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, [$other_book]);

    // Test that we can save a book programmatically.
    $this->drupalLogin($this->bookAuthor);
    $book = $this->createBookNode('new');
    $book->save();

    // Confirm that an unpublished book page has the 'Add child page' link.
    $this->drupalGet('node/' . $nodes[4]->id());
    $this->assertSession()->linkExists('Add child page');
    $nodes[4]->setUnPublished();
    $nodes[4]->save();
    $this->drupalGet('node/' . $nodes[4]->id());
    $this->assertSession()->linkExists('Add child page');

    // Confirm that a child page has the "Add sibling page".
    $this->drupalGet('node/' . $nodes[4]->id());
    $this->assertSession()->linkExists('Add sibling page');
    $this->clickLink('Add sibling page');
    /* Get the relative URL of the current session.
    This contains the pid passed in by 'Add sibling page'.
    Check that against the pid in $nodes[4]. */
    $current_url = parse_url($this->getSession()->getCurrentUrl(), PHP_URL_QUERY);
    $sibling_pid = substr($current_url, strpos($current_url, "=") + 1);

    $this->assertEquals($nodes[4]->book['pid'], $sibling_pid);

    // Test preview bug.
    $this->drupalGet('node/' . $nodes[0]->id() . '/edit');
    $this->submitForm([], 'Preview');
    $this->assertSession()->statusCodeEquals(200);
  }

  /**
   * Tests book export ("printer-friendly version") functionality.
   *
   * @throws \Behat\Mink\Exception\ElementNotFoundException
   * @throws \Behat\Mink\Exception\ElementTextException
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Behat\Mink\Exception\ResponseTextException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function testBookExport(): void {
    // Create a book.
    $nodes = $this->createBook();

    // Unpublish Node 2.
    $nodes[2]->setUnpublished()->save();

    // Log in as web user and view printer-friendly version.
    $this->drupalLogin($this->webUser);
    $this->drupalGet('node/' . $this->book->id());
    $this->clickLink('Printer-friendly version');

    $this->assertSession()->elementTextContains('css', 'h1', $this->book->label());

    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
    $book_title = $node_storage->load($this->book->id())->label();

    // Make sure each part of the book is there.
    foreach ($nodes as $node) {
      // Verify unpublished node doesn't appear in export.
      if (!$node->isPublished()) {
        $this->assertSession()->pageTextNotContains('Book traversal links ' . $node->label());
        $this->assertSession()->responseNotContains($node->body->processed);
      }
      else {
        $this->assertSession()->pageTextContains($book_title);
        $this->assertSession()->pageTextContains($node->label());
        $this->assertSession()->responseContains($node->body->processed);
      }
    }

    // Enable module to make base fields' displays configurable and test again.
    $this->container->get('module_installer')->install(['book_display_configurable_test']);
    $this->drupalGet('book/export/html/' . $this->book->id());
    $this->assertSession()->elementTextContains('css', 'span', $this->book->label());

    // Make sure we can't export an unsupported format.
    $this->drupalGet('book/export/foobar/' . $this->book->id());
    $this->assertSession()->statusCodeEquals(404);

    // Make sure we get a 404 on a non-existent book node.
    $this->drupalGet('book/export/html/123');
    $this->assertSession()->statusCodeEquals(404);

    // Make sure we get 404 on nodes not in any book.
    $node = $this->drupalCreateNode([
      'type' => 'article',
      'title' => 'Article-not-in-book',
    ]);
    $this->drupalGet('book/export/html/' . $node->id());
    $this->assertSession()->statusCodeEquals(404);
    $this->assertSession()->pageTextContains('Article-not-in-book is not in a book and cannot be exported');
    $node = $this->drupalCreateNode([
      'type' => 'book',
      'title' => 'Book-not-in-book',
    ]);
    $this->drupalGet('book/export/html/' . $node->id());
    $this->assertSession()->statusCodeEquals(404);
    $this->assertSession()->pageTextContains('Book-not-in-book is not in a book and cannot be exported');

    // Make sure an anonymous user cannot view printer-friendly version.
    $this->drupalLogout();

    // Load the book and verify there is no printer-friendly version link.
    $this->drupalGet('node/' . $this->book->id());
    $this->assertSession()->linkNotExists('Printer-friendly version', 'Anonymous user is not shown link to printer-friendly version.');

    // Try getting the URL directly, and verify it fails.
    $this->drupalGet('book/export/html/' . $this->book->id());
    $this->assertSession()->statusCodeEquals(403);

    // Now grant anonymous users permission to view the printer-friendly
    // version and verify that node access restrictions still prevent them from
    // seeing it.
    user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['access printer-friendly version']);
    $this->drupalGet('book/export/html/' . $this->book->id());
    $this->assertSession()->statusCodeEquals(403);
  }

  /**
   * Tests BookManager::getTableOfContents().
   *
   * @throws \Behat\Mink\Exception\ElementNotFoundException
   * @throws \Drupal\Core\Entity\EntityMalformedException
   */
  public function testGetTableOfContents(): void {
    // Create new book.
    $nodes = $this->createBook();
    $book = $this->book;

    $this->drupalLogin($this->bookAuthor);

    /*
     * Add Node 5 under Node 2.
     * Add Node 6, 7, 8, 9, 10, 11 under Node 3.
     * Book
     *  |- Node 0
     *   |- Node 1
     *   |- Node 2
     *    |- Node 5
     *  |- Node 3
     *   |- Node 6
     *    |- Node 7
     *     |- Node 8
     *      |- Node 9
     *       |- Node 10
     *        |- Node 11
     *  |- Node 4
     */
    foreach ([5 => 2, 6 => 3, 7 => 6, 8 => 7, 9 => 8, 10 => 9, 11 => 10] as $child => $parent) {
      $nodes[$child] = $this->createBookNode($book->id(), $nodes[$parent]->id());
    }
    $this->drupalGet($nodes[0]->toUrl('edit-form'));
    // Since Node 0 has children 2 levels deep, nodes 10 and 11 should not
    // appear in the selector.
    $this->assertSession()->optionNotExists('edit-book-pid', $nodes[10]->id());
    $this->assertSession()->optionNotExists('edit-book-pid', $nodes[11]->id());
    // Node 9 should be available as an option.
    $this->assertSession()->optionExists('edit-book-pid', $nodes[9]->id());

    // Get a shallow set of options.
    /** @var \Drupal\book\BookManagerInterface $manager */
    $manager = $this->container->get('book.manager');
    $options = $manager->getTableOfContents($book->id(), 3);
    // Verify that all expected option keys are present.
    $expected_nids = [
      $book->id(),
      $nodes[0]->id(),
      $nodes[1]->id(),
      $nodes[2]->id(),
      $nodes[3]->id(),
      $nodes[6]->id(),
      $nodes[4]->id(),
    ];
    $this->assertEquals($expected_nids, array_keys($options));
    // Exclude Node 3.
    $options = $manager->getTableOfContents($book->id(), 3, [$nodes[3]->id()]);
    // Verify that expected option keys are present after excluding Node 3.
    $expected_nids = [$book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[4]->id()];
    $this->assertEquals($expected_nids, array_keys($options));
  }

  /**
   * Tests the access for deleting top-level book nodes.
   *
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\Core\Entity\EntityMalformedException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function testBookDelete() {
    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
    $nodes = $this->createBook();
    $this->drupalLogin($this->adminUser);
    $edit = [];

    // Ensure that the top-level book node cannot be deleted.
    $this->drupalGet('node/' . $this->book->id() . '/outline/remove');
    $this->assertSession()->statusCodeEquals(403);

    // Ensure that a child book node can be deleted.
    $this->drupalGet('node/' . $nodes[4]->id() . '/outline/remove');
    $this->submitForm($edit, 'Remove');
    $node_storage->resetCache([$nodes[4]->id()]);
    $node4 = $node_storage->load($nodes[4]->id());
    $this->assertEmpty($node4->book, 'Deleting child book node properly allowed.');

    // $nodes[4] is stale, trying to delete it directly will cause an error.
    $node4->delete();
    unset($nodes[4]);

    // Delete all child book nodes and retest top-level node deletion.
    $node_storage->delete($nodes);

    $this->drupalGet('node/' . $this->book->id() . '/outline/remove');
    $this->submitForm($edit, 'Remove');
    $node_storage->resetCache([$this->book->id()]);
    $node = $node_storage->load($this->book->id());
    $this->assertEmpty($node->book, 'Deleting childless top-level book node properly allowed.');

    // Tests directly deleting a book parent.
    $nodes = $this->createBook();
    $this->drupalLogin($this->adminUser);
    $this->drupalGet($this->book->toUrl('delete-form'));
    $this->assertSession()->pageTextContains($this->book->label() . ' is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.');
    // Delete parent, and visit a child page.
    $this->drupalGet($this->book->toUrl('delete-form'));
    $this->submitForm([], 'Delete');
    $this->drupalGet($nodes[0]->toUrl());
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->pageTextContains($nodes[0]->label());
    // The book parents should be updated.
    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
    $node_storage->resetCache();
    $child = $node_storage->load($nodes[0]->id());
    $this->assertEquals($child->id(), $child->book['bid'], 'Child node book ID updated when parent is deleted.');
    // 3rd-level children should now be 2nd-level.
    $second = $node_storage->load($nodes[1]->id());
    $this->assertEquals($child->id(), $second->book['bid'], '3rd-level child node is now second level when top-level node is deleted.');

    // Set the child page book id to deleted book id, and visit the child page.
    $node = $node_storage->load($nodes[0]->id());
    $node->book['bid'] = $nodes[0]->book['bid'];
    $node->save();
    $this->drupalGet($nodes[0]->toUrl());
    $this->assertSession()->pageTextContains($nodes[0]->label());
  }

  /**
   * Tests outline of a book.
   *
   * @throws \Behat\Mink\Exception\ElementNotFoundException
   * @throws \Behat\Mink\Exception\ResponseTextException
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function testBookOutline(): void {
    $this->drupalLogin($this->bookAuthor);

    // Create new node not yet a book.
    $empty_book = $this->drupalCreateNode(['type' => 'book']);
    $this->drupalGet('node/' . $empty_book->id() . '/outline');
    $this->assertSession()->linkNotExists('Book outline', 'Book Author is not allowed to outline');

    $this->drupalLogin($this->adminUser);
    $this->drupalGet('node/' . $empty_book->id() . '/outline');
    $this->assertSession()->pageTextContains('Book outline');
    // Verify that the node does not belong to a book.
    $this->assertTrue($this->assertSession()->optionExists('edit-book-bid', 0)->isSelected());
    $this->assertSession()->linkNotExists('Remove from book outline');

    $edit = [];
    $edit['book[bid]'] = '1';
    $this->drupalGet('node/' . $empty_book->id() . '/outline');
    $this->submitForm($edit, 'Add to book outline');
    $node = $this->container->get('entity_type.manager')->getStorage('node')->load($empty_book->id());
    // Test the book array.
    $this->assertEquals($empty_book->id(), $node->book['nid']);
    $this->assertEquals($empty_book->id(), $node->book['bid']);
    $this->assertEquals(1, $node->book['depth']);
    $this->assertEquals($empty_book->id(), $node->book['p1']);
    $this->assertEquals('0', $node->book['pid']);

    // Create new book.
    $this->drupalLogin($this->bookAuthor);
    $book = $this->createBookNode('new');

    $this->drupalLogin($this->adminUser);
    $this->drupalGet('node/' . $book->id() . '/outline');
    $this->assertSession()->pageTextContains('Book outline');
    $this->clickLink('Remove from book outline');
    $this->assertSession()->pageTextContains('Are you sure you want to remove ' . $book->label() . ' from the book hierarchy?');

    // Create a new node and set the book after the node was created.
    $node = $this->drupalCreateNode(['type' => 'book']);
    $this->addNodeToBook(intval($node->id()), intval($node->id()));
    $node = $this->container->get('entity_type.manager')->getStorage('node')->load($node->id());

    // Test the book array.
    $this->assertEquals($node->id(), $node->book['nid']);
    $this->assertEquals($node->id(), $node->book['bid']);
    $this->assertEquals(1, $node->book['depth']);
    $this->assertEquals($node->id(), $node->book['p1']);
    $this->assertEquals('0', $node->book['pid']);

    // Test the form itself.
    $this->drupalGet('node/' . $node->id() . '/edit');
    $this->assertTrue($this->assertSession()
      ->optionExists('edit-book-bid', $node->id())
      ->isSelected());

    // Create a new node that is not of book type.
    $this->drupalLogin($this->adminUser);
    $this->drupalCreateContentType(['type' => 'page']);
    $non_book_node = $this->drupalCreateNode(['type' => 'page']);

    // Create a non-book node and place in an outline.
    $non_book_node_in_outline = $this->drupalCreateNode([
      'type' => 'page',
      'book' => [
        'bid' => 'new',
      ],
    ]);

    // Admin user has edit book field on all nodes.
    $this->drupalGet('node/' . $non_book_node->id() . '/edit');
    $this->assertSession()->fieldExists('edit-book-bid');
    // Admin user has access to outline path on all nodes.
    $this->assertSession()->linkByHrefExists('node/' . $non_book_node->id() . '/outline');

    // Book author user only has edit book field on allowed book type nodes.
    $this->drupalLogin($this->bookAuthor);
    $this->drupalGet('node/' . $non_book_node->id() . '/edit');
    $this->assertSession()->fieldNotExists('edit-book-bid');
    // Book author user only has outline access on allowed book type nodes.
    $this->assertSession()->linkByHrefNotExists('node/' . $non_book_node->id() . '/outline');

    // Update bookAuthor permissions to edit page content type.
    $this->bookAuthor = $this->drupalCreateUser([
      'create new books',
      'create book content',
      'edit own book content',
      'add content to books',
      'add any content to books',
      'node test view',
      'edit any page content',
    ]);
    $this->drupalLogin($this->bookAuthor);

    // Book author user has edit book field on non-book nodes if node is in
    // an outline already.
    $this->drupalGet('node/' . $non_book_node_in_outline->id() . '/edit');
    $this->assertSession()->pageTextContains('This is the top-level page in this book');
    // Book author user has access to outline path if a node is already in
    // an outline already.
    $this->assertSession()->linkByHrefExists('node/' . $non_book_node_in_outline->id() . '/outline');
  }

  /**
   * Tests that saveBookLink() returns something.
   */
  public function testSaveBookLink(): void {
    $book_manager = $this->container->get('book.manager');
    // Mock a link for a new book.
    $link = [
      'nid' => 1,
      'has_children' => 0,
      'original_bid' => 0,
      'pid' => 0,
      'weight' => 0,
      'bid' => 0,
    ];

    // Save the link.
    $return = $book_manager->saveBookLink($link, TRUE);

    // Add the link defaults to $link, so we have something to compare to
    // the return from saveBookLink().
    $link = $book_manager->getLinkDefaults($link['nid']);

    // Test the return from saveBookLink.
    $this->assertEquals($return, $link);
  }

  /**
   * Tests the listing of all books.
   *
   * @throws \Behat\Mink\Exception\ResponseTextException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ExpectationException
   */
  public function testBookListing(): void {
    // Uninstall 'node_access_test' as this interferes with the test.
    $this->container->get('module_installer')->uninstall(['node_access_test']);
    // Create a new book.
    $nodes = $this->createBook();

    // Load the book page and assert the created book title is displayed.
    $this->drupalGet('book');
    $this->assertSession()->pageTextContains($this->book->label());

    // Assert helper links aren't available for anonymous users.
    $this->drupalGet('node/' . $nodes[1]->id());
    $this->assertSession()->linkNotExists('Add child page');
    $this->assertSession()->linkNotExists('Add sibling page');

    // Unpublish the top book page and confirm that the created book title is
    // not displayed for anonymous.
    $this->book->setUnpublished();
    $this->book->save();

    $this->drupalGet('book');
    $this->assertSession()->pageTextNotContains($this->book->label());

    // Publish the top book page and unpublish a page in the book and confirm
    // that the created book title is displayed for anonymous.
    $this->book->setPublished();
    $this->book->save();
    $nodes[0]->setUnpublished();
    $nodes[0]->save();

    $this->drupalGet('book');
    $this->assertSession()->pageTextContains($this->book->label());

    // Unpublish the top book page and confirm that the created book title is
    // displayed for user which has 'view own unpublished content' permission.
    $this->drupalLogin($this->bookAuthor);
    $this->book->setUnpublished();
    $this->book->save();

    $this->drupalGet('book');
    $this->assertSession()->pageTextContains($this->book->label());

    // Ensure the user doesn't see the book if they don't own it.
    $this->book->setOwner($this->webUser)->save();
    $this->drupalGet('book');
    $this->assertSession()->pageTextNotContains($this->book->label());

    // Confirm that the created book title is displayed for user which has
    // 'view any unpublished content' permission.
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('book');
    $this->assertSession()->pageTextContains($this->book->label());
  }

  /**
   * Tests the administrative listing of all books.
   *
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testAdminBookListing(): void {
    // Create a new book.
    $this->createBook();

    // Load the book page and assert the created book title is displayed.
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/structure/book');
    $this->assertSession()->pageTextContains($this->book->label());
  }

  /**
   * Tests the administrative listing of all book pages in a book.
   *
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function testAdminBookNodeListing(): void {
    // Create a new book.
    $nodes = $this->createBook();
    $this->drupalLogin($this->adminUser);

    // Load the book page list and assert the created book title is displayed
    // and action links are shown on list items.
    $this->drupalGet('admin/structure/book/' . $this->book->id());
    $this->assertSession()->pageTextContains($this->book->label());

    // Test that the view link is found from the list.
    $this->assertSession()->elementTextEquals('xpath', '//table//ul[@class="dropbutton"]/li/a', 'View');

    // Test that all the book pages are displayed on the book outline page.
    $this->assertSession()->elementsCount('xpath', '//table//ul[@class="dropbutton"]/li/a', count($nodes));

    // Unpublish a book in the hierarchy.
    $nodes[0]->setUnPublished();
    $nodes[0]->save();

    // Node should still appear on the outline for admins.
    $this->drupalGet('admin/structure/book/' . $this->book->id());
    $this->assertSession()->elementsCount('xpath', '//table//ul[@class="dropbutton"]/li/a', count($nodes));

    // Saving a book page not as the current version shouldn't affect the book.
    $old_title = $nodes[1]->getTitle();
    $new_title = $this->getRandomGenerator()->name();
    $nodes[1]->isDefaultRevision(FALSE);
    $nodes[1]->setNewRevision();
    $nodes[1]->setTitle($new_title);
    $nodes[1]->save();
    $this->drupalGet('admin/structure/book/' . $this->book->id());
    $this->assertSession()->elementsCount('xpath', '//table//ul[@class="dropbutton"]/li/a', count($nodes));
    $this->assertSession()->responseNotContains($new_title);
    $this->assertSession()->responseContains($old_title);
  }

  /**
   * Ensure the loaded book in hook_node_load() does not depend on the user.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function testHookNodeLoadAccess(): void {
    $this->container->get('module_installer')->install(['node_access_test']);

    // Ensure that the loaded book in hook_node_load() does NOT depend on the
    // current user.
    $this->drupalLogin($this->bookAuthor);
    $this->book = $this->createBookNode('new');
    // Reset any internal static caching.
    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
    $node_storage->resetCache();

    // Log in as user without access to the book node, so no 'node test view'
    // permission.
    // @see node_access_test_node_grants().
    $this->drupalLogin($this->webUserWithoutNodeAccess);
    $book_node = $node_storage->load($this->book->id());
    $this->assertNotEmpty($book_node->book);
    $this->assertEquals($this->book->id(), $book_node->book['bid']);

    // Reset the internal cache to retrigger the hook_node_load() call.
    $node_storage->resetCache();

    $this->drupalLogin($this->webUser);
    $book_node = $node_storage->load($this->book->id());
    $this->assertNotEmpty($book_node->book);
    $this->assertEquals($this->book->id(), $book_node->book['bid']);
  }

  /**
   * Tests the ordering of books in all the listings.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testBookOrder(): void {
    $this->drupalLogin($this->adminUser);

    // Create three books.
    $book1 = $this->createBookNode('new');
    $book1->setTitle('AAA Book');
    $book1->save();

    $book2 = $this->createBookNode('new');
    $book2->setTitle('BBB Book');
    $book2->save();

    $book3 = $this->createBookNode('new');
    $book3->setTitle('CCC Book');
    $book3->save();

    // Set weight for books.
    $edit_url = 'node/' . $book1->id() . '/outline';
    $edit = ['book[weight]' => 1];
    $this->drupalGet($edit_url);
    $this->submitForm($edit, 'Update book outline');
    $this->assertSession()->pageTextContains('The book outline has been updated');

    $edit_url = 'node/' . $book3->id() . '/outline';
    $edit = ['book[weight]' => -1];
    $this->drupalGet($edit_url);
    $this->submitForm($edit, 'Update book outline');
    $this->assertSession()->pageTextContains('The book outline has been updated');

    // Place a book navigation block.
    $this->drupalPlaceBlock('book_navigation');

    // Test books order by weight.
    $expected_order = [
      $book3->getTitle(),
      $book2->getTitle(),
      $book1->getTitle(),
    ];
    $this->assertBookOrder($expected_order);

    // Set the books sorting by title.
    $this->config('book.settings')
      ->set('book_sort', 'title')
      ->save();

    // Test books order by title.
    $expected_order = [
      $book1->getTitle(),
      $book2->getTitle(),
      $book3->getTitle(),
    ];
    $this->assertBookOrder($expected_order);
  }

  /**
   * Asserts the ordering of books.
   *
   * @param array $expected_order
   *   Expected book order.
   */
  protected function assertBookOrder(array $expected_order): void {
    // URLs to test the ordering of books.
    $urls = [
      'Navigation block on front page' => '<front>',
      'Admin overview' => 'admin/structure/book',
      'Node add/edit' => 'node/add/book',
    ];

    foreach ($urls as $url) {
      $this->drupalGet($url);
      $content = $this->getSession()->getPage()->getContent();

      $actual_order = [];
      $offset = 0;
      foreach ($expected_order as $substring) {
        if (($pos = strpos($content, $substring, $offset)) !== FALSE) {
          $actual_order[] = $substring;
          $offset = $pos + strlen($substring);
        }
      }

      $this->assertSame($expected_order, $actual_order, "Books are incorrectly ordered on URL '$url'.");
    }
  }

  /**
   * Tests that the book settings form can be saved without error.
   */
  public function testSettingsForm(): void {
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/structure/book/settings');
    $this->submitForm([], 'Save configuration');
  }

  /**
   * Tests saving the book outline with empty title.
   *
   * @throws \Behat\Mink\Exception\ResponseTextException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function testEmptyBookTitle(): void {
    $book = Node::create([
      'type' => 'book',
      'title' => 'Book',
      'book' => ['bid' => 'new'],
    ]);
    $book->save();
    $page1 = Node::create([
      'type' => 'book',
      'title' => '1st page',
      'book' => ['bid' => $book->id(), 'pid' => $book->id(), 'weight' => 0],
    ]);
    $page1->save();
    $page2 = Node::create([
      'type' => 'book',
      'title' => '2nd page',
      'book' => ['bid' => $book->id(), 'pid' => $book->id(), 'weight' => 1],
    ]);
    $page2->save();

    // Head to admin screen and attempt to re-order.
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('admin/structure/book/' . $book->id());

    $edit = [
      "table[book-admin-{$page1->id()}][title]" => '',
    ];
    $this->submitForm($edit, 'Save book pages');
    $this->assertSession()->pageTextContains('Title field is required.');

    $title = $this->randomString();
    $edit = [
      "table[book-admin-{$page1->id()}][title]" => $title,
    ];
    $this->submitForm($edit, 'Save book pages');
    $this->assertSession()->pageTextContains($title);

    $node = $this->container->get('entity_type.manager')
      ->getStorage('node')
      ->loadByProperties(['title' => $title]);
    $this->assertNotEmpty($node);
    $node = reset($node);
    $this->assertEquals($node->getTitle(), $title);
  }

  /**
   * Testing updated hierarchy after we remove one node from outline.
   *
   * @throws \Drupal\Core\Entity\EntityMalformedException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function testBookParentDelete(): void {
    $nodes = $this->createBook();
    $this->drupalLogin($this->adminUser);

    /*
     * Add Node 5 under Node 1.
     * Book
     *  |- Node 0
     *    |- Node 1
     *      | - Node 5
     *    |- Node 2
     *  |- Node 3
     *  |- Node 4
     */
    $book = $this->book;
    $child = 5;
    $nodes[$child] = $this->createBookNode($book->id(), $nodes[1]->id());

    // Remove Node 1 from outline which should move Node 5 up 1 level.
    $this->drupalGet($nodes[1]->toUrl()->toString() . '/outline/remove');
    $this->submitForm([], 'Remove');
    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
    $node_storage->resetCache();

    $this->drupalLogout();
    $this->drupalLogin($this->webUser);

    $this->checkBookNode($nodes[0], [$nodes[2], $nodes[5]], FALSE, $book, FALSE, [$book]);
  }

  /**
   * Tests the child ordering feature.
   *
   * @throws \Behat\Mink\Exception\ResponseTextException
   * @throws \Behat\Mink\Exception\ExpectationException
   */
  public function testChildOrdering(): void {
    // Create new book.
    $nodes = $this->createBook();

    $this->drupalLogin($this->adminUser);

    // Third node has no children, therefore no child order link.
    $this->drupalGet('node/' . $nodes[3]->id());
    $this->assertSession()->pageTextNotContains('Child Order');

    // First node in the book has 2 children.
    $this->drupalGet('node/' . $nodes[0]->id());
    $this->assertSession()->pageTextContains('Child order');
    $this->clickLink('Child order');

    // Verify children.
    $this->assertSession()->statusCodeEquals(200);
    $child1 = $nodes[1];
    $child2 = $nodes[2];
    $this->assertSession()->pageTextContains($child1->getTitle());
    $this->assertSession()->pageTextContains($child2->getTitle());

    // Verify weight changes save.
    $edit = [
      'table[book-admin-' . $child1->id() . '][weight]' => 0,
      'table[book-admin-' . $child2->id() . '][weight]' => 1,
    ];
    $this->submitForm($edit, 'Save book pages');
    $this->assertSession()->fieldValueEquals('table[book-admin-' . $child1->id() . '][weight]', 0);
    $this->assertSession()->fieldValueEquals('table[book-admin-' . $child2->id() . '][weight]', 1);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc