BookTest.php 29.5 KB
Newer Older
1 2
<?php

3 4
namespace Drupal\book\Tests;

5
use Drupal\Component\Utility\SafeMarkup;
6
use Drupal\Core\Cache\Cache;
7
use Drupal\Core\Entity\EntityInterface;
8
use Drupal\simpletest\WebTestBase;
9
use Drupal\user\RoleInterface;
10

11
/**
12 13 14
 * Create a book, add pages, and test book interface.
 *
 * @group book
15
 */
16
class BookTest extends WebTestBase {
17 18

  /**
19
   * Modules to install.
20 21 22
   *
   * @var array
   */
23
  public static $modules = array('book', 'block', 'node_access_test', 'book_test');
24

25 26 27
  /**
   * A book node.
   *
28
   * @var \Drupal\node\NodeInterface
29
   */
30
  protected $book;
31 32 33 34 35 36

  /**
   * A user with permission to create and edit books.
   *
   * @var object
   */
37
  protected $bookAuthor;
38 39 40 41 42 43

  /**
   * A user with permission to view a book and access printer-friendly version.
   *
   * @var object
   */
44
  protected $webUser;
45 46 47 48 49 50

  /**
   * A user with permission to create and edit books and to administer blocks.
   *
   * @var object
   */
51
  protected $adminUser;
52

53 54 55 56 57 58 59
  /**
   * A user without the 'node test view' permission.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $webUserWithoutNodeAccess;

60 61 62
  /**
   * {@inheritdoc}
   */
63
  protected function setUp() {
64
    parent::setUp();
65
    $this->drupalPlaceBlock('system_breadcrumb_block');
66
    $this->drupalPlaceBlock('page_title_block');
67 68 69

    // node_access_test requires a node_access_rebuild().
    node_access_rebuild();
70

71
    // Create users.
72 73
    $this->bookAuthor = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
    $this->webUser = $this->drupalCreateUser(array('access printer-friendly version', 'node test view'));
74
    $this->webUserWithoutNodeAccess = $this->drupalCreateUser(array('access printer-friendly version'));
75
    $this->adminUser = $this->drupalCreateUser(array('create new books', 'create book content', 'edit any book content', 'delete any book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'node test view', 'administer content types', 'administer site configuration'));
76
  }
77

78
  /**
79
   * Creates a new book with a page hierarchy.
80 81
   *
   * @return \Drupal\node\NodeInterface[]
82
   */
83
  function createBook() {
84
    // Create new book.
85
    $this->drupalLogin($this->bookAuthor);
86 87 88 89 90

    $this->book = $this->createBookNode('new');
    $book = $this->book;

    /*
91
     * Add page hierarchy to book.
92 93 94 95 96 97 98 99
     * Book
     *  |- Node 0
     *   |- Node 1
     *   |- Node 2
     *  |- Node 3
     *  |- Node 4
     */
    $nodes = array();
100
    $nodes[] = $this->createBookNode($book->id()); // Node 0.
101 102
    $nodes[] = $this->createBookNode($book->id(), $nodes[0]->book['nid']); // Node 1.
    $nodes[] = $this->createBookNode($book->id(), $nodes[0]->book['nid']); // Node 2.
103 104
    $nodes[] = $this->createBookNode($book->id()); // Node 3.
    $nodes[] = $this->createBookNode($book->id()); // Node 4.
105 106

    $this->drupalLogout();
107

108 109 110
    return $nodes;
  }

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
  /**
   * Tests the book navigation cache context.
   *
   * @see \Drupal\book\Cache\BookNavigationCacheContext
   */
  public function testBookNavigationCacheContext() {
    // 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.
    \Drupal::state()->set('book_test.debug_book_navigation_cache_context', TRUE);
126
    Cache::invalidateTags(['book_test.debug_book_navigation_cache_context']);
127 128 129 130

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

    // On non-node route.
131
    $this->drupalGet($this->adminUser->urlInfo());
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
    $this->assertRaw('[route.book_navigation]=book.none');

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

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

151 152 153 154 155
  /**
   * Tests saving the book outline on an empty book.
   */
  function testEmptyBook() {
    // Create a new empty book.
156
    $this->drupalLogin($this->bookAuthor);
157 158 159 160
    $book = $this->createBookNode('new');
    $this->drupalLogout();

    // Log in as a user with access to the book outline and save the form.
161
    $this->drupalLogin($this->adminUser);
162 163 164 165
    $this->drupalPostForm('admin/structure/book/' . $book->id(), array(), t('Save book pages'));
    $this->assertText(t('Updated book @book.', array('@book' => $book->label())));
  }

166
  /**
167
   * Tests book functionality through node interfaces.
168 169 170 171 172
   */
  function testBook() {
    // Create new book.
    $nodes = $this->createBook();
    $book = $this->book;
173

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

176 177 178 179 180 181 182 183
    // Check that book pages display along with the correct outlines and
    // previous/next links.
    $this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0], array());
    $this->checkBookNode($nodes[0], array($nodes[1], $nodes[2]), $book, $book, $nodes[1], array($book));
    $this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], array($book, $nodes[0]));
    $this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], array($book, $nodes[0]));
    $this->checkBookNode($nodes[3], NULL, $nodes[2], $book, $nodes[4], array($book));
    $this->checkBookNode($nodes[4], NULL, $nodes[3], $book, FALSE, array($book));
184 185

    $this->drupalLogout();
186
    $this->drupalLogin($this->bookAuthor);
187 188 189 190 191

    // Check the presence of expected cache tags.
    $this->drupalGet('node/add/book');
    $this->assertCacheTag('config:book.settings');

192 193 194 195 196 197 198 199 200 201 202 203
    /*
     * Add Node 5 under Node 3.
     * Book
     *  |- Node 0
     *   |- Node 1
     *   |- Node 2
     *  |- Node 3
     *   |- Node 5
     *  |- Node 4
     */
    $nodes[] = $this->createBookNode($book->id(), $nodes[3]->book['nid']); // Node 5.
    $this->drupalLogout();
204
    $this->drupalLogin($this->webUser);
205 206 207 208
    // Verify the new outline - make sure we don't get stale cached data.
    $this->checkBookNode($nodes[3], array($nodes[5]), $nodes[2], $book, $nodes[5], array($book));
    $this->checkBookNode($nodes[4], NULL, $nodes[5], $book, FALSE, array($book));
    $this->drupalLogout();
209
    // Create a second book, and move an existing book page into it.
210
    $this->drupalLogin($this->bookAuthor);
211
    $other_book = $this->createBookNode('new');
212 213
    $node = $this->createBookNode($book->id());
    $edit = array('book[bid]' => $other_book->id());
214
    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
215 216

    $this->drupalLogout();
217
    $this->drupalLogin($this->webUser);
218 219 220 221 222

    // 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;
223 224
    $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node, array());
    $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book));
225 226

    // Test that we can save a book programatically.
227
    $this->drupalLogin($this->bookAuthor);
228 229
    $book = $this->createBookNode('new');
    $book->save();
230 231 232
  }

  /**
233 234 235
   * Checks the outline of sub-pages; previous, up, and next.
   *
   * Also checks the printer friendly version of the outline.
236
   *
237
   * @param \Drupal\Core\Entity\EntityInterface $node
238
   *   Node to check.
239
   * @param $nodes
240
   *   Nodes that should be in outline.
241
   * @param $previous
242
   *   (optional) Previous link node. Defaults to FALSE.
243
   * @param $up
244
   *   (optional) Up link node. Defaults to FALSE.
245
   * @param $next
246 247
   *   (optional) Next link node. Defaults to FALSE.
   * @param array $breadcrumb
248
   *   The nodes that should be displayed in the breadcrumb.
249
   */
250
  function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) {
251 252
    // $number does not use drupal_static as it should not be reset
    // since it uniquely identifies each call to checkBookNode().
253
    static $number = 0;
254
    $this->drupalGet('node/' . $node->id());
255 256 257

    // Check outline structure.
    if ($nodes !== NULL) {
258
      $this->assertPattern($this->generateOutlinePattern($nodes), format_string('Node @number outline confirmed.', array('@number' => $number)));
259 260
    }
    else {
261
      $this->pass(format_string('Node %number does not have outline.', array('%number' => $number)));
262 263 264 265
    }

    // Check previous, up, and next links.
    if ($previous) {
266 267
      /** @var \Drupal\Core\Url $url */
      $url = $previous->urlInfo();
268
      $url->setOptions(array('attributes' => array('rel' => array('prev'), 'title' => t('Go to previous page'))));
269
      $text = SafeMarkup::format('<b>‹</b> @label', array('@label' => $previous->label()));
270
      $this->assertRaw(\Drupal::l($text, $url), 'Previous page link found.');
271
    }
272

273
    if ($up) {
274 275
      /** @var \Drupal\Core\Url $url */
      $url = $up->urlInfo();
276
      $url->setOptions(array('attributes' => array('title' => t('Go to parent page'))));
277
      $this->assertRaw(\Drupal::l('Up', $url), 'Up page link found.');
278
    }
279

280
    if ($next) {
281 282
      /** @var \Drupal\Core\Url $url */
      $url = $next->urlInfo();
283
      $url->setOptions(array('attributes' => array('rel' => array('next'), 'title' => t('Go to next page'))));
284
      $text = SafeMarkup::format('@label <b>›</b>', array('@label' => $next->label()));
285
      $this->assertRaw(\Drupal::l($text, $url), 'Next page link found.');
286 287
    }

288 289
    // Compute the expected breadcrumb.
    $expected_breadcrumb = array();
290
    $expected_breadcrumb[] = \Drupal::url('<front>');
291
    foreach ($breadcrumb as $a_node) {
292
      $expected_breadcrumb[] = $a_node->url();
293 294 295
    }

    // Fetch links in the current breadcrumb.
296
    $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a');
297 298 299 300 301 302
    $got_breadcrumb = array();
    foreach ($links as $link) {
      $got_breadcrumb[] = (string) $link['href'];
    }

    // Compare expected and got breadcrumbs.
303
    $this->assertIdentical($expected_breadcrumb, $got_breadcrumb, 'The breadcrumb is correctly displayed on the page.');
304

305
    // Check printer friendly version.
306
    $this->drupalGet('book/export/html/' . $node->id());
307
    $this->assertText($node->label(), 'Printer friendly title found.');
308
    $this->assertRaw($node->body->processed, 'Printer friendly body found.');
309 310 311 312 313

    $number++;
  }

  /**
314 315 316 317
   * Creates a regular expression to check for the sub-nodes in the outline.
   *
   * @param array $nodes
   *   An array of nodes to check in outline.
318
   *
319 320
   * @return string
   *   A regular expression that locates sub-nodes of the outline.
321 322 323 324
   */
  function generateOutlinePattern($nodes) {
    $outline = '';
    foreach ($nodes as $node) {
325
      $outline .= '(node\/' . $node->id() . ')(.*?)(' . $node->label() . ')(.*?)';
326 327
    }

328
    return '/<nav id="book-navigation-' . $this->book->id() . '"(.*?)<ul(.*?)' . $outline . '<\/ul>/s';
329 330 331
  }

  /**
332
   * Creates a book node.
333
   *
334 335 336 337
   * @param int|string $book_nid
   *   A book node ID or set to 'new' to create a new book.
   * @param int|null $parent
   *   (optional) Parent book reference ID. Defaults to NULL.
338 339 340
   *
   * @return \Drupal\node\NodeInterface
   *   The created node.
341 342
   */
  function createBookNode($book_nid, $parent = NULL) {
343 344
    // $number does not use drupal_static as it should not be reset
    // since it uniquely identifies each call to createBookNode().
345 346 347
    static $number = 0; // Used to ensure that when sorted nodes stay in same order.

    $edit = array();
348
    $edit['title[0][value]'] = str_pad($number, 2, '0', STR_PAD_LEFT) . ' - SimpleTest test node ' . $this->randomMachineName(10);
349
    $edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomMachineName(32) . ' ' . $this->randomMachineName(32);
350 351 352
    $edit['book[bid]'] = $book_nid;

    if ($parent !== NULL) {
353
      $this->drupalPostForm('node/add/book', $edit, t('Change book (update list of parents)'));
354

355
      $edit['book[pid]'] = $parent;
356
      $this->drupalPostForm(NULL, $edit, t('Save'));
357
      // Make sure the parent was flagged as having children.
358
      $parent_node = \Drupal::entityManager()->getStorage('node')->loadUnchanged($parent);
359
      $this->assertFalse(empty($parent_node->book['has_children']), 'Parent node is marked as having children');
360 361
    }
    else {
362
      $this->drupalPostForm('node/add/book', $edit, t('Save'));
363 364 365
    }

    // Check to make sure the book node was created.
366
    $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
367
    $this->assertNotNull(($node === FALSE ? NULL : $node), 'Book node found in database.');
368 369 370 371
    $number++;

    return $node;
  }
372

373 374 375 376 377 378
  /**
   * Tests book export ("printer-friendly version") functionality.
   */
  function testBookExport() {
    // Create a book.
    $nodes = $this->createBook();
379

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

385 386
    // Make sure each part of the book is there.
    foreach ($nodes as $node) {
387
      $this->assertText($node->label(), 'Node title found in printer friendly version.');
388
      $this->assertRaw($node->body->processed, 'Node body found in printer friendly version.');
389
    }
390

391
    // Make sure we can't export an unsupported format.
392
    $this->drupalGet('book/export/foobar/' . $this->book->id());
393
    $this->assertResponse('404', 'Unsupported export format returned "not found".');
394

395 396
    // Make sure we get a 404 on a not existing book node.
    $this->drupalGet('book/export/html/123');
397
    $this->assertResponse('404', 'Not existing book node returned "not found".');
398

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

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

406
    // Try getting the URL directly, and verify it fails.
407
    $this->drupalGet('book/export/html/' . $this->book->id());
408
    $this->assertResponse('403', 'Anonymous user properly forbidden.');
409 410 411 412

    // Now grant anonymous users permission to view the printer-friendly
    // version and verify that node access restrictions still prevent them from
    // seeing it.
413
    user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array('access printer-friendly version'));
414
    $this->drupalGet('book/export/html/' . $this->book->id());
415
    $this->assertResponse('403', 'Anonymous user properly forbidden from seeing the printer-friendly version when denied by node access.');
416
  }
417

418 419 420
  /**
   * Tests the functionality of the book navigation block.
   */
421
  function testBookNavigationBlock() {
422
    $this->drupalLogin($this->adminUser);
423

424
    // Enable the block.
425
    $block = $this->drupalPlaceBlock('book_navigation');
426

427
    // Give anonymous users the permission 'node test view'.
428
    $edit = array();
429 430
    $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE;
    $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions'));
431
    $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
432

433 434 435
    // Test correct display of the block.
    $nodes = $this->createBook();
    $this->drupalGet('<front>');
436
    $this->assertText($block->label(), 'Book navigation block is displayed.');
437 438
    $this->assertText($this->book->label(), format_string('Link to book root (@title) is displayed.', array('@title' => $nodes[0]->label())));
    $this->assertNoText($nodes[0]->label(), 'No links to individual book pages are displayed.');
439
  }
440

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
  /**
   * Tests BookManager::getTableOfContents().
   */
  public function testGetTableOfContents() {
    // 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'));
    // Snice Node 0 has children 2 levels deep, nodes 10 and 11 should not
    // appear in the selector.
    $this->assertNoOption('edit-book-pid', $nodes[10]->id());
    $this->assertNoOption('edit-book-pid', $nodes[11]->id());
    // Node 9 should be available as an option.
    $this->assertOption('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);
    $expected_nids = [$book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[3]->id(), $nodes[6]->id(), $nodes[4]->id()];
    $this->assertEqual(count($options), count($expected_nids));
    $diff = array_diff($expected_nids, array_keys($options));
    $this->assertTrue(empty($diff), 'Found all expected option keys');
    // Exclude Node 3.
    $options = $manager->getTableOfContents($book->id(), 3, array($nodes[3]->id()));
    $expected_nids = array($book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[4]->id());
    $this->assertEqual(count($options), count($expected_nids));
    $diff = array_diff($expected_nids, array_keys($options));
    $this->assertTrue(empty($diff), 'Found all expected option keys after excluding Node 3');
  }

495
  /**
496
   * Tests the book navigation block when an access module is installed.
497
   */
498
  function testNavigationBlockOnAccessModuleInstalled() {
499
    $this->drupalLogin($this->adminUser);
500
    $block = $this->drupalPlaceBlock('book_navigation', array('block_mode' => 'book pages'));
501 502 503

    // Give anonymous users the permission 'node test view'.
    $edit = array();
504 505
    $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE;
    $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions'));
506 507 508 509 510 511
    $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");

    // Create a book.
    $this->createBook();

    // Test correct display of the block to registered users.
512
    $this->drupalLogin($this->webUser);
513
    $this->drupalGet('node/' . $this->book->id());
514
    $this->assertText($block->label(), 'Book navigation block is displayed to registered users.');
515 516 517
    $this->drupalLogout();

    // Test correct display of the block to anonymous users.
518
    $this->drupalGet('node/' . $this->book->id());
519 520 521 522 523
    $this->assertText($block->label(), 'Book navigation block is displayed to anonymous users.');

    // Test the 'book pages' block_mode setting.
    $this->drupalGet('<front>');
    $this->assertNoText($block->label(), 'Book navigation block is not shown on non-book pages.');
524
  }
525 526 527 528

  /**
   * Tests the access for deleting top-level book nodes.
   */
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
  function testBookDelete() {
    $node_storage = $this->container->get('entity.manager')->getStorage('node');
    $nodes = $this->createBook();
    $this->drupalLogin($this->adminUser);
    $edit = array();

    // Test access to delete top-level and child book nodes.
    $this->drupalGet('node/' . $this->book->id() . '/outline/remove');
    $this->assertResponse('403', 'Deleting top-level book node properly forbidden.');
    $this->drupalPostForm('node/' . $nodes[4]->id() . '/outline/remove', $edit, t('Remove'));
    $node_storage->resetCache(array($nodes[4]->id()));
    $node4 = $node_storage->load($nodes[4]->id());
    $this->assertTrue(empty($node4->book), 'Deleting child book node properly allowed.');

    // Delete all child book nodes and retest top-level node deletion.
    foreach ($nodes as $node) {
      $nids[] = $node->id();
    }
    entity_delete_multiple('node', $nids);
    $this->drupalPostForm('node/' . $this->book->id() . '/outline/remove', $edit, t('Remove'));
    $node_storage->resetCache(array($this->book->id()));
    $node = $node_storage->load($this->book->id());
    $this->assertTrue(empty($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->urlInfo('delete-form'));
    $this->assertRaw(t('%title is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.', ['%title' => $this->book->label()]));
    // Delete parent, and visit a child page.
    $this->drupalPostForm($this->book->urlInfo('delete-form'), [], t('Delete'));
    $this->drupalGet($nodes[0]->urlInfo());
    $this->assertResponse(200);
    $this->assertText($nodes[0]->label());
    // The book parents should be updated.
    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
    $node_storage->resetCache();
    $child = $node_storage->load($nodes[0]->id());
    $this->assertEqual($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->assertEqual($child->id(), $second->book['bid'], '3rd-level child node is now second level when top-level node is deleted.');
  }
572

573 574 575 576 577
  /**
   * Tests re-ordering of books.
   */
  public function testBookOrdering() {
    // Create new book.
578
    $this->createBook();
579 580
    $book = $this->book;

581
    $this->drupalLogin($this->adminUser);
582 583
    $node1 = $this->createBookNode($book->id());
    $node2 = $this->createBookNode($book->id());
584
    $pid = $node1->book['nid'];
585 586

    // Head to admin screen and attempt to re-order.
587
    $this->drupalGet('admin/structure/book/' . $book->id());
588
    $edit = array(
589 590
      "table[book-admin-{$node1->id()}][weight]" => 1,
      "table[book-admin-{$node2->id()}][weight]" => 2,
591
      // Put node 2 under node 1.
592
      "table[book-admin-{$node2->id()}][pid]" => $pid,
593
    );
594
    $this->drupalPostForm(NULL, $edit, t('Save book pages'));
595
    // Verify weight was updated.
596 597
    $this->assertFieldByName("table[book-admin-{$node1->id()}][weight]", 1);
    $this->assertFieldByName("table[book-admin-{$node2->id()}][weight]", 2);
598
    $this->assertFieldByName("table[book-admin-{$node2->id()}][pid]", $pid);
599
  }
600 601 602 603 604

  /**
   * Tests outline of a book.
   */
  public function testBookOutline() {
605
    $this->drupalLogin($this->bookAuthor);
606 607 608 609 610 611

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

612
    $this->drupalLogin($this->adminUser);
613 614 615
    $this->drupalGet('node/' . $empty_book->id() . '/outline');
    $this->assertRaw(t('Book outline'));
    $this->assertOptionSelected('edit-book-bid', 0, 'Node does not belong to a book');
616
    $this->assertNoLink(t('Remove from book outline'));
617 618 619 620

    $edit = array();
    $edit['book[bid]'] = '1';
    $this->drupalPostForm('node/' . $empty_book->id() . '/outline', $edit, t('Add to book outline'));
621
    $node = \Drupal::entityManager()->getStorage('node')->load($empty_book->id());
622 623 624 625 626 627 628
    // Test the book array.
    $this->assertEqual($node->book['nid'], $empty_book->id());
    $this->assertEqual($node->book['bid'], $empty_book->id());
    $this->assertEqual($node->book['depth'], 1);
    $this->assertEqual($node->book['p1'], $empty_book->id());
    $this->assertEqual($node->book['pid'], '0');

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

633
    $this->drupalLogin($this->adminUser);
634 635
    $this->drupalGet('node/' . $book->id() . '/outline');
    $this->assertRaw(t('Book outline'));
636 637
    $this->clickLink(t('Remove from book outline'));
    $this->assertRaw(t('Are you sure you want to remove %title from the book hierarchy?', array('%title' => $book->label())));
638 639 640 641 642 643

    // Create a new node and set the book after the node was created.
    $node = $this->drupalCreateNode(array('type' => 'book'));
    $edit = array();
    $edit['book[bid]'] = $node->id();
    $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
644
    $node = \Drupal::entityManager()->getStorage('node')->load($node->id());
645 646 647 648 649 650 651 652 653 654 655

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

    // Test the form itself.
    $this->drupalGet('node/' . $node->id() . '/edit');
    $this->assertOptionSelected('edit-book-bid', $node->id());
656
  }
657

658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
  /**
   * Tests that saveBookLink() returns something.
   */
  public function testSaveBookLink() {
    $book_manager = \Drupal::service('book.manager');

    // Mock a link for a new book.
    $link = array('nid' => 1, 'has_children' => 0, 'original_bid' => 0, 'parent_depth_limit' => 8, 'pid' => 0, 'weight' => 0, 'bid' => 1);
    $new = TRUE;

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

    // 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->assertEqual($return, $link);
  }

678 679 680 681 682 683 684
  /**
   * Tests the listing of all books.
   */
  public function testBookListing() {
    // Create a new book.
    $this->createBook();

685
    // Must be a user with 'node test view' permission since node_access_test is installed.
686
    $this->drupalLogin($this->webUser);
687 688 689 690 691 692

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

    $this->assertText($this->book->label(), 'The book title is displayed on the book listing page.');
  }
693 694 695 696 697 698 699 700 701

  /**
   * Tests the administrative listing of all books.
   */
  public function testAdminBookListing() {
    // Create a new book.
    $this->createBook();

    // Load the book page and assert the created book title is displayed.
702
    $this->drupalLogin($this->adminUser);
703 704 705 706
    $this->drupalGet('admin/structure/book');
    $this->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');
  }

707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
  /**
   * Tests the administrative listing of all book pages in a book.
   */
  public function testAdminBookNodeListing() {
    // Create a new book.
    $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->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');

    $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a');
    $this->assertEqual((string) $elements[0], 'View', 'View link is found from the list.');
  }

724 725 726 727 728 729 730 731 732 733 734 735 736 737
  /**
   * Ensure the loaded book in hook_node_load() does not depend on the user.
   */
  public function testHookNodeLoadAccess() {
    \Drupal::service('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 = \Drupal::entityManager()->getStorage('node');
    $node_storage->resetCache();

738
    // Log in as user without access to the book node, so no 'node test view'
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754
    // permission.
    // @see node_access_test_node_grants().
    $this->drupalLogin($this->webUserWithoutNodeAccess);
    $book_node = $node_storage->load($this->book->id());
    $this->assertTrue(!empty($book_node->book));
    $this->assertEqual($book_node->book['bid'], $this->book->id());

    // 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->assertTrue(!empty($book_node->book));
    $this->assertEqual($book_node->book['bid'], $this->book->id());
  }

755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
  /**
   * Tests the book navigation block when book is unpublished.
   *
   * There was a fatal error with "Show block only on book pages" block mode.
   */
  public function testBookNavigationBlockOnUnpublishedBook() {
    // Create a new book.
    $this->createBook();

    // Create administrator user.
    $administratorUser = $this->drupalCreateUser(['administer blocks', 'administer nodes', 'bypass node access']);
    $this->drupalLogin($administratorUser);

    // Enable the block with "Show block only on book pages" mode.
    $this->drupalPlaceBlock('book_navigation', ['block_mode' => 'book pages']);

    // Unpublish book node.
    $edit = [];
    $this->drupalPostForm('node/' . $this->book->id() . '/edit', $edit, t('Save and unpublish'));

    // Test node page.
    $this->drupalGet('node/' . $this->book->id());
    $this->assertText($this->book->label(), 'Unpublished book with "Show block only on book pages" book navigation settings.');
  }

780
}