.../Drupal/system/Controller/SystemController.php | 10 +- .../Tests/Controller/SystemControllerTest.php | 347 ++++++++++++++++++++ 2 files changed, 351 insertions(+), 6 deletions(-) diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php index e16c838..d07752c 100644 --- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php +++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php @@ -434,13 +434,11 @@ public static function setLinkActiveClass(array $element, array $context) { $class .= 'active'; $node->setAttribute('class', $class); - $body_dom_node = $dom->getElementsByTagName('body')->item(0); - $updated_tag = substr($dom->saveHTML($body_dom_node), 6, -7); + // Get the updated tag. + $updated_tag = $dom->saveXML($node, LIBXML_NOEMPTYTAG); + // saveXML() added a closing tag, remove it. + $updated_tag = substr($updated_tag, 0, strrpos($updated_tag, '<')); - // If saveHTML() added a closing tag (it does so for ), remove it. - if (substr_count($updated_tag, '<') > 1) { - $updated_tag = substr($updated_tag, 0, strrpos($updated_tag, '<')); - } $element['#markup'] = str_replace($tag, $updated_tag, $element['#markup']); // Ensure we only search the remaining HTML. diff --git a/core/modules/system/tests/Drupal/system/Tests/Controller/SystemControllerTest.php b/core/modules/system/tests/Drupal/system/Tests/Controller/SystemControllerTest.php new file mode 100644 index 0000000..82c694e --- /dev/null +++ b/core/modules/system/tests/Drupal/system/Tests/Controller/SystemControllerTest.php @@ -0,0 +1,347 @@ + 'System controller set active link class test', + 'description' => 'Unit test of system controller #post_render_cache callback for marking active links.', + 'group' => 'System' + ); + } + + /** + * Provides test data for testSetLinkActiveClass(). + * + * @see \Drupal\system\Controller\SystemController::setLinkActiveClass() + */ + public function providerTestSetLinkActiveClass() { + // Define all the variations that *don't* affect whether or not an "active" + // class is set, but that should remain unchanged: + // - surrounding HTML + // - tags for which to test the setting of the "active" class + // - content of said tags + $edge_case_html5 = ''; + $html = array( + // Simple HTML. + 0 => array('prefix' => '

', 'suffix' => '

'), + // Tricky HTML5 example that's unsupported by PHP <=5.4's DOMDocument: + // https://drupal.org/comment/7938201#comment-7938201. + 1 => array('prefix' => '

', 'suffix' => '

' . $edge_case_html5 . '
'), + // Multi-byte content *before* the HTML that needs the "active" class. + 2 => array('prefix' => '

αβγδεζηθικλμνξοσὠ

', 'suffix' => '

'), + ); + $tags = array( + // Of course, it must work on anchors. + 'a', + // Unfortunately, it must also work on list items. + 'li', + // … and therefor, on *any* tag, really. + 'foo', + ); + $contents = array( + // Regular content. + 'test', + // Mix of UTF-8 and HTML entities, both must be retained. + '☆ 3 × 4 = €12 and 4 × 3 = €12 ☆', + // Multi-byte content. + 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ', + // Text that closely approximates an important attribute, but should be + // ignored. + 'data-drupal-link-system-path="<front>"', + ); + + // Define all variations that *do* affect whether or not an "active" class + // is set: all possible situations that can be encountered. + $situations = array(); + + // Situations with context: front page, English, no query. + $context = array( + 'path' => 'myfrontpage', + 'front' => TRUE, + 'language' => 'en', + 'query' => array(), + ); + // Nothing to do. + $markup = 'bar'; + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array()); + // Matching path, plus all matching variations. + $attributes = array( + 'data-drupal-link-system-path' => 'myfrontpage', + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en')); + // Matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE)); + // Special matching path, plus all variations. + $attributes = array( + 'data-drupal-link-system-path' => '', + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en')); + // Special matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE)); + + // Situations with context: non-front page, Dutch, no query. + $context = array( + 'path' => 'llama', + 'front' => FALSE, + 'language' => 'nl', + 'query' => array(), + ); + // Nothing to do. + $markup = 'bar'; + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array()); + // Matching path, plus all matching variations. + $attributes = array( + 'data-drupal-link-system-path' => 'llama', + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'nl')); + // Matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE)); + // Special non-matching path, plus all variations. + $attributes = array( + 'data-drupal-link-system-path' => '', + ); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => '{"foo":"bar"}')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE)); + + // Situations with context: non-front page, Dutch, with query. + $context = array( + 'path' => 'llama', + 'front' => FALSE, + 'language' => 'nl', + 'query' => array('foo' => 'bar'), + ); + // Nothing to do. + $markup = 'bar'; + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array()); + // Matching path, plus all matching variations. + $attributes = array( + 'data-drupal-link-system-path' => 'llama', + 'data-drupal-link-query' => Json::encode(array('foo' => 'bar')), + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'nl')); + // Matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en')); + unset($attributes['data-drupal-link-query']); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE)); + // Special non-matching path, plus all variations. + $attributes = array( + 'data-drupal-link-system-path' => '', + ); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en')); + unset($attributes['data-drupal-link-query']); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE)); + + // Situations with context: non-front page, Dutch, with query. + $context = array( + 'path' => 'llama', + 'front' => FALSE, + 'language' => 'nl', + 'query' => array('foo' => 'bar'), + ); + // Nothing to do. + $markup = 'bar'; + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array()); + // Matching path, plus all matching variations. + $attributes = array( + 'data-drupal-link-system-path' => 'llama', + 'data-drupal-link-query' => Json::encode(array('foo' => 'bar')), + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'nl')); + // Matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en')); + unset($attributes['data-drupal-link-query']); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE)); + // Special non-matching path, plus all variations. + $attributes = array( + 'data-drupal-link-system-path' => '', + ); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl')); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en')); + unset($attributes['data-drupal-link-query']); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl', 'data-drupal-link-query' => TRUE)); + + // Situations with context: front page, English, query. + $context = array( + 'path' => 'myfrontpage', + 'front' => TRUE, + 'language' => 'en', + 'query' => array('foo' => 'bar'), + ); + // Nothing to do. + $markup = 'bar'; + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => array()); + // Matching path, plus all matching variations. + $attributes = array( + 'data-drupal-link-system-path' => 'myfrontpage', + 'data-drupal-link-query' => Json::encode(array('foo' => 'bar')), + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en')); + // Matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl')); + unset($attributes['data-drupal-link-query']); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE)); + // Special matching path, plus all variations. + $attributes = array( + 'data-drupal-link-system-path' => '', + 'data-drupal-link-query' => Json::encode(array('foo' => 'bar')), + ); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes); + $situations[] = array('context' => $context, 'is active' => TRUE, 'attributes' => $attributes + array('hreflang' => 'en')); + // Special matching path, plus all non-matching variations. + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'nl')); + unset($attributes['data-drupal-link-query']); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('data-drupal-link-query' => TRUE)); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => "")); + $situations[] = array('context' => $context, 'is active' => FALSE, 'attributes' => $attributes + array('hreflang' => 'en', 'data-drupal-link-query' => TRUE)); + + // Helper function to generate a stubbed renderable array. + $create_element = function ($markup) { + return array( + '#markup' => $markup, + '#attached' => array(), + ); + }; + + // Loop over the surrounding HTML variations. + $data = array(); + for ($h = 0; $h < count($html); $h++) { + $html_prefix = $html[$h]['prefix']; + $html_suffix = $html[$h]['suffix']; + // Loop over the tag variations. + for ($t = 0; $t < count($tags); $t++) { + $tag = $tags[$t]; + // Loop over the tag contents variations. + for ($c = 0; $c < count($contents); $c++) { + $tag_content = $contents[$c]; + + $create_markup = function (Attribute $attributes) use ($html_prefix, $html_suffix, $tag, $tag_content) { + return $html_prefix . '<' . $tag . $attributes . '>' . $tag_content . '' . $html_suffix; + }; + + // Loop over the situations. + for ($s = 0; $s < count($situations); $s++) { + $situation = $situations[$s]; + + // Build the source markup. + $source_markup = $create_markup(new Attribute($situation['attributes'])); + + // Build the target markup. If no "active" class should be set, the + // resulting HTML should be identical. Otherwise, it should get an + // "active" class, either by extending an existing "class" attribute + // or by adding a "class" attribute. + $target_markup = NULL; + if (!$situation['is active']) { + $target_markup = $source_markup; + } + else { + $active_attributes = $situation['attributes']; + if (!isset($active_attributes['class'])) { + $active_attributes['class'] = array(); + } + $active_attributes['class'][] = 'active'; + $target_markup = $create_markup(new Attribute($active_attributes)); + } + + $data[] = array($create_element($source_markup), $situation['context'], $create_element($target_markup)); + } + } + } + } + + return $data; + } + + /** + * Tests setLinkActiveClass(). + * + * @param array $element + * A renderable array with the following keys: + * - #markup + * - #attached + * @param array $context + * The page context to simulate. An array with the following keys: + * - path: the system path of the currently active page + * - front: whether the current page is the front page (which implies the + * current path might also be ) + * - language: the language code of the currently active page + * - query: the query string for the currently active page + * @param array $expected_element + * The returned renderable array. + * + * @dataProvider providerTestSetLinkActiveClass + */ + public function testSetLinkActiveClass(array $element, array $context, $expected_element) { + $this->assertSame($expected_element, SystemController::setLinkActiveClass($element, $context)); + } + +}