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 // Loop over the surrounding HTML variations.
243 for ($h = 0; $h < count($html); $h++) {
244 $html_prefix = $html[$h]['prefix'];
245 $html_suffix = $html[$h]['suffix'];
246 // Loop over the tag variations.
247 for ($t = 0; $t < count($tags); $t++) {
249 // Loop over the tag contents variations.
250 for ($c = 0; $c < count($contents); $c++) {
251 $tag_content = $contents[$c];
253 $create_markup = function (Attribute $attributes) use ($html_prefix, $html_suffix, $tag, $tag_content) {
254 return $html_prefix . '<' . $tag . $attributes . '>' . $tag_content . '</' . $tag . '>' . $html_suffix;
257 // Loop over the situations.
258 for ($s = 0; $s < count($situations); $s++) {
259 $situation = $situations[$s];
261 // Build the source markup.
262 $source_markup = $create_markup(new Attribute($situation['attributes']));
264 // Build the target markup. If no "is-active" class should be set,
265 // the resulting HTML should be identical. Otherwise, it should get
266 // an "is-active" class, either by extending an existing "class"
267 // attribute or by adding a "class" attribute.
268 $target_markup = NULL;
269 if (!$situation['is active']) {
270 $target_markup = $source_markup;
273 $active_attributes = $situation['attributes'];
274 if (!isset($active_attributes['class'])) {
275 $active_attributes['class'] = [];
277 $active_attributes['class'][] = 'is-active';
278 $target_markup = $create_markup(new Attribute($active_attributes));
281 $data[] = [$source_markup, $situation['context']['path'], $situation['context']['front'], $situation['context']['language'], $situation['context']['query'], $target_markup];
287 // Test case to verify that the 'is-active' class is not added multiple
290 0 => '<a data-drupal-link-system-path="<front>">Once</a> <a data-drupal-link-system-path="<front>">Twice</a>',
295 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>',
298 // Test cases to verify that the 'is-active' class is added when on the
299 // front page, and there are two different kinds of matching links on the
301 // - the matching path (the resolved front page path)
302 // - the special matching path ('<front>')
303 $front_special_link = '<a data-drupal-link-system-path="<front>">Front</a>';
304 $front_special_link_active = '<a data-drupal-link-system-path="<front>" class="is-active">Front</a>';
305 $front_path_link = '<a data-drupal-link-system-path="myfrontpage">Front Path</a>';
306 $front_path_link_active = '<a data-drupal-link-system-path="myfrontpage" class="is-active">Front Path</a>';
308 0 => $front_path_link . ' ' . $front_special_link,
313 5 => $front_path_link_active . ' ' . $front_special_link_active,
316 0 => $front_special_link . ' ' . $front_path_link,
321 5 => $front_special_link_active . ' ' . $front_path_link_active,
324 // Test cases to verify that links to the front page do not get the
325 // 'is-active' class when not on the front page.
326 $other_link = '<a data-drupal-link-system-path="otherpage">Other page</a>';
327 $other_link_active = '<a data-drupal-link-system-path="otherpage" class="is-active">Other page</a>';
328 $data['<front>-and-other-link-on-other-path'] = [
329 0 => $front_special_link . ' ' . $other_link,
334 5 => $front_special_link . ' ' . $other_link_active,
336 $data['front-and-other-link-on-other-path'] = [
337 0 => $front_path_link . ' ' . $other_link,
342 5 => $front_path_link . ' ' . $other_link_active,
344 $data['other-and-<front>-link-on-other-path'] = [
345 0 => $other_link . ' ' . $front_special_link,
350 5 => $other_link_active . ' ' . $front_special_link,
352 $data['other-and-front-link-on-other-path'] = [
353 0 => $other_link . ' ' . $front_path_link,
358 5 => $other_link_active . ' ' . $front_path_link,
364 * Tests setLinkActiveClass().
366 * @param string $html_markup
367 * The original HTML markup.
368 * @param string $current_path
369 * The system path of the currently active page.
370 * @param bool $is_front
371 * Whether the current page is the front page (which implies the current
372 * path might also be <front>).
373 * @param string $url_language
374 * The language code of the current URL.
375 * @param array $query
376 * The query string for the current URL.
377 * @param string $expected_html_markup
378 * The expected updated HTML markup.
380 * @dataProvider providerTestSetLinkActiveClass
381 * @covers ::setLinkActiveClass
383 public function testSetLinkActiveClass($html_markup, $current_path, $is_front, $url_language, array $query, $expected_html_markup) {
384 $this->assertSame($expected_html_markup, ActiveLinkResponseFilter::setLinkActiveClass($html_markup, $current_path, $is_front, $url_language, $query));