3 namespace Drupal\Tests\Core\Utility;
5 use Drupal\Component\Render\MarkupInterface;
6 use Drupal\Core\GeneratedNoLink;
7 use Drupal\Core\GeneratedUrl;
8 use Drupal\Core\Language\Language;
10 use Drupal\Core\Render\Markup;
12 use Drupal\Core\Utility\LinkGenerator;
13 use Drupal\Tests\UnitTestCase;
14 use Drupal\Core\DependencyInjection\ContainerBuilder;
17 * @coversDefaultClass \Drupal\Core\Utility\LinkGenerator
20 class LinkGeneratorTest extends UnitTestCase {
23 * The tested link generator.
25 * @var \Drupal\Core\Utility\LinkGenerator
27 protected $linkGenerator;
30 * The mocked url generator.
32 * @var \PHPUnit_Framework_MockObject_MockObject
34 protected $urlGenerator;
37 * The mocked module handler.
39 * @var \PHPUnit_Framework_MockObject_MockObject
41 protected $moduleHandler;
44 * The mocked renderer service.
46 * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Render\RendererInterface
51 * The mocked URL Assembler service.
53 * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Utility\UnroutedUrlAssemblerInterface
55 protected $urlAssembler;
58 * Contains the LinkGenerator default options.
60 protected $defaultOptions = [
63 'set_active_class' => FALSE,
70 protected function setUp() {
73 $this->urlGenerator = $this->getMockBuilder('\Drupal\Core\Routing\UrlGenerator')
74 ->disableOriginalConstructor()
76 $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
77 $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
78 $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->renderer);
79 $this->urlAssembler = $this->getMock('\Drupal\Core\Utility\UnroutedUrlAssemblerInterface');
83 * Provides test data for testing the link method.
85 * @see \Drupal\Tests\Core\Utility\LinkGeneratorTest::testGenerateHrefs()
88 * Returns some test data.
90 public function providerTestGenerateHrefs() {
92 // Test that the url returned by the URL generator is used.
93 ['test_route_1', [], FALSE, '/test-route-1'],
94 // Test that $parameters is passed to the URL generator.
95 ['test_route_2', ['value' => 'example'], FALSE, '/test-route-2/example'],
96 // Test that the 'absolute' option is passed to the URL generator.
97 ['test_route_3', [], TRUE, 'http://example.com/test-route-3'],
102 * Tests the link method with certain hrefs.
104 * @see \Drupal\Core\Utility\LinkGenerator::generate()
105 * @see \Drupal\Tests\Core\Utility\LinkGeneratorTest::providerTestGenerate()
107 * @dataProvider providerTestGenerateHrefs
109 public function testGenerateHrefs($route_name, array $parameters, $absolute, $expected_url) {
110 $this->urlGenerator->expects($this->once())
111 ->method('generateFromRoute')
112 ->with($route_name, $parameters, ['absolute' => $absolute] + $this->defaultOptions)
113 ->willReturn((new GeneratedUrl())->setGeneratedUrl($expected_url));
114 $this->moduleHandler->expects($this->once())
117 $url = new Url($route_name, $parameters, ['absolute' => $absolute]);
118 $url->setUrlGenerator($this->urlGenerator);
119 $result = $this->linkGenerator->generate('Test', $url);
121 'attributes' => ['href' => $expected_url],
126 * Tests the generate() method with a route.
130 public function testGenerate() {
131 $this->urlGenerator->expects($this->once())
132 ->method('generateFromRoute')
133 ->with('test_route_1', [], ['fragment' => 'the-fragment'] + $this->defaultOptions)
134 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1#the-fragment'));
136 $this->moduleHandler->expects($this->once())
138 ->with('link', $this->isType('array'));
140 $url = new Url('test_route_1', [], ['fragment' => 'the-fragment']);
141 $url->setUrlGenerator($this->urlGenerator);
143 $result = $this->linkGenerator->generate('Test', $url);
146 'href' => '/test-route-1#the-fragment',
153 * Tests the generate() method with the <nolink> route.
157 public function testGenerateNoLink() {
158 $this->urlGenerator->expects($this->never())
159 ->method('generateFromRoute');
160 $this->moduleHandler->expects($this->once())
162 ->with('link', $this->isType('array'));
164 $url = Url::fromRoute('<nolink>');
165 $url->setUrlGenerator($this->urlGenerator);
167 $result = $this->linkGenerator->generate('Test', $url);
168 $this->assertTrue($result instanceof GeneratedNoLink);
169 $this->assertSame('<span>Test</span>', (string) $result);
173 * Tests the generate() method with an external URL.
175 * The set_active_class option is set to TRUE to ensure this does not cause
176 * an error together with an external URL.
180 public function testGenerateExternal() {
181 $this->urlAssembler->expects($this->once())
183 ->with('https://www.drupal.org', ['set_active_class' => TRUE, 'external' => TRUE] + $this->defaultOptions)
184 ->will($this->returnArgument(0));
186 $this->moduleHandler->expects($this->once())
188 ->with('link', $this->isType('array'));
190 $this->urlAssembler->expects($this->once())
192 ->with('https://www.drupal.org', ['set_active_class' => TRUE, 'external' => TRUE] + $this->defaultOptions)
193 ->willReturnArgument(0);
195 $url = Url::fromUri('https://www.drupal.org');
196 $url->setUrlGenerator($this->urlGenerator);
197 $url->setUnroutedUrlAssembler($this->urlAssembler);
198 $url->setOption('set_active_class', TRUE);
200 $result = $this->linkGenerator->generate('Drupal', $url);
203 'href' => 'https://www.drupal.org',
205 'content' => 'Drupal',
210 * Tests the generate() method with a url containing double quotes.
214 public function testGenerateUrlWithQuotes() {
215 $this->urlAssembler->expects($this->once())
217 ->with('base:example', ['query' => ['foo' => '"bar"', 'zoo' => 'baz']] + $this->defaultOptions)
218 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/example?foo=%22bar%22&zoo=baz'));
220 $path_validator = $this->getMock('Drupal\Core\Path\PathValidatorInterface');
221 $container_builder = new ContainerBuilder();
222 $container_builder->set('path.validator', $path_validator);
223 \Drupal::setContainer($container_builder);
225 $path = '/example?foo="bar"&zoo=baz';
226 $url = Url::fromUserInput($path);
227 $url->setUrlGenerator($this->urlGenerator);
228 $url->setUnroutedUrlAssembler($this->urlAssembler);
230 $result = $this->linkGenerator->generate('Drupal', $url);
234 'href' => '/example?foo=%22bar%22&zoo=baz',
236 'content' => 'Drupal',
241 * Tests the link method with additional attributes.
243 * @see \Drupal\Core\Utility\LinkGenerator::generate()
245 public function testGenerateAttributes() {
246 $this->urlGenerator->expects($this->once())
247 ->method('generateFromRoute')
248 ->with('test_route_1', [], $this->defaultOptions)
249 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1'));
251 // Test that HTML attributes are added to the anchor.
252 $url = new Url('test_route_1', [], [
253 'attributes' => ['title' => 'Tooltip'],
255 $url->setUrlGenerator($this->urlGenerator);
256 $result = $this->linkGenerator->generate('Test', $url);
259 'href' => '/test-route-1',
260 'title' => 'Tooltip',
266 * Tests the link method with passed query options.
268 * @see \Drupal\Core\Utility\LinkGenerator::generate()
270 public function testGenerateQuery() {
271 $this->urlGenerator->expects($this->once())
272 ->method('generateFromRoute')
273 ->with('test_route_1', [], ['query' => ['test' => 'value']] + $this->defaultOptions)
274 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1?test=value'));
276 $url = new Url('test_route_1', [], [
277 'query' => ['test' => 'value'],
279 $url->setUrlGenerator($this->urlGenerator);
280 $result = $this->linkGenerator->generate('Test', $url);
283 'href' => '/test-route-1?test=value',
289 * Tests the link method with passed query options via parameters.
291 * @see \Drupal\Core\Utility\LinkGenerator::generate()
293 public function testGenerateParametersAsQuery() {
294 $this->urlGenerator->expects($this->once())
295 ->method('generateFromRoute')
296 ->with('test_route_1', ['test' => 'value'], $this->defaultOptions)
297 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1?test=value'));
299 $url = new Url('test_route_1', ['test' => 'value'], []);
300 $url->setUrlGenerator($this->urlGenerator);
301 $result = $this->linkGenerator->generate('Test', $url);
304 'href' => '/test-route-1?test=value',
310 * Tests the link method with arbitrary passed options.
312 * @see \Drupal\Core\Utility\LinkGenerator::generate()
314 public function testGenerateOptions() {
315 $this->urlGenerator->expects($this->once())
316 ->method('generateFromRoute')
317 ->with('test_route_1', [], ['key' => 'value'] + $this->defaultOptions)
318 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1?test=value'));
319 $url = new Url('test_route_1', [], [
322 $url->setUrlGenerator($this->urlGenerator);
323 $result = $this->linkGenerator->generate('Test', $url);
326 'href' => '/test-route-1?test=value',
332 * Tests the link method with a script tab.
334 * @see \Drupal\Core\Utility\LinkGenerator::generate()
336 public function testGenerateXss() {
337 $this->urlGenerator->expects($this->once())
338 ->method('generateFromRoute')
339 ->with('test_route_4', [], $this->defaultOptions)
340 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-4'));
342 // Test that HTML link text is escaped by default.
343 $url = new Url('test_route_4');
344 $url->setUrlGenerator($this->urlGenerator);
345 $result = $this->linkGenerator->generate("<script>alert('XSS!')</script>", $url);
346 $this->assertNoXPathResults('//a[@href="/test-route-4"]/script', $result);
350 * Tests the link method with html.
352 * @see \Drupal\Core\Utility\LinkGenerator::generate()
354 public function testGenerateWithHtml() {
355 $this->urlGenerator->expects($this->at(0))
356 ->method('generateFromRoute')
357 ->with('test_route_5', [], $this->defaultOptions)
358 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-5'));
359 $this->urlGenerator->expects($this->at(1))
360 ->method('generateFromRoute')
361 ->with('test_route_5', [], $this->defaultOptions)
362 ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-5'));
364 // Test that HTML tags are stripped from the 'title' attribute.
365 $url = new Url('test_route_5', [], [
366 'attributes' => ['title' => '<em>HTML Tooltip</em>'],
368 $url->setUrlGenerator($this->urlGenerator);
369 $result = $this->linkGenerator->generate('Test', $url);
372 'href' => '/test-route-5',
373 'title' => 'HTML Tooltip',
377 // Test that safe HTML is output inside the anchor tag unescaped. The
378 // Markup::create() call is an intentional unit test for the interaction
379 // between MarkupInterface and the LinkGenerator.
380 $url = new Url('test_route_5', []);
381 $url->setUrlGenerator($this->urlGenerator);
382 $result = $this->linkGenerator->generate(Markup::create('<em>HTML output</em>'), $url);
384 'attributes' => ['href' => '/test-route-5'],
389 $this->assertTrue(strpos($result, '<em>HTML output</em>') !== FALSE);
393 * Tests the active class on the link method.
395 * @see \Drupal\Core\Utility\LinkGenerator::generate()
397 public function testGenerateActive() {
398 $this->urlGenerator->expects($this->exactly(5))
399 ->method('generateFromRoute')
400 ->willReturnCallback(function ($name, $parameters = [], $options = [], $collect_bubbleable_metadata = FALSE) {
403 return (new GeneratedUrl())->setGeneratedUrl('/test-route-1');
405 return (new GeneratedUrl())->setGeneratedUrl('/test-route-3');
407 if ($parameters['object'] == '1') {
408 return (new GeneratedUrl())->setGeneratedUrl('/test-route-4/1');
413 $this->urlGenerator->expects($this->exactly(4))
414 ->method('getPathFromRoute')
415 ->will($this->returnValueMap([
416 ['test_route_1', [], 'test-route-1'],
417 ['test_route_3', [], 'test-route-3'],
418 ['test_route_4', ['object' => '1'], 'test-route-4/1'],
421 $this->moduleHandler->expects($this->exactly(5))
425 $url = new Url('test_route_1', [], ['set_active_class' => TRUE]);
426 $url->setUrlGenerator($this->urlGenerator);
427 $result = $this->linkGenerator->generate('Test', $url);
429 'attributes' => ['data-drupal-link-system-path' => 'test-route-1'],
432 // Render a link with the set_active_class option disabled.
433 $url = new Url('test_route_1', [], ['set_active_class' => FALSE]);
434 $url->setUrlGenerator($this->urlGenerator);
435 $result = $this->linkGenerator->generate('Test', $url);
436 $this->assertNoXPathResults('//a[@data-drupal-link-system-path="test-route-1"]', $result);
438 // Render a link with an associated language.
439 $url = new Url('test_route_1', [], [
440 'language' => new Language(['id' => 'de']),
441 'set_active_class' => TRUE,
443 $url->setUrlGenerator($this->urlGenerator);
444 $result = $this->linkGenerator->generate('Test', $url);
447 'data-drupal-link-system-path' => 'test-route-1',
452 // Render a link with a query parameter.
453 $url = new Url('test_route_3', [], [
454 'query' => ['value' => 'example_1'],
455 'set_active_class' => TRUE,
457 $url->setUrlGenerator($this->urlGenerator);
458 $result = $this->linkGenerator->generate('Test', $url);
461 'data-drupal-link-system-path' => 'test-route-3',
462 'data-drupal-link-query' => '{"value":"example_1"}',
466 // Render a link with route parameters and a query parameter.
467 $url = new Url('test_route_4', ['object' => '1'], [
468 'query' => ['value' => 'example_1'],
469 'set_active_class' => TRUE,
471 $url->setUrlGenerator($this->urlGenerator);
472 $result = $this->linkGenerator->generate('Test', $url);
475 'data-drupal-link-system-path' => 'test-route-4/1',
476 'data-drupal-link-query' => '{"value":"example_1"}',
482 * Tests the LinkGenerator's support for collecting bubbleable metadata.
484 * @see \Drupal\Core\Utility\LinkGenerator::generate()
485 * @see \Drupal\Core\Utility\LinkGenerator::generateFromLink()
487 public function testGenerateBubbleableMetadata() {
488 $options = ['query' => [], 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE];
489 $this->urlGenerator->expects($this->any())
490 ->method('generateFromRoute')
491 ->will($this->returnValueMap([
492 ['test_route_1', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-1')],
495 $url = new Url('test_route_1');
496 $url->setUrlGenerator($this->urlGenerator);
497 $expected_link_markup = '<a href="/test-route-1">Test</a>';
499 // Test ::generate().
500 $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generate('Test', $url));
501 $generated_link = $this->linkGenerator->generate('Test', $url);
502 $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
503 $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
505 // Test ::generateFromLink().
506 $link = new Link('Test', $url);
507 $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generateFromLink($link));
508 $generated_link = $this->linkGenerator->generateFromLink($link);
509 $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
510 $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
514 * Tests altering the URL object using hook_link_alter().
518 public function testGenerateWithAlterHook() {
519 $options = ['query' => [], 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE];
520 $this->urlGenerator->expects($this->any())
521 ->method('generateFromRoute')
522 ->will($this->returnValueMap([
523 ['test_route_1', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-1')],
524 ['test_route_2', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-2')],
527 $url = new Url('test_route_2');
528 $url->setUrlGenerator($this->urlGenerator);
530 $this->moduleHandler->expects($this->atLeastOnce())
532 ->willReturnCallback(function ($hook, &$options) {
533 $options['url'] = (new Url('test_route_1'))->setUrlGenerator($this->urlGenerator);
536 $expected_link_markup = '<a href="/test-route-1">Test</a>';
537 $this->assertEquals($expected_link_markup, (string) $this->linkGenerator->generate('Test', $url)->getGeneratedLink());
541 * Tests whether rendering the same link twice works.
543 * This is a regression test for https://www.drupal.org/node/2842399.
545 public function testGenerateTwice() {
546 $this->urlGenerator->expects($this->any())
547 ->method('generateFromRoute')
548 ->will($this->returnValue((new GeneratedUrl())->setGeneratedUrl('/')));
550 $url = Url::fromRoute('<front>', [], ['attributes' => ['class' => ['foo', 'bar']]]);
551 $url->setUrlGenerator($this->urlGenerator);
553 $link = Link::fromTextAndUrl('text', $url);
554 $link->setLinkGenerator($this->linkGenerator);
555 $output = $link->toString() . $link->toString();
556 $this->assertEquals('<a href="/" class="foo bar">text</a><a href="/" class="foo bar">text</a>', $output);
560 * Checks that a link with certain properties exists in a given HTML snippet.
562 * @param array $properties
563 * An associative array of link properties, with the following keys:
564 * - attributes: optional array of HTML attributes that should be present.
565 * - content: optional link content.
566 * @param \Drupal\Component\Render\MarkupInterface $html
569 * How many times the link should be present in the HTML. Defaults to 1.
571 public static function assertLink(array $properties, MarkupInterface $html, $count = 1) {
572 // Provide default values.
573 $properties += ['attributes' => []];
575 // Create an XPath query that selects a link element.
578 // Append XPath predicates for the attributes and content text.
580 foreach ($properties['attributes'] as $attribute => $value) {
581 $predicates[] = "@$attribute='$value'";
583 if (!empty($properties['content'])) {
584 $predicates[] = "contains(.,'{$properties['content']}')";
586 if (!empty($predicates)) {
587 $query .= '[' . implode(' and ', $predicates) . ']';
590 // Execute the query.
591 $document = new \DOMDocument();
592 $document->loadHTML($html);
593 $xpath = new \DOMXPath($document);
595 self::assertEquals($count, $xpath->query($query)->length);
599 * Checks that the given XPath query has no results in a given HTML snippet.
601 * @param string $query
602 * The XPath query to execute.
603 * @param string $html
604 * The HTML snippet to check.
607 * The number of results that are found.
609 protected function assertNoXPathResults($query, $html) {
610 $document = new \DOMDocument();
611 $document->loadHTML($html);
612 $xpath = new \DOMXPath($document);
614 self::assertFalse((bool) $xpath->query($query)->length);