3 namespace Drupal\Tests\Core\EventSubscriber;
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Core\EventSubscriber\ActiveLinkResponseFilter;
7 use Drupal\Core\Template\Attribute;
8 use Drupal\Tests\UnitTestCase;
11 * @coversDefaultClass \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
12 * @group EventSubscriber
14 class ActiveLinkResponseFilterTest extends UnitTestCase {
17 * Provides test data for testSetLinkActiveClass().
19 * @see \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter::setLinkActiveClass()
21 public function providerTestSetLinkActiveClass() {
22 // Define all the variations that *don't* affect whether or not an
23 // "is-active" class is set, but that should remain unchanged:
25 // - tags for which to test the setting of the "is-active" class
26 // - content of said tags
27 $edge_case_html5 = '<audio src="foo.ogg">
28 <track kind="captions" src="foo.en.vtt" srclang="en" label="English">
29 <track kind="captions" src="foo.sv.vtt" srclang="sv" label="Svenska">
33 0 => ['prefix' => '<div><p>', 'suffix' => '</p></div>'],
34 // Tricky HTML5 example that's unsupported by PHP <=5.4's DOMDocument:
35 // https://www.drupal.org/comment/7938201#comment-7938201.
36 1 => ['prefix' => '<div><p>', 'suffix' => '</p>' . $edge_case_html5 . '</div>'],
37 // Multi-byte content *before* the HTML that needs the "is-active" class.
38 2 => ['prefix' => '<div><p>αβγδεζηθικλμνξοσὠ</p><p>', 'suffix' => '</p></div>'],
41 // Of course, it must work on anchors.
43 // Unfortunately, it must also work on list items.
45 // … and therefore, on *any* tag, really.
51 // Mix of UTF-8 and HTML entities, both must be retained.
52 '☆ 3 × 4 = €12 and 4 × 3 = €12 ☆',
53 // Multi-byte content.
55 // Text that closely approximates an important attribute, but should be
57 'data-drupal-link-system-path="<front>"',
60 // Define all variations that *do* affect whether or not an "is-active"
61 // class is set: all possible situations that can be encountered.
64 // Situations with context: front page, English, no query.
66 'path' => 'myfrontpage',
72 $markup = '<foo>bar</foo>';
73 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => []];
74 // Matching path, plus all matching variations.
76 'data-drupal-link-system-path' => 'myfrontpage',
78 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
79 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'en']];
80 // Matching path, plus all non-matching variations.
81 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl']];
82 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => '{"foo":"bar"}']];
83 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
84 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
85 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => '{"foo":"bar"}']];
86 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => ""]];
87 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => TRUE]];
88 // Special matching path, plus all variations.
90 'data-drupal-link-system-path' => '<front>',
92 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
93 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'en']];
94 // Special matching path, plus all non-matching variations.
95 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl']];
96 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => '{"foo":"bar"}']];
97 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
98 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
99 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => '{"foo":"bar"}']];
100 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => ""]];
101 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => TRUE]];
103 // Situations with context: non-front page, Dutch, no query.
110 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => []];
111 // Matching path, plus all matching variations.
113 'data-drupal-link-system-path' => 'llama',
115 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
116 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'nl']];
117 // Matching path, plus all non-matching variations.
118 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en']];
119 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => '{"foo":"bar"}']];
120 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
121 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
122 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => '{"foo":"bar"}']];
123 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => ""]];
124 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => TRUE]];
125 // Special non-matching path, plus all variations.
127 'data-drupal-link-system-path' => '<front>',
129 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes];
130 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en']];
131 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => '{"foo":"bar"}']];
132 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
133 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
134 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => '{"foo":"bar"}']];
135 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => ""]];
136 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => TRUE]];
138 // Situations with context: non-front page, Dutch, with query.
143 'query' => ['foo' => 'bar'],
145 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => []];
146 // Matching path, plus all matching variations.
148 'data-drupal-link-system-path' => 'llama',
149 'data-drupal-link-query' => Json::encode(['foo' => 'bar']),
151 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
152 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'nl']];
153 // Matching path, plus all non-matching variations.
154 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en']];
155 unset($attributes['data-drupal-link-query']);
156 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => ""]];
157 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => TRUE]];
158 // Special non-matching path, plus all variations.
160 'data-drupal-link-system-path' => '<front>',
162 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes];
163 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl']];
164 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en']];
165 unset($attributes['data-drupal-link-query']);
166 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => ""]];
167 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => TRUE]];
169 // Situations with context: non-front page, Dutch, with query.
174 'query' => ['foo' => 'bar'],
176 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => []];
177 // Matching path, plus all matching variations.
179 'data-drupal-link-system-path' => 'llama',
180 'data-drupal-link-query' => Json::encode(['foo' => 'bar']),
182 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
183 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'nl']];
184 // Matching path, plus all non-matching variations.
185 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en']];
186 unset($attributes['data-drupal-link-query']);
187 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
188 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
189 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => ""]];
190 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => TRUE]];
191 // Special non-matching path, plus all variations.
193 'data-drupal-link-system-path' => '<front>',
195 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes];
196 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl']];
197 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en']];
198 unset($attributes['data-drupal-link-query']);
199 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
200 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
201 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => ""]];
202 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl', 'data-drupal-link-query' => TRUE]];
204 // Situations with context: front page, English, query.
206 'path' => 'myfrontpage',
209 'query' => ['foo' => 'bar'],
211 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => []];
212 // Matching path, plus all matching variations.
214 'data-drupal-link-system-path' => 'myfrontpage',
215 'data-drupal-link-query' => Json::encode(['foo' => 'bar']),
217 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
218 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'en']];
219 // Matching path, plus all non-matching variations.
220 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl']];
221 unset($attributes['data-drupal-link-query']);
222 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
223 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
224 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => ""]];
225 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => TRUE]];
226 // Special matching path, plus all variations.
228 'data-drupal-link-system-path' => '<front>',
229 'data-drupal-link-query' => Json::encode(['foo' => 'bar']),
231 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
232 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes + ['hreflang' => 'en']];
233 // Special matching path, plus all non-matching variations.
234 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'nl']];
235 unset($attributes['data-drupal-link-query']);
236 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => ""]];
237 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['data-drupal-link-query' => TRUE]];
238 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => ""]];
239 $situations[] = ['context' => $context, 'is active' => FALSE, 'attributes' => $attributes + ['hreflang' => 'en', 'data-drupal-link-query' => TRUE]];
241 // Query with unsorted keys must match when the attribute is in sorted form.
243 'path' => 'myfrontpage',
246 'query' => ['foo' => 'bar', 'baz' => 'qux'],
249 'data-drupal-link-system-path' => 'myfrontpage',
250 'data-drupal-link-query' => Json::encode(['baz' => 'qux', 'foo' => 'bar']),
252 $situations[] = ['context' => $context, 'is active' => TRUE, 'attributes' => $attributes];
254 // Loop over the surrounding HTML variations.
256 for ($h = 0; $h < count($html); $h++) {
257 $html_prefix = $html[$h]['prefix'];
258 $html_suffix = $html[$h]['suffix'];
259 // Loop over the tag variations.
260 for ($t = 0; $t < count($tags); $t++) {
262 // Loop over the tag contents variations.
263 for ($c = 0; $c < count($contents); $c++) {
264 $tag_content = $contents[$c];
266 $create_markup = function (Attribute $attributes) use ($html_prefix, $html_suffix, $tag, $tag_content) {
267 return $html_prefix . '<' . $tag . $attributes . '>' . $tag_content . '</' . $tag . '>' . $html_suffix;
270 // Loop over the situations.
271 for ($s = 0; $s < count($situations); $s++) {
272 $situation = $situations[$s];
274 // Build the source markup.
275 $source_markup = $create_markup(new Attribute($situation['attributes']));
277 // Build the target markup. If no "is-active" class should be set,
278 // the resulting HTML should be identical. Otherwise, it should get
279 // an "is-active" class, either by extending an existing "class"
280 // attribute or by adding a "class" attribute.
281 $target_markup = NULL;
282 if (!$situation['is active']) {
283 $target_markup = $source_markup;
286 $active_attributes = $situation['attributes'];
287 if (!isset($active_attributes['class'])) {
288 $active_attributes['class'] = [];
290 $active_attributes['class'][] = 'is-active';
291 $target_markup = $create_markup(new Attribute($active_attributes));
294 $data[] = [$source_markup, $situation['context']['path'], $situation['context']['front'], $situation['context']['language'], $situation['context']['query'], $target_markup];
300 // Test case to verify that the 'is-active' class is not added multiple
303 0 => '<a data-drupal-link-system-path="<front>">Once</a> <a data-drupal-link-system-path="<front>">Twice</a>',
308 5 => '<a data-drupal-link-system-path="<front>" class="is-active">Once</a> <a data-drupal-link-system-path="<front>" class="is-active">Twice</a>',
311 // Test cases to verify that the 'is-active' class is added when on the
312 // front page, and there are two different kinds of matching links on the
314 // - the matching path (the resolved front page path)
315 // - the special matching path ('<front>')
316 $front_special_link = '<a data-drupal-link-system-path="<front>">Front</a>';
317 $front_special_link_active = '<a data-drupal-link-system-path="<front>" class="is-active">Front</a>';
318 $front_path_link = '<a data-drupal-link-system-path="myfrontpage">Front Path</a>';
319 $front_path_link_active = '<a data-drupal-link-system-path="myfrontpage" class="is-active">Front Path</a>';
321 0 => $front_path_link . ' ' . $front_special_link,
326 5 => $front_path_link_active . ' ' . $front_special_link_active,
329 0 => $front_special_link . ' ' . $front_path_link,
334 5 => $front_special_link_active . ' ' . $front_path_link_active,
337 // Test cases to verify that links to the front page do not get the
338 // 'is-active' class when not on the front page.
339 $other_link = '<a data-drupal-link-system-path="otherpage">Other page</a>';
340 $other_link_active = '<a data-drupal-link-system-path="otherpage" class="is-active">Other page</a>';
341 $data['<front>-and-other-link-on-other-path'] = [
342 0 => $front_special_link . ' ' . $other_link,
347 5 => $front_special_link . ' ' . $other_link_active,
349 $data['front-and-other-link-on-other-path'] = [
350 0 => $front_path_link . ' ' . $other_link,
355 5 => $front_path_link . ' ' . $other_link_active,
357 $data['other-and-<front>-link-on-other-path'] = [
358 0 => $other_link . ' ' . $front_special_link,
363 5 => $other_link_active . ' ' . $front_special_link,
365 $data['other-and-front-link-on-other-path'] = [
366 0 => $other_link . ' ' . $front_path_link,
371 5 => $other_link_active . ' ' . $front_path_link,
377 * Tests setLinkActiveClass().
379 * @param string $html_markup
380 * The original HTML markup.
381 * @param string $current_path
382 * The system path of the currently active page.
383 * @param bool $is_front
384 * Whether the current page is the front page (which implies the current
385 * path might also be <front>).
386 * @param string $url_language
387 * The language code of the current URL.
388 * @param array $query
389 * The query string for the current URL.
390 * @param string $expected_html_markup
391 * The expected updated HTML markup.
393 * @dataProvider providerTestSetLinkActiveClass
394 * @covers ::setLinkActiveClass
396 public function testSetLinkActiveClass($html_markup, $current_path, $is_front, $url_language, array $query, $expected_html_markup) {
397 $this->assertSame($expected_html_markup, ActiveLinkResponseFilter::setLinkActiveClass($html_markup, $current_path, $is_front, $url_language, $query));