3 namespace Drupal\Tests\forum\Functional;
5 use Drupal\Core\Entity\Entity\EntityFormDisplay;
6 use Drupal\Core\Entity\Entity\EntityViewDisplay;
7 use Drupal\Core\Entity\EntityInterface;
10 use Drupal\taxonomy\Entity\Vocabulary;
11 use Drupal\Tests\BrowserTestBase;
14 * Tests for forum.module.
16 * Create, view, edit, delete, and change forum entries and verify its
17 * consistency in the database.
21 class ForumTest extends BrowserTestBase {
28 public static $modules = ['taxonomy', 'comment', 'forum', 'node', 'block', 'menu_ui', 'help'];
31 * A user with various administrative privileges.
36 * A user that can create forum topics and edit its own topics.
38 protected $editOwnTopicsUser;
41 * A user that can create, edit, and delete forum topics.
43 protected $editAnyTopicsUser;
46 * A user with no special privileges.
51 * An administrative user who can bypass comment approval.
53 protected $postCommentUser;
56 * An array representing a forum container.
58 protected $forumContainer;
61 * An array representing a forum.
66 * An array representing a root forum.
71 * An array of forum topic node IDs.
78 protected function setUp() {
80 $this->drupalPlaceBlock('system_breadcrumb_block');
81 $this->drupalPlaceBlock('page_title_block');
84 $this->adminUser = $this->drupalCreateUser([
85 'access administration pages',
90 'administer taxonomy',
91 'create forum content',
94 $this->editAnyTopicsUser = $this->drupalCreateUser([
95 'access administration pages',
96 'create forum content',
97 'edit any forum content',
98 'delete any forum content',
100 $this->editOwnTopicsUser = $this->drupalCreateUser([
101 'create forum content',
102 'edit own forum content',
103 'delete own forum content',
105 $this->webUser = $this->drupalCreateUser();
106 $this->postCommentUser = $this->drupalCreateUser([
107 'administer content types',
108 'create forum content',
110 'skip comment approval',
113 $this->drupalPlaceBlock('help_block', ['region' => 'help']);
114 $this->drupalPlaceBlock('local_actions_block');
118 * Tests forum functionality through the admin and user interfaces.
120 public function testForum() {
121 //Check that the basic forum install creates a default forum topic
122 $this->drupalGet('/forum');
123 // Look for the "General discussion" default forum
124 $this->assertRaw(Link::createFromRoute(t('General discussion'), 'forum.page', ['taxonomy_term' => 1])->toString(), "Found the default forum at the /forum listing");
125 // Check the presence of expected cache tags.
126 $this->assertCacheTag('config:forum.settings');
128 $this->drupalGet(Url::fromRoute('forum.page', ['taxonomy_term' => 1]));
129 $this->assertCacheTag('config:forum.settings');
131 // Do the admin tests.
132 $this->doAdminTests($this->adminUser);
134 // Check display order.
135 $display = EntityViewDisplay::load('node.forum.default');
136 $body = $display->getComponent('body');
137 $comment = $display->getComponent('comment_forum');
138 $taxonomy = $display->getComponent('taxonomy_forums');
140 // Assert field order is body » taxonomy » comments.
141 $this->assertTrue($taxonomy['weight'] < $body['weight']);
142 $this->assertTrue($body['weight'] < $comment['weight']);
145 $display = EntityFormDisplay::load('node.forum.default');
146 $body = $display->getComponent('body');
147 $comment = $display->getComponent('comment_forum');
148 $taxonomy = $display->getComponent('taxonomy_forums');
150 // Assert category comes before body in order.
151 $this->assertTrue($taxonomy['weight'] < $body['weight']);
153 $this->generateForumTopics();
155 // Log in an unprivileged user to view the forum topics and generate an
156 // active forum topics list.
157 $this->drupalLogin($this->webUser);
158 // Verify that this user is shown a message that they may not post content.
159 $this->drupalGet('forum/' . $this->forum['tid']);
160 $this->assertText(t('You are not allowed to post new content in the forum'), "Authenticated user without permission to post forum content is shown message in local tasks to that effect.");
162 // Log in, and do basic tests for a user with permission to edit any forum
164 $this->doBasicTests($this->editAnyTopicsUser, TRUE);
165 // Create a forum node authored by this user.
166 $any_topics_user_node = $this->createForumTopic($this->forum, FALSE);
168 // Log in, and do basic tests for a user with permission to edit only its
169 // own forum content.
170 $this->doBasicTests($this->editOwnTopicsUser, FALSE);
171 // Create a forum node authored by this user.
172 $own_topics_user_node = $this->createForumTopic($this->forum, FALSE);
173 // Verify that this user cannot edit forum content authored by another user.
174 $this->verifyForums($any_topics_user_node, FALSE, 403);
176 // Verify that this user is shown a local task to add new forum content.
177 $this->drupalGet('forum');
178 $this->assertLink(t('Add new Forum topic'));
179 $this->drupalGet('forum/' . $this->forum['tid']);
180 $this->assertLink(t('Add new Forum topic'));
182 // Log in a user with permission to edit any forum content.
183 $this->drupalLogin($this->editAnyTopicsUser);
184 // Verify that this user can edit forum content authored by another user.
185 $this->verifyForums($own_topics_user_node, TRUE);
187 // Verify the topic and post counts on the forum page.
188 $this->drupalGet('forum');
190 // Verify row for testing forum.
191 $forum_arg = [':forum' => 'forum-list-' . $this->forum['tid']];
193 // Topics cell contains number of topics and number of unread topics.
194 $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="forum__topics"]', $forum_arg);
195 $topics = $this->xpath($xpath);
196 $topics = trim($topics[0]->getText());
197 // The extracted text contains the number of topics (6) and new posts
198 // (also 6) in this table cell.
199 $this->assertEquals('6 6 new posts in forum ' . $this->forum['name'], $topics, 'Number of topics found.');
201 // Verify the number of unread topics.
202 $elements = $this->xpath('//tr[@id=:forum]//td[@class="forum__topics"]//a', $forum_arg);
203 $this->assertStringStartsWith('6 new posts', $elements[0]->getText(), 'Number of unread topics found.');
204 // Verify that the forum name is in the unread topics text.
205 $elements = $this->xpath('//tr[@id=:forum]//em[@class="placeholder"]', $forum_arg);
206 $this->assertContains($this->forum['name'], $elements[0]->getText(), 'Forum name found in unread topics text.');
208 // Verify total number of posts in forum.
209 $elements = $this->xpath('//tr[@id=:forum]//td[@class="forum__posts"]', $forum_arg);
210 $this->assertEquals('6', $elements[0]->getText(), 'Number of posts found.');
212 // Test loading multiple forum nodes on the front page.
213 $this->drupalLogin($this->drupalCreateUser(['administer content types', 'create forum content', 'post comments']));
214 $this->drupalPostForm('admin/structure/types/manage/forum', ['options[promote]' => 'promote'], t('Save content type'));
215 $this->createForumTopic($this->forum, FALSE);
216 $this->createForumTopic($this->forum, FALSE);
217 $this->drupalGet('node');
219 // Test adding a comment to a forum topic.
220 $node = $this->createForumTopic($this->forum, FALSE);
222 $edit['comment_body[0][value]'] = $this->randomMachineName();
223 $this->drupalPostForm('node/' . $node->id(), $edit, t('Save'));
224 $this->assertResponse(200);
226 // Test editing a forum topic that has a comment.
227 $this->drupalLogin($this->editAnyTopicsUser);
228 $this->drupalGet('forum/' . $this->forum['tid']);
229 $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save'));
230 $this->assertResponse(200);
232 // Test the root forum page title change.
233 $this->drupalGet('forum');
234 $this->assertCacheTag('config:taxonomy.vocabulary.' . $this->forum['vid']);
235 $this->assertTitle(t('Forums | Drupal'));
236 $vocabulary = Vocabulary::load($this->forum['vid']);
237 $vocabulary->set('name', 'Discussions');
239 $this->drupalGet('forum');
240 $this->assertTitle(t('Discussions | Drupal'));
242 // Test anonymous action link.
243 $this->drupalLogout();
244 $this->drupalGet('forum/' . $this->forum['tid']);
245 $this->assertLink(t('Log in to post new content in the forum.'));
249 * Tests that forum nodes can't be added without a parent.
251 * Verifies that forum nodes are not created without choosing "forum" from the
254 public function testAddOrphanTopic() {
255 // Must remove forum topics to test creating orphan topics.
256 $vid = $this->config('forum.settings')->get('vocabulary');
257 $tids = \Drupal::entityQuery('taxonomy_term')
258 ->condition('vid', $vid)
260 entity_delete_multiple('taxonomy_term', $tids);
262 // Create an orphan forum item.
264 $edit['title[0][value]'] = $this->randomMachineName(10);
265 $edit['body[0][value]'] = $this->randomMachineName(120);
266 $this->drupalLogin($this->adminUser);
267 $this->drupalPostForm('node/add/forum', $edit, t('Save'));
269 $nid_count = db_query('SELECT COUNT(nid) FROM {node}')->fetchField();
270 $this->assertEqual(0, $nid_count, 'A forum node was not created when missing a forum vocabulary.');
272 // Reset the defaults for future tests.
273 \Drupal::service('module_installer')->install(['forum']);
277 * Runs admin tests on the admin user.
279 * @param object $user
280 * The logged-in user.
282 private function doAdminTests($user) {
284 $this->drupalLogin($user);
286 // Add forum to the Tools menu.
288 $this->drupalPostForm('admin/structure/menu/manage/tools', $edit, t('Save'));
289 $this->assertResponse(200);
291 // Edit forum taxonomy.
292 // Restoration of the settings fails and causes subsequent tests to fail.
293 $this->editForumVocabulary();
294 // Create forum container.
295 $this->forumContainer = $this->createForum('container');
296 // Verify "edit container" link exists and functions correctly.
297 $this->drupalGet('admin/structure/forum');
298 // Verify help text is shown.
299 $this->assertText(t('Forums contain forum topics. Use containers to group related forums'));
300 // Verify action links are there.
301 $this->assertLink('Add forum');
302 $this->assertLink('Add container');
303 $this->clickLink('edit container');
304 $this->assertRaw('Edit container', 'Followed the link to edit the container');
305 // Create forum inside the forum container.
306 $this->forum = $this->createForum('forum', $this->forumContainer['tid']);
307 // Verify the "edit forum" link exists and functions correctly.
308 $this->drupalGet('admin/structure/forum');
309 $this->clickLink('edit forum');
310 $this->assertRaw('Edit forum', 'Followed the link to edit the forum');
311 // Navigate back to forum structure page.
312 $this->drupalGet('admin/structure/forum');
313 // Create second forum in container, destined to be deleted below.
314 $delete_forum = $this->createForum('forum', $this->forumContainer['tid']);
315 // Save forum overview.
316 $this->drupalPostForm('admin/structure/forum/', [], t('Save'));
317 $this->assertRaw(t('The configuration options have been saved.'));
318 // Delete this second forum.
319 $this->deleteForum($delete_forum['tid']);
320 // Create forum at the top (root) level.
321 $this->rootForum = $this->createForum('forum');
323 // Test vocabulary form alterations.
324 $this->drupalGet('admin/structure/taxonomy/manage/forums');
325 $this->assertSession()->buttonExists('Save');
326 $this->assertSession()->buttonNotExists('Delete');
328 // Test term edit form alterations.
329 $this->drupalGet('taxonomy/term/' . $this->forumContainer['tid'] . '/edit');
330 // Test parent field been hidden by forum module.
331 $this->assertNoField('parent[]', 'Parent field not found.');
333 // Create a default vocabulary named "Tags".
334 $description = 'Use tags to group articles on similar topics into categories.';
335 $help = 'Enter a comma-separated list of words to describe your content.';
336 $vocabulary = Vocabulary::create([
338 'description' => $description,
340 'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
344 // Test tags vocabulary form is not affected.
345 $this->drupalGet('admin/structure/taxonomy/manage/tags');
346 $this->assertSession()->buttonExists('Save');
347 $this->assertLink(t('Delete'));
348 // Test tags vocabulary term form is not affected.
349 $this->drupalGet('admin/structure/taxonomy/manage/tags/add');
350 $this->assertField('parent[]', 'Parent field found.');
351 // Test relations widget exists.
352 $relations_widget = $this->xpath("//details[@id='edit-relations']");
353 $this->assertTrue(isset($relations_widget[0]), 'Relations widget element found.');
357 * Edits the forum taxonomy.
359 public function editForumVocabulary() {
360 // Backup forum taxonomy.
361 $vid = $this->config('forum.settings')->get('vocabulary');
362 $original_vocabulary = Vocabulary::load($vid);
364 // Generate a random name and description.
366 'name' => $this->randomMachineName(10),
367 'description' => $this->randomMachineName(100),
370 // Edit the vocabulary.
371 $this->drupalPostForm('admin/structure/taxonomy/manage/' . $original_vocabulary->id(), $edit, t('Save'));
372 $this->assertResponse(200);
373 $this->assertRaw(t('Updated vocabulary %name.', ['%name' => $edit['name']]), 'Vocabulary was edited');
375 // Grab the newly edited vocabulary.
376 $current_vocabulary = Vocabulary::load($vid);
378 // Make sure we actually edited the vocabulary properly.
379 $this->assertEqual($current_vocabulary->label(), $edit['name'], 'The name was updated');
380 $this->assertEqual($current_vocabulary->getDescription(), $edit['description'], 'The description was updated');
382 // Restore the original vocabulary's name and description.
383 $current_vocabulary->set('name', $original_vocabulary->label());
384 $current_vocabulary->set('description', $original_vocabulary->getDescription());
385 $current_vocabulary->save();
386 // Reload vocabulary to make sure changes are saved.
387 $current_vocabulary = Vocabulary::load($vid);
388 $this->assertEqual($current_vocabulary->label(), $original_vocabulary->label(), 'The original vocabulary settings were restored');
392 * Creates a forum container or a forum.
394 * @param string $type
395 * The forum type (forum container or forum).
397 * The forum parent. This defaults to 0, indicating a root forum.
399 * @return \Drupal\Core\Database\StatementInterface
400 * The created taxonomy term data.
402 public function createForum($type, $parent = 0) {
403 // Generate a random name/description.
404 $name = $this->randomMachineName(10);
405 $description = $this->randomMachineName(100);
408 'name[0][value]' => $name,
409 'description[0][value]' => $description,
410 'parent[0]' => $parent,
415 $this->drupalPostForm('admin/structure/forum/add/' . $type, $edit, t('Save'));
416 $this->assertResponse(200);
417 $type = ($type == 'container') ? 'forum container' : 'forum';
420 'Created new @type @term.',
421 ['@term' => $name, '@type' => t($type)]
423 format_string('@type was created', ['@type' => ucfirst($type)])
426 // Verify that the creation message contains a link to a term.
427 $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'term/']);
428 $this->assert(isset($view_link), 'The message area contains a link to a term');
431 $term = db_query("SELECT * FROM {taxonomy_term_field_data} t WHERE t.vid = :vid AND t.name = :name AND t.description__value = :desc AND t.default_langcode = 1", [':vid' => $this->config('forum.settings')->get('vocabulary'), ':name' => $name, ':desc' => $description])->fetchAssoc();
432 $this->assertTrue(!empty($term), 'The ' . $type . ' exists in the database');
434 // Verify forum hierarchy.
436 $parent_tid = db_query("SELECT t.parent FROM {taxonomy_term_hierarchy} t WHERE t.tid = :tid", [':tid' => $tid])->fetchField();
437 $this->assertTrue($parent == $parent_tid, 'The ' . $type . ' is linked to its container');
439 $forum = $this->container->get('entity.manager')->getStorage('taxonomy_term')->load($tid);
440 $this->assertEqual(($type == 'forum container'), (bool) $forum->forum_container->value);
450 public function deleteForum($tid) {
452 $this->drupalGet('admin/structure/forum/edit/forum/' . $tid);
453 $this->clickLink(t('Delete'));
454 $this->assertText('Are you sure you want to delete the forum');
455 $this->assertNoText('Add forum');
456 $this->assertNoText('Add forum container');
457 $this->drupalPostForm(NULL, [], t('Delete'));
459 // Assert that the forum no longer exists.
460 $this->drupalGet('forum/' . $tid);
461 $this->assertResponse(404, 'The forum was not found');
465 * Runs basic tests on the indicated user.
467 * @param \Drupal\Core\Session\AccountInterface $user
468 * The logged in user.
470 * User has 'access administration pages' privilege.
472 private function doBasicTests($user, $admin) {
474 $this->drupalLogin($user);
475 // Attempt to create forum topic under a container.
476 $this->createForumTopic($this->forumContainer, TRUE);
477 // Create forum node.
478 $node = $this->createForumTopic($this->forum, FALSE);
479 // Verify the user has access to all the forum nodes.
480 $this->verifyForums($node, $admin);
484 * Tests a forum with a new post displays properly.
486 public function testForumWithNewPost() {
487 // Log in as the first user.
488 $this->drupalLogin($this->adminUser);
489 // Create a forum container.
490 $this->forumContainer = $this->createForum('container');
492 $this->forum = $this->createForum('forum');
494 $node = $this->createForumTopic($this->forum, FALSE);
496 // Log in as a second user.
497 $this->drupalLogin($this->postCommentUser);
498 // Post a reply to the topic.
500 $edit['subject[0][value]'] = $this->randomMachineName();
501 $edit['comment_body[0][value]'] = $this->randomMachineName();
502 $this->drupalPostForm('node/' . $node->id(), $edit, t('Save'));
503 $this->assertResponse(200);
505 // Test replying to a comment.
506 $this->clickLink('Reply');
507 $this->assertResponse(200);
508 $this->assertFieldByName('comment_body[0][value]');
510 // Log in as the first user.
511 $this->drupalLogin($this->adminUser);
512 // Check that forum renders properly.
513 $this->drupalGet("forum/{$this->forum['tid']}");
514 $this->assertResponse(200);
516 // Verify there is no unintentional HTML tag escaping.
517 $this->assertNoEscaped('<', '');
521 * Creates a forum topic.
523 * @param array $forum
525 * @param bool $container
526 * TRUE if $forum is a container; FALSE otherwise.
529 * The created topic node.
531 public function createForumTopic($forum, $container = FALSE) {
532 // Generate a random subject/body.
533 $title = $this->randomMachineName(20);
534 $body = $this->randomMachineName(200);
537 'title[0][value]' => $title,
538 'body[0][value]' => $body,
540 $tid = $forum['tid'];
542 // Create the forum topic, preselecting the forum ID via a URL parameter.
543 $this->drupalPostForm('node/add/forum', $edit, t('Save'), ['query' => ['forum_id' => $tid]]);
545 $type = t('Forum topic');
547 $this->assertNoText(t('@type @title has been created.', ['@type' => $type, '@title' => $title]), 'Forum topic was not created');
548 $this->assertRaw(t('The item %title is a forum container, not a forum.', ['%title' => $forum['name']]), 'Error message was shown');
552 $this->assertText(t('@type @title has been created.', ['@type' => $type, '@title' => $title]), 'Forum topic was created');
553 $this->assertNoRaw(t('The item %title is a forum container, not a forum.', ['%title' => $forum['name']]), 'No error message was shown');
555 // Verify that the creation message contains a link to a term.
556 $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'term/']);
557 $this->assert(isset($view_link), 'The message area contains a link to a term');
560 // Retrieve node object, ensure that the topic was created and in the proper forum.
561 $node = $this->drupalGetNodeByTitle($title);
562 $this->assertTrue($node != NULL, format_string('Node @title was loaded', ['@title' => $title]));
563 $this->assertEqual($node->taxonomy_forums->target_id, $tid, 'Saved forum topic was in the expected forum');
566 $this->drupalGet('node/' . $node->id());
567 $this->assertRaw($title, 'Subject was found');
568 $this->assertRaw($body, 'Body was found');
574 * Verifies that the logged in user has access to a forum node.
576 * @param \Drupal\Core\Entity\EntityInterface $node
577 * The node being checked.
579 * Boolean to indicate whether the user can 'access administration pages'.
580 * @param int $response
581 * The expected HTTP response code.
583 private function verifyForums(EntityInterface $node, $admin, $response = 200) {
584 $response2 = ($admin) ? 200 : 403;
586 // View forum help node.
587 $this->drupalGet('admin/help/forum');
588 $this->assertResponse($response2);
589 if ($response2 == 200) {
590 $this->assertTitle(t('Forum | Drupal'), 'Forum help title was displayed');
591 $this->assertText(t('Forum'), 'Forum help node was displayed');
594 // View forum container page.
595 $this->verifyForumView($this->forumContainer);
597 $this->verifyForumView($this->forum, $this->forumContainer);
598 // View root forum page.
599 $this->verifyForumView($this->rootForum);
602 $this->drupalGet('node/' . $node->id());
603 $this->assertResponse(200);
604 $this->assertTitle($node->label() . ' | Drupal', 'Forum node was displayed');
605 $breadcrumb_build = [
606 Link::createFromRoute(t('Home'), '<front>'),
607 Link::createFromRoute(t('Forums'), 'forum.index'),
608 Link::createFromRoute($this->forumContainer['name'], 'forum.page', ['taxonomy_term' => $this->forumContainer['tid']]),
609 Link::createFromRoute($this->forum['name'], 'forum.page', ['taxonomy_term' => $this->forum['tid']]),
612 '#theme' => 'breadcrumb',
613 '#links' => $breadcrumb_build,
615 $this->assertRaw(\Drupal::service('renderer')->renderRoot($breadcrumb), 'Breadcrumbs were displayed');
617 // View forum edit node.
618 $this->drupalGet('node/' . $node->id() . '/edit');
619 $this->assertResponse($response);
620 if ($response == 200) {
621 $this->assertTitle('Edit Forum topic ' . $node->label() . ' | Drupal', 'Forum edit node was displayed');
624 if ($response == 200) {
625 // Edit forum node (including moving it to another forum).
627 $edit['title[0][value]'] = 'node/' . $node->id();
628 $edit['body[0][value]'] = $this->randomMachineName(256);
629 // Assume the topic is initially associated with $forum.
630 $edit['taxonomy_forums'] = $this->rootForum['tid'];
631 $edit['shadow'] = TRUE;
632 $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
633 $this->assertText(t('Forum topic @title has been updated.', ['@title' => $edit['title[0][value]']]), 'Forum node was edited');
635 // Verify topic was moved to a different forum.
636 $forum_tid = db_query("SELECT tid FROM {forum} WHERE nid = :nid AND vid = :vid", [
637 ':nid' => $node->id(),
638 ':vid' => $node->getRevisionId(),
640 $this->assertTrue($forum_tid == $this->rootForum['tid'], 'The forum topic is linked to a different forum');
642 // Delete forum node.
643 $this->drupalPostForm('node/' . $node->id() . '/delete', [], t('Delete'));
644 $this->assertResponse($response);
645 $this->assertRaw(t('Forum topic %title has been deleted.', ['%title' => $edit['title[0][value]']]), 'Forum node was deleted');
650 * Verifies the display of a forum page.
652 * @param array $forum
653 * A row from the taxonomy_term_data table in an array.
654 * @param array $parent
655 * (optional) An array representing the forum's parent.
657 private function verifyForumView($forum, $parent = NULL) {
659 $this->drupalGet('forum/' . $forum['tid']);
660 $this->assertResponse(200);
661 $this->assertTitle($forum['name'] . ' | Drupal');
663 $breadcrumb_build = [
664 Link::createFromRoute(t('Home'), '<front>'),
665 Link::createFromRoute(t('Forums'), 'forum.index'),
667 if (isset($parent)) {
668 $breadcrumb_build[] = Link::createFromRoute($parent['name'], 'forum.page', ['taxonomy_term' => $parent['tid']]);
672 '#theme' => 'breadcrumb',
673 '#links' => $breadcrumb_build,
675 $this->assertRaw(\Drupal::service('renderer')->renderRoot($breadcrumb), 'Breadcrumbs were displayed');
679 * Generates forum topics.
681 private function generateForumTopics() {
683 for ($i = 0; $i < 5; $i++) {
684 $node = $this->createForumTopic($this->forum, FALSE);
685 $this->nids[] = $node->id();