Verified Commit b7837b5c authored by Clay Freeman's avatar Clay Freeman
Browse files

Issue #3292968 by clayfreeman: Remove dependency on Drupal-internal front page...

Issue #3292968 by clayfreeman: Remove dependency on Drupal-internal front page configuration value and breadcrumb testing trait
parent 4a675c0b
Loading
Loading
Loading
Loading
+6 −30
Original line number Diff line number Diff line
@@ -68,11 +68,7 @@ class MenuBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
  }

  /**
   * Prepends a link to the front page as the first link if not already present.
   *
   * This method is only concerned with checking if the first link refers to the
   * front page. Even if a link to the front page exists further in the link
   * sequence, a front page link may still be added.
   * Prepend a link to the front page (if desired).
   *
   * @param array $links
   *   The breadcrumb link sequence, passed by reference.
@@ -80,33 +76,13 @@ class MenuBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
   * @return \Drupal\Core\Cache\CacheableMetadata
   *   Additional cacheable metadata accrued by this method.
   */
  protected function addMissingFrontLink(array &$links): CacheableMetadata {
    $cacheability = new CacheableMetadata();
  protected function addFrontLink(array &$links): CacheableMetadata {
    $config = $this->config();

    $cacheability = new CacheableMetadata();
    $cacheability->addCacheableDependency($config);
    if (empty($config->get('prepend_front'))) {
      return $cacheability;
    }

    // Attempt to retrieve the front page path from the site's settings.
    $site_config = $this->configFactory->get('system.site');
    $page_front = $site_config->get('page.front');

    $internal_path = FALSE;
    // The front page must always be a routed URL. If there are no breadcrumb
    // links, or if the first breadcrumb link is unrouted, then a front page
    // link should always be prepended to the link sequence.
    if (!empty($links) && reset($links)->getUrl()->isRouted()) {
      $internal_path = reset($links)->getUrl()->getInternalPath();
      $internal_path = rtrim('/' . $internal_path, '/');

      // The result of the path test is only affected by updates to the
      // configured front page path for routed URLs.
      $cacheability->addCacheableDependency($site_config);
    }

    if ($internal_path !== $page_front) {
    if (!empty($config->get('prepend_front'))) {
      array_unshift($links, Link::createFromRoute($this->t('Home'), '<front>'));
    }

@@ -153,8 +129,8 @@ class MenuBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
      }
    }

    // Prepend a link to the front page if one is not already present.
    $breadcrumb->addCacheableDependency($this->addMissingFrontLink($links));
    // Prepend a link to the front page (if desired).
    $breadcrumb->addCacheableDependency($this->addFrontLink($links));
    $breadcrumb->setLinks($links);

    return $breadcrumb;
+103 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\crouton\Functional;

use Drupal\Core\Url;

/**
 * Provides functional test assertions for breadcrumbs.
 *
 * Copyright (C) 2022  Library Solutions, LLC (et al.).
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */
trait AssertBreadcrumbTrait {

  /**
   * Assert that certain breadcrumb links appear.
   *
   * The expected 'href' values will be processed prior to assertion.
   *
   * @param \Drupal\Core\Url|string|null $goto
   *   A page to load. The current page will be tested if NULL.
   * @param array $expected
   *   A sequence of associative arrays with an 'href' key whose value is the
   *   expected breadcrumb link path and a 'text' key whose value is the
   *   expected breadcrumb link text (not sanitized).
   *
   * @see ::processLink()
   *   For more information about the processing of 'href' values.
   */
  protected function assertBreadcrumbLinks($goto, array $expected) {
    if (isset($goto)) {
      $this->drupalGet($goto);
    }

    $expected = array_map([$this, 'processBreadcrumbLink'], $expected);

    $this->assertSame($expected, array_filter($expected), 'Expected breadcrumb links are valid');
    $this->assertSame($expected, $this->getBreadcrumbLinks());
  }

  /**
   * Fetch all breadcrumb links on the current page.
   *
   * @return array
   *   A sequence of associative arrays with an 'href' key whose value is the
   *   breadcrumb link path and a 'text' key whose value is the breadcrumb link
   *   text (not sanitized).
   */
  protected function getBreadcrumbLinks() {
    $results = [];

    foreach ($this->xpath('//nav[@aria-labelledby="system-breadcrumb"]//ol/li/a') as $element) {
      $results[] = [
        'href' => $element->getAttribute('href'),
        'text' => $element->getText(),
      ];
    }

    return $results;
  }

  /**
   * Process the supplied link for a breadcrumb assertion.
   *
   * The supplied 'href' value will be processed; an empty href will be replaced
   * with the front page URL, and any href not prefixed with a forward slash
   * will be prepended with the appropriate Drupal base prefix.
   *
   * @param array $link
   *   An associative array with an 'href' key whose value is the expected
   *   breadcrumb link path and a 'text' key whose value is the expected
   *   breadcrumb link text (not sanitized).
   *
   * @return array
   *   The input link with a processed value for the 'href' key on success, or
   *   NULL if the link is invalid.
   */
  protected function processBreadcrumbLink(array $link): ?array {
    $href = $link['href'] ?? FALSE;
    $text = $link['text'] ?? NULL;

    if (is_string($text) && is_string($href ?? '')) {
      if ($href == '') {
        $href = Url::fromRoute('<front>')->toString();
      }
      elseif (substr($href, 0, 1) !== '/') {
        $href = Url::fromUri('base:' . $href)->toString();
      }

      return [
        'href' => $href,
        'text' => $text,
      ];
    }

    return NULL;
  }

}
+101 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\crouton\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Functional tests for AssertBreadcrumbTrait.
 *
 * Copyright (C) 2022  Library Solutions, LLC (et al.).
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * @coversDefaultClass \Drupal\Tests\crouton\Functional\AssertBreadcrumbTrait
 * @group crouton
 */
class AssertBreadcrumbTraitFunctionalTest extends BrowserTestBase {

  use AssertBreadcrumbTrait;

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'block',
  ];

  /**
   * Data provider for ::testGetBreadcrumbLinks().
   */
  public function providerTestGetBreadcrumbLinks() {
    return [
      'performance configuration, without breadcrumb block' => [
        FALSE,
        [
          'access administration pages',
        ],
        'admin/config/development/performance',
        [],
      ],
      'performance configuration, with breadcrumb block' => [
        TRUE,
        [
          'access administration pages',
        ],
        'admin/config/development/performance',
        [
          [
            'href' => '',
            'text' => 'Home',
          ],
          [
            'href' => 'admin',
            'text' => 'Administration',
          ],
          [
            'href' => 'admin/config',
            'text' => 'Configuration',
          ],
          [
            'href' => 'admin/config/development',
            'text' => 'Development',
          ],
        ],
      ],
    ];
  }

  /**
   * Test breadcrumb retrieval for the current page.
   *
   * @dataProvider providerTestGetBreadcrumbLinks
   *
   * @covers ::getBreadcrumbLinks
   */
  public function testGetBreadcrumbLinks(bool $place_breadcrumb_block, array $user_permissions, string $page, array $expected) {
    if ($place_breadcrumb_block) {
      $this->drupalPlaceBlock('system_breadcrumb_block');
    }

    if (!empty($user_permissions)) {
      $this->drupalLogin($this->drupalCreateUser($user_permissions));
    }

    $this->drupalGet($page);

    $expected = array_map([$this, 'processBreadcrumbLink'], $expected);

    $this->assertSame($expected, array_filter($expected), 'Expected breadcrumb links are valid');
    $this->assertSame($expected, $this->getBreadcrumbLinks());
  }

}
+102 −78
Original line number Diff line number Diff line
@@ -2,11 +2,9 @@

namespace Drupal\Tests\crouton\Functional;

use Drupal\node\Entity\Node;
use Drupal\menu_link_content\Entity\MenuLinkContent;

use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;

use Drupal\menu_link_content\MenuLinkContentInterface;

/**
 * Functional tests for the menu-based breadcrumb builder.
@@ -41,100 +39,126 @@ class MenuBasedBreadcrumbBuilderFunctionalTest extends BrowserTestBase {
  ];

  /**
   * Data provider for ::testBreadcrumbPrepend().
   */
  public function providerTestBreadcrumbPrepend() {
    return [
      'matching' => [TRUE],
      'not matching' => [FALSE],
    ];
  }

  /**
   * Test that menu-based breadcrumbs are displayed.
   * {@inheritdoc}
   */
  public function testBreadcrumbs() {
    $expected = [];
  public function setUp(): void {
    parent::setUp();

    $config_factory = \Drupal::configFactory();
    $this->drupalPlaceBlock('system_breadcrumb_block');

    $crouton_settings = $config_factory->getEditable('crouton.settings');
    $crouton_settings = \Drupal::configFactory()->getEditable('crouton.settings');
    $crouton_settings->set('menu_name', 'main');
    $crouton_settings->save();
  }

    $content_type = $this->drupalCreateContentType();
  /**
   * Create a menu link to some arbitrary content.
   *
   * @param \Drupal\menu_link_content\MenuLinkContentInterface|null $parent
   *   An optional parent menu link.
   *
   * @return \Drupal\menu_link_content\MenuLinkContentInterface
   *   The resulting menu link.
   */
  protected function createMenuLink(?MenuLinkContentInterface $parent = NULL) {
    $entity_type_manager = \Drupal::entityTypeManager();

    $node_storage = $entity_type_manager->getStorage('node');
    /** @var \Drupal\node\NodeInterface */
    $node = $node_storage->create([
      'title' => $this->randomMachineName(),
      'type' => $this->drupalCreateContentType()->id(),
    ]);

    for ($i = 1; $i <= 3; ++$i) {
      ($node = Node::create([
        'title' => "Test {$i}",
        'type' => $content_type->id(),
      ]))->save();
    $node->save();

      ($link = MenuLinkContent::create([
    $menu_link_content_storage = $entity_type_manager->getStorage('menu_link_content');
    /** @var \Drupal\menu_link_content\MenuLinkContentInterface */
    $link = $menu_link_content_storage->create([
      'title' => $node->label(),
        'parent' => isset($link) ? $link->getPluginId() : NULL,
      'parent' => isset($parent) ? $parent->getPluginId() : NULL,
      'provider' => 'menu_link_content',
      'menu_name' => 'main',
      'link' => [
        'uri' => "internal:/{$node->toUrl()->getInternalPath()}",
      ],
      ]))->save();

      $expected[$node->toUrl()->getInternalPath()] = $node->label();
    }
    ]);

    array_pop($expected);
    $link->save();

    $this->drupalPlaceBlock('system_breadcrumb_block');
    $this->assertBreadcrumb($node->toUrl(), $expected);
    return $link;
  }

  /**
   * Test the prepend of the front page to the breadcrumb trail.
   * Get the expected breadcrumb for the provided link.
   *
   * @dataProvider providerTestBreadcrumbPrepend
   * @param \Drupal\menu_link_content\MenuLinkContentInterface $link
   *   The link for which to get the expected breadcrumb.
   *
   * @covers ::addMissingFrontLink
   * @return array
   *   An associative array with an 'href' key whose value is the menu link path
   *   and a 'text' key whose value is the menu link text (not sanitized).
   */
  public function testBreadcrumbPrepend(bool $matching) {
    $expected = !$matching ? ['' => 'Home'] : [];
  protected function getExpectedBreadcrumbForLink(MenuLinkContentInterface $link): array {
    return [
      'href' => $link->getUrlObject()->getInternalPath(),
      'text' => $link->getTitle(),
    ];
  }

    $config_factory = \Drupal::configFactory();
  /**
   * Data provider for ::testBreadcrumbs().
   */
  public function providerTestBreadcrumbs() {
    return [
      'no append current, no prepend front' => [
        FALSE,
        FALSE,
      ],
      'no append current, prepend front' => [
        FALSE,
        TRUE,
      ],
      'append current, no prepend front' => [
        TRUE,
        FALSE,
      ],
      'append current, prepend front' => [
        TRUE,
        TRUE,
      ],
    ];
  }

    $crouton_settings = $config_factory->getEditable('crouton.settings');
    $crouton_settings->set('menu_name', 'main');
    $crouton_settings->set('prepend_front', TRUE);
  /**
   * Test that menu-based breadcrumbs work as expected.
   *
   * @dataProvider providerTestBreadcrumbs
   */
  public function testBreadcrumbs(bool $append_current, bool $prepend_front) {
    $crouton_settings = \Drupal::configFactory()->getEditable('crouton.settings');
    $crouton_settings->set('append_current', $append_current);
    $crouton_settings->set('prepend_front', $prepend_front);
    $crouton_settings->save();

    $content_type = $this->drupalCreateContentType();

    for ($i = 1; $i <= 3; ++$i) {
      ($node = Node::create([
        'title' => "Test {$i}",
        'type' => $content_type->id(),
      ]))->save();

      ($link = MenuLinkContent::create([
        'title' => $node->label(),
        'parent' => isset($link) ? $link->getPluginId() : NULL,
        'provider' => 'menu_link_content',
        'menu_name' => 'main',
        'link' => [
          'uri' => "internal:/{$node->toUrl()->getInternalPath()}",
        ],
      ]))->save();
    $expected = [];

      $expected[$node->toUrl()->getInternalPath()] = $node->label();
    if ($prepend_front) {
      $expected[] = [
        'href' => '',
        'text' => 'Home',
      ];
    }

    array_pop($expected);
    $expected[] = $this->getExpectedBreadcrumbForLink($link = $this->createMenuLink());
    $expected[] = $this->getExpectedBreadcrumbForLink($link = $this->createMenuLink($link));

    $system_site = $config_factory->getEditable('system.site');
    $system_site->set('page.front', $matching ? '/' . key($expected) : '/node');
    $system_site->save();
    $link = $this->createMenuLink($link);
    if ($append_current) {
      $expected[] = $this->getExpectedBreadcrumbForLink($link);
    }

    $this->drupalPlaceBlock('system_breadcrumb_block');
    $this->assertBreadcrumb($node->toUrl(), $expected);
    $this->assertBreadcrumbLinks($link->getUrlObject(), $expected);
  }

}