1<?php 2 3/** 4 * @file 5 * Tests for book.module. 6 */ 7 8/** 9 * Tests the functionality of the Book module. 10 */ 11class BookTestCase extends DrupalWebTestCase { 12 13 /** 14 * A book node. 15 * 16 * @var object 17 */ 18 protected $book; 19 20 /** 21 * A user with permission to create and edit books. 22 * 23 * @var object 24 */ 25 protected $book_author; 26 27 /** 28 * A user with permission to view a book and access printer-friendly version. 29 * 30 * @var object 31 */ 32 protected $web_user; 33 34 /** 35 * A user with permission to create and edit books and to administer blocks. 36 * 37 * @var object 38 */ 39 protected $admin_user; 40 41 public static function getInfo() { 42 return array( 43 'name' => 'Book functionality', 44 'description' => 'Create a book, add pages, and test book interface.', 45 'group' => 'Book', 46 ); 47 } 48 49 function setUp() { 50 parent::setUp(array('book', 'node_access_test')); 51 52 // node_access_test requires a node_access_rebuild(). 53 node_access_rebuild(); 54 55 // Create users. 56 $this->book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books')); 57 $this->web_user = $this->drupalCreateUser(array('access printer-friendly version', 'node test view')); 58 $this->admin_user = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'node test view')); 59 } 60 61 /** 62 * Creates a new book with a page hierarchy. 63 */ 64 function createBook() { 65 // Create new book. 66 $this->drupalLogin($this->book_author); 67 68 $this->book = $this->createBookNode('new'); 69 $book = $this->book; 70 71 /* 72 * Add page hierarchy to book. 73 * Book 74 * |- Node 0 75 * |- Node 1 76 * |- Node 2 77 * |- Node 3 78 * |- Node 4 79 */ 80 $nodes = array(); 81 $nodes[] = $this->createBookNode($book->nid); // Node 0. 82 $nodes[] = $this->createBookNode($book->nid, $nodes[0]->book['mlid']); // Node 1. 83 $nodes[] = $this->createBookNode($book->nid, $nodes[0]->book['mlid']); // Node 2. 84 $nodes[] = $this->createBookNode($book->nid); // Node 3. 85 $nodes[] = $this->createBookNode($book->nid); // Node 4. 86 87 $this->drupalLogout(); 88 89 return $nodes; 90 } 91 92 /** 93 * Tests book functionality through node interfaces. 94 */ 95 function testBook() { 96 // Create new book. 97 $nodes = $this->createBook(); 98 $book = $this->book; 99 100 $this->drupalLogin($this->web_user); 101 102 // Check that book pages display along with the correct outlines and 103 // previous/next links. 104 $this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0]); 105 $this->checkBookNode($nodes[0], array($nodes[1], $nodes[2]), $book, $book, $nodes[1], array($book)); 106 $this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], array($book, $nodes[0])); 107 $this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], array($book, $nodes[0])); 108 $this->checkBookNode($nodes[3], NULL, $nodes[2], $book, $nodes[4], array($book)); 109 $this->checkBookNode($nodes[4], NULL, $nodes[3], $book, FALSE, array($book)); 110 111 $this->drupalLogout(); 112 113 // Create a second book, and move an existing book page into it. 114 $this->drupalLogin($this->book_author); 115 $other_book = $this->createBookNode('new'); 116 $node = $this->createBookNode($book->nid); 117 $edit = array('book[bid]' => $other_book->nid); 118 $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); 119 120 $this->drupalLogout(); 121 $this->drupalLogin($this->web_user); 122 123 // Check that the nodes in the second book are displayed correctly. 124 // First we must set $this->book to the second book, so that the 125 // correct regex will be generated for testing the outline. 126 $this->book = $other_book; 127 $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node); 128 $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book)); 129 } 130 131 /** 132 * Checks the outline of sub-pages; previous, up, and next. 133 * 134 * Also checks the printer friendly version of the outline. 135 * 136 * @param $node 137 * Node to check. 138 * @param $nodes 139 * Nodes that should be in outline. 140 * @param $previous 141 * (optional) Previous link node. Defaults to FALSE. 142 * @param $up 143 * (optional) Up link node. Defaults to FALSE. 144 * @param $next 145 * (optional) Next link node. Defaults to FALSE. 146 * @param $breadcrumb 147 * (optional) The nodes that should be displayed in the breadcrumb. 148 */ 149 function checkBookNode($node, $nodes = NULL, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb = array()) { 150 // $number does not use drupal_static as it should not be reset 151 // since it uniquely identifies each call to checkBookNode(). 152 static $number = 0; 153 $this->drupalGet('node/' . $node->nid); 154 155 // Check outline structure. 156 if ($nodes !== NULL) { 157 $this->assertPattern($this->generateOutlinePattern($nodes), format_string('Node %number outline confirmed.', array('%number' => $number))); 158 } 159 else { 160 $this->pass(format_string('Node %number does not have outline.', array('%number' => $number))); 161 } 162 163 // Check previous, up, and next links. 164 if ($previous) { 165 $this->assertRaw(l('‹ ' . $previous->title, 'node/' . $previous->nid, array('attributes' => array('class' => array('page-previous'), 'title' => t('Go to previous page')))), 'Previous page link found.'); 166 } 167 168 if ($up) { 169 $this->assertRaw(l('up', 'node/' . $up->nid, array('attributes' => array('class' => array('page-up'), 'title' => t('Go to parent page')))), 'Up page link found.'); 170 } 171 172 if ($next) { 173 $this->assertRaw(l($next->title . ' ›', 'node/' . $next->nid, array('attributes' => array('class' => array('page-next'), 'title' => t('Go to next page')))), 'Next page link found.'); 174 } 175 176 // Compute the expected breadcrumb. 177 $expected_breadcrumb = array(); 178 $expected_breadcrumb[] = url(''); 179 foreach ($breadcrumb as $a_node) { 180 $expected_breadcrumb[] = url('node/' . $a_node->nid); 181 } 182 183 // Fetch links in the current breadcrumb. 184 $links = $this->xpath('//div[@class="breadcrumb"]/a'); 185 $got_breadcrumb = array(); 186 foreach ($links as $link) { 187 $got_breadcrumb[] = (string) $link['href']; 188 } 189 190 // Compare expected and got breadcrumbs. 191 $this->assertIdentical($expected_breadcrumb, $got_breadcrumb, 'The breadcrumb is correctly displayed on the page.'); 192 193 // Check printer friendly version. 194 $this->drupalGet('book/export/html/' . $node->nid); 195 $this->assertText($node->title, 'Printer friendly title found.'); 196 $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), 'Printer friendly body found.'); 197 198 $number++; 199 } 200 201 /** 202 * Creates a regular expression to check for the sub-nodes in the outline. 203 * 204 * @param array $nodes 205 * An array of nodes to check in outline. 206 * 207 * @return 208 * A regular expression that locates sub-nodes of the outline. 209 */ 210 function generateOutlinePattern($nodes) { 211 $outline = ''; 212 foreach ($nodes as $node) { 213 $outline .= '(node\/' . $node->nid . ')(.*?)(' . $node->title . ')(.*?)'; 214 } 215 216 return '/<div id="book-navigation-' . $this->book->nid . '"(.*?)<ul(.*?)' . $outline . '<\/ul>/s'; 217 } 218 219 /** 220 * Creates a book node. 221 * 222 * @param $book_nid 223 * A book node ID or set to 'new' to create a new book. 224 * @param $parent 225 * (optional) Parent book reference ID. Defaults to NULL. 226 */ 227 function createBookNode($book_nid, $parent = NULL) { 228 // $number does not use drupal_static as it should not be reset 229 // since it uniquely identifies each call to createBookNode(). 230 static $number = 0; // Used to ensure that when sorted nodes stay in same order. 231 232 $edit = array(); 233 $langcode = LANGUAGE_NONE; 234 $edit["title"] = $number . ' - SimpleTest test node ' . $this->randomName(10); 235 $edit["body[$langcode][0][value]"] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32); 236 $edit['book[bid]'] = $book_nid; 237 238 if ($parent !== NULL) { 239 $this->drupalPost('node/add/book', $edit, t('Change book (update list of parents)')); 240 241 $edit['book[plid]'] = $parent; 242 $this->drupalPost(NULL, $edit, t('Save')); 243 } 244 else { 245 $this->drupalPost('node/add/book', $edit, t('Save')); 246 } 247 248 // Check to make sure the book node was created. 249 $node = $this->drupalGetNodeByTitle($edit['title']); 250 $this->assertNotNull(($node === FALSE ? NULL : $node), 'Book node found in database.'); 251 $number++; 252 253 return $node; 254 } 255 256 /** 257 * Tests book export ("printer-friendly version") functionality. 258 */ 259 function testBookExport() { 260 // Create a book. 261 $nodes = $this->createBook(); 262 263 // Login as web user and view printer-friendly version. 264 $this->drupalLogin($this->web_user); 265 $this->drupalGet('node/' . $this->book->nid); 266 $this->clickLink(t('Printer-friendly version')); 267 268 // Make sure each part of the book is there. 269 foreach ($nodes as $node) { 270 $this->assertText($node->title, 'Node title found in printer friendly version.'); 271 $this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), 'Node body found in printer friendly version.'); 272 } 273 274 // Make sure we can't export an unsupported format. 275 $this->drupalGet('book/export/foobar/' . $this->book->nid); 276 $this->assertResponse('404', 'Unsupported export format returned "not found".'); 277 278 // Make sure we get a 404 on a not existing book node. 279 $this->drupalGet('book/export/html/123'); 280 $this->assertResponse('404', 'Not existing book node returned "not found".'); 281 282 // Make sure an anonymous user cannot view printer-friendly version. 283 $this->drupalLogout(); 284 285 // Load the book and verify there is no printer-friendly version link. 286 $this->drupalGet('node/' . $this->book->nid); 287 $this->assertNoLink(t('Printer-friendly version'), 'Anonymous user is not shown link to printer-friendly version.'); 288 289 // Try getting the URL directly, and verify it fails. 290 $this->drupalGet('book/export/html/' . $this->book->nid); 291 $this->assertResponse('403', 'Anonymous user properly forbidden.'); 292 293 // Now grant anonymous users permission to view the printer-friendly 294 // version and verify that node access restrictions still prevent them from 295 // seeing it. 296 user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access printer-friendly version')); 297 $this->drupalGet('book/export/html/' . $this->book->nid); 298 $this->assertResponse('403', 'Anonymous user properly forbidden from seeing the printer-friendly version when denied by node access.'); 299 } 300 301 /** 302 * Tests the functionality of the book navigation block. 303 */ 304 function testBookNavigationBlock() { 305 $this->drupalLogin($this->admin_user); 306 307 // Set block title to confirm that the interface is available. 308 $block_title = $this->randomName(16); 309 $this->drupalPost('admin/structure/block/manage/book/navigation/configure', array('title' => $block_title), t('Save block')); 310 $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.'); 311 312 // Set the block to a region to confirm block is available. 313 $edit = array(); 314 $edit['blocks[book_navigation][region]'] = 'footer'; 315 $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); 316 $this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.'); 317 318 // Give anonymous users the permission 'node test view'. 319 $edit = array(); 320 $edit[DRUPAL_ANONYMOUS_RID . '[node test view]'] = TRUE; 321 $this->drupalPost('admin/people/permissions/' . DRUPAL_ANONYMOUS_RID, $edit, t('Save permissions')); 322 $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users."); 323 324 // Test correct display of the block. 325 $nodes = $this->createBook(); 326 $this->drupalGet('<front>'); 327 $this->assertText($block_title, 'Book navigation block is displayed.'); 328 $this->assertText($this->book->title, format_string('Link to book root (@title) is displayed.', array('@title' => $nodes[0]->title))); 329 $this->assertNoText($nodes[0]->title, 'No links to individual book pages are displayed.'); 330 } 331 332 /** 333 * Tests the book navigation block when an access module is enabled. 334 */ 335 function testNavigationBlockOnAccessModuleEnabled() { 336 $this->drupalLogin($this->admin_user); 337 $edit = array(); 338 339 // Set the block title. 340 $block_title = $this->randomName(16); 341 $edit['title'] = $block_title; 342 343 // Set block display to 'Show block only on book pages'. 344 $edit['book_block_mode'] = 'book pages'; 345 $this->drupalPost('admin/structure/block/manage/book/navigation/configure', $edit, t('Save block')); 346 $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.'); 347 348 // Set the block to a region to confirm block is available. 349 $edit = array(); 350 $edit['blocks[book_navigation][region]'] = 'footer'; 351 $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); 352 $this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.'); 353 354 // Give anonymous users the permission 'node test view'. 355 $edit = array(); 356 $edit[DRUPAL_ANONYMOUS_RID . '[node test view]'] = TRUE; 357 $this->drupalPost('admin/people/permissions/' . DRUPAL_ANONYMOUS_RID, $edit, t('Save permissions')); 358 $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users."); 359 360 // Create a book. 361 $this->createBook(); 362 363 // Test correct display of the block to registered users. 364 $this->drupalLogin($this->web_user); 365 $this->drupalGet('node/' . $this->book->nid); 366 $this->assertText($block_title, 'Book navigation block is displayed to registered users.'); 367 $this->drupalLogout(); 368 369 // Test correct display of the block to anonymous users. 370 $this->drupalGet('node/' . $this->book->nid); 371 $this->assertText($block_title, 'Book navigation block is displayed to anonymous users.'); 372 } 373 374 /** 375 * Tests the access for deleting top-level book nodes. 376 */ 377 function testBookDelete() { 378 $nodes = $this->createBook(); 379 $this->drupalLogin($this->admin_user); 380 $edit = array(); 381 382 // Test access to delete top-level and child book nodes. 383 $this->drupalGet('node/' . $this->book->nid . '/outline/remove'); 384 $this->assertResponse('403', 'Deleting top-level book node properly forbidden.'); 385 $this->drupalPost('node/' . $nodes[4]->nid . '/outline/remove', $edit, t('Remove')); 386 $node4 = node_load($nodes[4]->nid, NULL, TRUE); 387 $this->assertTrue(empty($node4->book), 'Deleting child book node properly allowed.'); 388 389 // Delete all child book nodes and retest top-level node deletion. 390 foreach ($nodes as $node) { 391 $nids[] = $node->nid; 392 } 393 node_delete_multiple($nids); 394 $this->drupalPost('node/' . $this->book->nid . '/outline/remove', $edit, t('Remove')); 395 $node = node_load($this->book->nid, NULL, TRUE); 396 $this->assertTrue(empty($node->book), 'Deleting childless top-level book node properly allowed.'); 397 } 398} 399