LinkFieldTest.php 33.6 KB
Newer Older
1 2
<?php

3
namespace Drupal\Tests\link\Functional;
4

5
use Drupal\Component\Utility\Html;
6
use Drupal\Component\Utility\Unicode;
7
use Drupal\Core\Entity\Entity\EntityViewDisplay;
8
use Drupal\Core\Link;
9
use Drupal\Core\Url;
10
use Drupal\entity_test\Entity\EntityTest;
11
use Drupal\field\Entity\FieldConfig;
12
use Drupal\link\LinkItemInterface;
13
use Drupal\node\NodeInterface;
14
use Drupal\Tests\BrowserTestBase;
15
use Drupal\field\Entity\FieldStorageConfig;
16
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
17 18 19

/**
 * Tests link field widgets and formatters.
20 21
 *
 * @group link
22
 */
23
class LinkFieldTest extends BrowserTestBase {
24

25 26
  use PathAliasTestTrait;

27 28 29 30 31
  /**
   * Modules to enable.
   *
   * @var array
   */
32
  protected static $modules = [
33 34 35 36 37
    'entity_test',
    'link',
    'node',
    'link_test_base_field',
  ];
38

39 40 41 42 43
  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'classy';

44 45 46
  /**
   * A field to use in this test class.
   *
47
   * @var \Drupal\field\Entity\FieldStorageConfig
48
   */
49
  protected $fieldStorage;
50 51 52 53

  /**
   * The instance used in this test class.
   *
54
   * @var \Drupal\field\Entity\FieldConfig
55
   */
56
  protected $field;
57

58
  protected function setUp(): void {
59 60
    parent::setUp();

61
    $this->drupalLogin($this->drupalCreateUser([
62 63
      'view test entity',
      'administer entity_test content',
64
      'link to any page',
65
    ]));
66 67 68 69 70
  }

  /**
   * Tests link field URL validation.
   */
71
  public function testURLValidation() {
72
    $field_name = mb_strtolower($this->randomMachineName());
73
    // Create a field with settings to validate.
74
    $this->fieldStorage = FieldStorageConfig::create([
75
      'field_name' => $field_name,
76
      'entity_type' => 'entity_test',
77
      'type' => 'link',
78
    ]);
79
    $this->fieldStorage->save();
80
    $this->field = FieldConfig::create([
81
      'field_storage' => $this->fieldStorage,
82
      'bundle' => 'entity_test',
83
      'settings' => [
84
        'title' => DRUPAL_DISABLED,
85
        'link_type' => LinkItemInterface::LINK_GENERIC,
86
      ],
87
    ]);
88
    $this->field->save();
89 90 91
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');
    $display_repository->getFormDisplay('entity_test', 'entity_test')
92
      ->setComponent($field_name, [
93
        'type' => 'link_default',
94
        'settings' => [
95
          'placeholder_url' => 'http://example.com',
96 97
        ],
      ])
98
      ->save();
99
    $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
100
      ->setComponent($field_name, [
101
        'type' => 'link',
102
      ])
103 104
      ->save();

105
    // Display creation form.
106
    $this->drupalGet('entity_test/add');
107
    $this->assertFieldByName("{$field_name}[0][uri]", '', 'Link URL field is displayed');
108
    $this->assertRaw('placeholder="http://example.com"');
109

110
    // Create a path alias.
111
    $this->createPathAlias('/admin', '/a/path/alias');
112 113 114 115

    // Create a node to test the link widget.
    $node = $this->drupalCreateNode();

116
    $restricted_node = $this->drupalCreateNode(['status' => NodeInterface::NOT_PUBLISHED]);
117

118 119
    // Define some valid URLs (keys are the entered values, values are the
    // strings displayed to the user).
120
    $valid_external_entries = [
121
      'http://www.example.com/' => 'http://www.example.com/',
122 123 124 125
      // Strings within parenthesis without leading space char.
      'http://www.example.com/strings_(string_within_parenthesis)' => 'http://www.example.com/strings_(string_within_parenthesis)',
      // Numbers within parenthesis without leading space char.
      'http://www.example.com/numbers_(9999)' => 'http://www.example.com/numbers_(9999)',
126 127
    ];
    $valid_internal_entries = [
128 129 130 131 132 133 134 135
      '/entity_test/add' => '/entity_test/add',
      '/a/path/alias' => '/a/path/alias',

      // Front page, with query string and fragment.
      '/' => '&lt;front&gt;',
      '/?example=llama' => '&lt;front&gt;?example=llama',
      '/#example' => '&lt;front&gt;#example',

136 137 138 139
      // Trailing spaces should be ignored.
      '/ ' => '&lt;front&gt;',
      '/path with spaces ' => '/path with spaces',

140 141 142 143
      // @todo '<front>' is valid input for BC reasons, may be removed by
      //   https://www.drupal.org/node/2421941
      '<front>' => '&lt;front&gt;',
      '<front>#example' => '&lt;front&gt;#example',
144
      '<front>?example=llama' => '&lt;front&gt;?example=llama',
145

146 147 148 149 150
      // Text-only links.
      '<nolink>' => '&lt;nolink&gt;',
      'route:<nolink>' => '&lt;nolink&gt;',
      '<none>' => '&lt;none&gt;',

151 152 153 154 155 156 157 158 159
      // Query string and fragment.
      '?example=llama' => '?example=llama',
      '#example' => '#example',

      // Entity reference autocomplete value.
      $node->label() . ' (1)' => $node->label() . ' (1)',
      // Entity URI displayed as ER autocomplete value when displayed in a form.
      'entity:node/1' => $node->label() . ' (1)',
      // URI for an entity that exists, but is not accessible by the user.
160
      'entity:node/' . $restricted_node->id() => '- Restricted access - (' . $restricted_node->id() . ')',
161 162
      // URI for an entity that doesn't exist, but with a valid ID.
      'entity:user/999999' => 'entity:user/999999',
163
    ];
164

165
    // Define some invalid URLs.
166
    $validation_error_1 = "The path '@link_path' is invalid.";
167
    $validation_error_2 = 'Manually entered paths should start with one of the following characters: / ? #';
168
    $validation_error_3 = "The path '@link_path' is inaccessible.";
169
    $invalid_external_entries = [
170
      // Invalid protocol
171
      'invalid://not-a-valid-protocol' => $validation_error_1,
172
      // Missing host name
173
      'http://' => $validation_error_1,
174 175
    ];
    $invalid_internal_entries = [
176 177
      'no-leading-slash' => $validation_error_2,
      'entity:non_existing_entity_type/yar' => $validation_error_1,
178 179
      // URI for an entity that doesn't exist, with an invalid ID.
      'entity:user/invalid-parameter' => $validation_error_1,
180
    ];
181 182 183 184 185 186

    // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC.
    $this->assertValidEntries($field_name, $valid_external_entries + $valid_internal_entries);
    $this->assertInvalidEntries($field_name, $invalid_external_entries + $invalid_internal_entries);

    // Test external URLs for 'link_type' = LinkItemInterface::LINK_EXTERNAL.
187
    $this->field->setSetting('link_type', LinkItemInterface::LINK_EXTERNAL);
188
    $this->field->save();
189 190 191 192
    $this->assertValidEntries($field_name, $valid_external_entries);
    $this->assertInvalidEntries($field_name, $valid_internal_entries + $invalid_external_entries);

    // Test external URLs for 'link_type' = LinkItemInterface::LINK_INTERNAL.
193
    $this->field->setSetting('link_type', LinkItemInterface::LINK_INTERNAL);
194
    $this->field->save();
195 196
    $this->assertValidEntries($field_name, $valid_internal_entries);
    $this->assertInvalidEntries($field_name, $valid_external_entries + $invalid_internal_entries);
197 198 199 200 201 202 203 204

    // Ensure that users with 'link to any page', don't apply access checking.
    $this->drupalLogin($this->drupalCreateUser([
      'view test entity',
      'administer entity_test content',
    ]));
    $this->assertValidEntries($field_name, ['/entity_test/add' => '/entity_test/add']);
    $this->assertInValidEntries($field_name, ['/admin' => $validation_error_3]);
205 206 207 208 209 210 211 212 213 214 215
  }

  /**
   * Asserts that valid URLs can be submitted.
   *
   * @param string $field_name
   *   The field name.
   * @param array $valid_entries
   *   An array of valid URL entries.
   */
  protected function assertValidEntries($field_name, array $valid_entries) {
216
    foreach ($valid_entries as $uri => $string) {
217
      $edit = [
218
        "{$field_name}[0][uri]" => $uri,
219
      ];
220
      $this->drupalPostForm('entity_test/add', $edit, t('Save'));
221
      preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
222
      $id = $match[1];
223
      $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
224
      $this->assertRaw('"' . $string . '"');
225 226 227 228 229 230 231 232 233 234 235 236
    }
  }

  /**
   * Asserts that invalid URLs cannot be submitted.
   *
   * @param string $field_name
   *   The field name.
   * @param array $invalid_entries
   *   An array of invalid URL entries.
   */
  protected function assertInvalidEntries($field_name, array $invalid_entries) {
237
    foreach ($invalid_entries as $invalid_value => $error_message) {
238
      $edit = [
239
        "{$field_name}[0][uri]" => $invalid_value,
240
      ];
241
      $this->drupalPostForm('entity_test/add', $edit, t('Save'));
242
      $this->assertText(t($error_message, ['@link_path' => $invalid_value]));
243 244 245 246
    }
  }

  /**
247
   * Tests the link title settings of a link field.
248
   */
249
  public function testLinkTitle() {
250
    $field_name = mb_strtolower($this->randomMachineName());
251
    // Create a field with settings to validate.
252
    $this->fieldStorage = FieldStorageConfig::create([
253
      'field_name' => $field_name,
254
      'entity_type' => 'entity_test',
255
      'type' => 'link',
256
    ]);
257
    $this->fieldStorage->save();
258
    $this->field = FieldConfig::create([
259
      'field_storage' => $this->fieldStorage,
260
      'bundle' => 'entity_test',
261
      'label' => 'Read more about this entity',
262
      'settings' => [
263
        'title' => DRUPAL_OPTIONAL,
264
        'link_type' => LinkItemInterface::LINK_GENERIC,
265
      ],
266
    ]);
267
    $this->field->save();
268 269 270
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');
    $display_repository->getFormDisplay('entity_test', 'entity_test')
271
      ->setComponent($field_name, [
272
        'type' => 'link_default',
273
        'settings' => [
274
          'placeholder_url' => 'http://example.com',
275
          'placeholder_title' => 'Enter the text for this link',
276 277
        ],
      ])
278
      ->save();
279
    $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
280
      ->setComponent($field_name, [
281 282
        'type' => 'link',
        'label' => 'hidden',
283
      ])
284 285
      ->save();

286
    // Verify that the link text field works according to the field setting.
287
    foreach ([DRUPAL_DISABLED, DRUPAL_REQUIRED, DRUPAL_OPTIONAL] as $title_setting) {
288
      // Update the link title field setting.
289
      $this->field->setSetting('title', $title_setting);
290
      $this->field->save();
291 292

      // Display creation form.
293
      $this->drupalGet('entity_test/add');
294 295
      // Assert label is shown.
      $this->assertText('Read more about this entity');
296
      $this->assertFieldByName("{$field_name}[0][uri]", '', 'URL field found.');
297
      $this->assertRaw('placeholder="http://example.com"');
298 299

      if ($title_setting === DRUPAL_DISABLED) {
300
        $this->assertNoFieldByName("{$field_name}[0][title]", '', 'Link text field not found.');
301
        $this->assertNoRaw('placeholder="Enter the text for this link"');
302 303
      }
      else {
304
        $this->assertRaw('placeholder="Enter the text for this link"');
305

306
        $this->assertFieldByName("{$field_name}[0][title]", '', 'Link text field found.');
307 308 309 310 311 312 313 314
        if ($title_setting === DRUPAL_OPTIONAL) {
          // Verify that the URL is required, if the link text is non-empty.
          $edit = [
            "{$field_name}[0][title]" => 'Example',
          ];
          $this->drupalPostForm(NULL, $edit, t('Save'));
          $this->assertText(t('The URL field is required when the @title field is specified.', ['@title' => t('Link text')]));
        }
315
        if ($title_setting === DRUPAL_REQUIRED) {
316
          // Verify that the link text is required, if the URL is non-empty.
317
          $edit = [
318
            "{$field_name}[0][uri]" => 'http://www.example.com',
319
          ];
320
          $this->drupalPostForm(NULL, $edit, t('Save'));
321
          $this->assertText(t('@title field is required if there is @uri input.', ['@title' => t('Link text'), '@uri' => t('URL')]));
322

323
          // Verify that the link text is not required, if the URL is empty.
324
          $edit = [
325
            "{$field_name}[0][uri]" => '',
326
          ];
327
          $this->drupalPostForm(NULL, $edit, t('Save'));
328
          $this->assertNoText(t('@name field is required.', ['@name' => t('Link text')]));
329

330
          // Verify that a URL and link text meets requirements.
331
          $this->drupalGet('entity_test/add');
332
          $edit = [
333
            "{$field_name}[0][uri]" => 'http://www.example.com',
334
            "{$field_name}[0][title]" => 'Example',
335
          ];
336
          $this->drupalPostForm(NULL, $edit, t('Save'));
337
          $this->assertNoText(t('@name field is required.', ['@name' => t('Link text')]));
338 339 340 341
        }
      }
    }

342
    // Verify that a link without link text is rendered using the URL as text.
343
    $value = 'http://www.example.com/';
344
    $edit = [
345
      "{$field_name}[0][uri]" => $value,
346
      "{$field_name}[0][title]" => '',
347
    ];
348
    $this->drupalPostForm(NULL, $edit, t('Save'));
349
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
350
    $id = $match[1];
351
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
352

353
    $output = $this->renderTestEntity($id);
354
    $expected_link = (string) Link::fromTextAndUrl($value, Url::fromUri($value))->toString();
355
    $this->assertStringContainsString($expected_link, $output);
356

357
    // Verify that a link with text is rendered using the link text.
358
    $title = $this->randomMachineName();
359
    $edit = [
360
      "{$field_name}[0][title]" => $title,
361
    ];
362
    $this->drupalPostForm("entity_test/manage/$id/edit", $edit, t('Save'));
363
    $this->assertText(t('entity_test @id has been updated.', ['@id' => $id]));
364

365
    $output = $this->renderTestEntity($id);
366
    $expected_link = (string) Link::fromTextAndUrl($title, Url::fromUri($value))->toString();
367
    $this->assertStringContainsString($expected_link, $output);
368 369 370 371 372
  }

  /**
   * Tests the default 'link' formatter.
   */
373
  public function testLinkFormatter() {
374
    $field_name = mb_strtolower($this->randomMachineName());
375
    // Create a field with settings to validate.
376
    $this->fieldStorage = FieldStorageConfig::create([
377
      'field_name' => $field_name,
378
      'entity_type' => 'entity_test',
379
      'type' => 'link',
380
      'cardinality' => 3,
381
    ]);
382
    $this->fieldStorage->save();
383
    FieldConfig::create([
384
      'field_storage' => $this->fieldStorage,
385
      'label' => 'Read more about this entity',
386
      'bundle' => 'entity_test',
387
      'settings' => [
388
        'title' => DRUPAL_OPTIONAL,
389
        'link_type' => LinkItemInterface::LINK_GENERIC,
390
      ],
391
    ])->save();
392 393 394
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');
    $display_repository->getFormDisplay('entity_test', 'entity_test')
395
      ->setComponent($field_name, [
396
        'type' => 'link_default',
397
      ])
398
      ->save();
399
    $display_options = [
400 401
      'type' => 'link',
      'label' => 'hidden',
402
    ];
403
    $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
404
      ->setComponent($field_name, $display_options)
405 406
      ->save();

407
    // Create an entity with three link field values:
408
    // - The first field item uses a URL only.
409
    // - The second field item uses a URL and link text.
410
    // - The third field item uses a fragment-only URL with text.
411 412
    // For consistency in assertion code below, the URL is assigned to the title
    // variable for the first field.
413
    $this->drupalGet('entity_test/add');
414 415
    $url1 = 'http://www.example.com/content/articles/archive?author=John&year=2012#com';
    $url2 = 'http://www.example.org/content/articles/archive?author=John&year=2012#org';
416
    $url3 = '#net';
417 418 419
    $title1 = $url1;
    // Intentionally contains an ampersand that needs sanitization on output.
    $title2 = 'A very long & strange example title that could break the nice layout of the site';
420
    $title3 = 'Fragment only';
421
    $edit = [
422
      "{$field_name}[0][uri]" => $url1,
423
      // Note that $title1 is not submitted.
424
      "{$field_name}[0][title]" => '',
425
      "{$field_name}[1][uri]" => $url2,
426
      "{$field_name}[1][title]" => $title2,
427 428
      "{$field_name}[2][uri]" => $url3,
      "{$field_name}[2][title]" => $title3,
429
    ];
430 431
    // Assert label is shown.
    $this->assertText('Read more about this entity');
432
    $this->drupalPostForm(NULL, $edit, t('Save'));
433
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
434
    $id = $match[1];
435
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
436 437 438 439

    // Verify that the link is output according to the formatter settings.
    // Not using generatePermutations(), since that leads to 32 cases, which
    // would not test actual link field formatter functionality but rather
440 441
    // the link generator and options/attributes. Only 'url_plain' has a
    // dependency on 'url_only', so we have a total of ~10 cases.
442 443 444 445 446 447 448 449 450 451 452
    $options = [
      'trim_length' => [NULL, 6],
      'rel' => [NULL, 'nofollow'],
      'target' => [NULL, '_blank'],
      'url_only' => [
        ['url_only' => FALSE],
        ['url_only' => FALSE, 'url_plain' => TRUE],
        ['url_only' => TRUE],
        ['url_only' => TRUE, 'url_plain' => TRUE],
      ],
    ];
453 454 455 456
    foreach ($options as $setting => $values) {
      foreach ($values as $new_value) {
        // Update the field formatter settings.
        if (!is_array($new_value)) {
457
          $display_options['settings'] = [$setting => $new_value];
458 459
        }
        else {
460
          $display_options['settings'] = $new_value;
461
        }
462
        $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
463
          ->setComponent($field_name, $display_options)
464
          ->save();
465

466
        $output = $this->renderTestEntity($id);
467 468 469
        switch ($setting) {
          case 'trim_length':
            $url = $url1;
470
            $title = isset($new_value) ? Unicode::truncate($title1, $new_value, FALSE, TRUE) : $title1;
471
            $this->assertStringContainsString('<a href="' . Html::escape($url) . '">' . Html::escape($title) . '</a>', $output);
472 473

            $url = $url2;
474
            $title = isset($new_value) ? Unicode::truncate($title2, $new_value, FALSE, TRUE) : $title2;
475
            $this->assertStringContainsString('<a href="' . Html::escape($url) . '">' . Html::escape($title) . '</a>', $output);
476 477 478

            $url = $url3;
            $title = isset($new_value) ? Unicode::truncate($title3, $new_value, FALSE, TRUE) : $title3;
479
            $this->assertStringContainsString('<a href="' . Html::escape($url) . '">' . Html::escape($title) . '</a>', $output);
480 481 482 483
            break;

          case 'rel':
            $rel = isset($new_value) ? ' rel="' . $new_value . '"' : '';
484 485 486
            $this->assertStringContainsString('<a href="' . Html::escape($url1) . '"' . $rel . '>' . Html::escape($title1) . '</a>', $output);
            $this->assertStringContainsString('<a href="' . Html::escape($url2) . '"' . $rel . '>' . Html::escape($title2) . '</a>', $output);
            $this->assertStringContainsString('<a href="' . Html::escape($url3) . '"' . $rel . '>' . Html::escape($title3) . '</a>', $output);
487 488 489 490
            break;

          case 'target':
            $target = isset($new_value) ? ' target="' . $new_value . '"' : '';
491 492 493
            $this->assertStringContainsString('<a href="' . Html::escape($url1) . '"' . $target . '>' . Html::escape($title1) . '</a>', $output);
            $this->assertStringContainsString('<a href="' . Html::escape($url2) . '"' . $target . '>' . Html::escape($title2) . '</a>', $output);
            $this->assertStringContainsString('<a href="' . Html::escape($url3) . '"' . $target . '>' . Html::escape($title3) . '</a>', $output);
494 495 496 497 498
            break;

          case 'url_only':
            // In this case, $new_value is an array.
            if (!$new_value['url_only']) {
499 500 501
              $this->assertStringContainsString('<a href="' . Html::escape($url1) . '">' . Html::escape($title1) . '</a>', $output);
              $this->assertStringContainsString('<a href="' . Html::escape($url2) . '">' . Html::escape($title2) . '</a>', $output);
              $this->assertStringContainsString('<a href="' . Html::escape($url3) . '">' . Html::escape($title3) . '</a>', $output);
502 503 504
            }
            else {
              if (empty($new_value['url_plain'])) {
505 506 507
                $this->assertStringContainsString('<a href="' . Html::escape($url1) . '">' . Html::escape($url1) . '</a>', $output);
                $this->assertStringContainsString('<a href="' . Html::escape($url2) . '">' . Html::escape($url2) . '</a>', $output);
                $this->assertStringContainsString('<a href="' . Html::escape($url3) . '">' . Html::escape($url3) . '</a>', $output);
508 509
              }
              else {
510 511 512 513 514 515
                $this->assertStringNotContainsString('<a href="' . Html::escape($url1) . '">' . Html::escape($url1) . '</a>', $output);
                $this->assertStringNotContainsString('<a href="' . Html::escape($url2) . '">' . Html::escape($url2) . '</a>', $output);
                $this->assertStringNotContainsString('<a href="' . Html::escape($url3) . '">' . Html::escape($url3) . '</a>', $output);
                $this->assertStringContainsString(Html::escape($url1), $output);
                $this->assertStringContainsString(Html::escape($url2), $output);
                $this->assertStringContainsString(Html::escape($url3), $output);
516 517 518 519 520 521 522 523 524 525 526 527 528 529
              }
            }
            break;
        }
      }
    }
  }

  /**
   * Tests the 'link_separate' formatter.
   *
   * This test is mostly the same as testLinkFormatter(), but they cannot be
   * merged, since they involve different configuration and output.
   */
530
  public function testLinkSeparateFormatter() {
531
    $field_name = mb_strtolower($this->randomMachineName());
532
    // Create a field with settings to validate.
533
    $this->fieldStorage = FieldStorageConfig::create([
534
      'field_name' => $field_name,
535
      'entity_type' => 'entity_test',
536
      'type' => 'link',
537
      'cardinality' => 3,
538
    ]);
539
    $this->fieldStorage->save();
540
    FieldConfig::create([
541
      'field_storage' => $this->fieldStorage,
542
      'bundle' => 'entity_test',
543
      'settings' => [
544
        'title' => DRUPAL_OPTIONAL,
545
        'link_type' => LinkItemInterface::LINK_GENERIC,
546
      ],
547
    ])->save();
548
    $display_options = [
549 550
      'type' => 'link_separate',
      'label' => 'hidden',
551
    ];
552 553 554
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');
    $display_repository->getFormDisplay('entity_test', 'entity_test')
555
      ->setComponent($field_name, [
556
        'type' => 'link_default',
557
      ])
558
      ->save();
559
    $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
560
      ->setComponent($field_name, $display_options)
561 562
      ->save();

563
    // Create an entity with three link field values:
564
    // - The first field item uses a URL only.
565
    // - The second field item uses a URL and link text.
566
    // - The third field item uses a fragment-only URL with text.
567 568
    // For consistency in assertion code below, the URL is assigned to the title
    // variable for the first field.
569
    $this->drupalGet('entity_test/add');
570 571
    $url1 = 'http://www.example.com/content/articles/archive?author=John&year=2012#com';
    $url2 = 'http://www.example.org/content/articles/archive?author=John&year=2012#org';
572
    $url3 = '#net';
573 574
    // Intentionally contains an ampersand that needs sanitization on output.
    $title2 = 'A very long & strange example title that could break the nice layout of the site';
575
    $title3 = 'Fragment only';
576
    $edit = [
577 578
      "{$field_name}[0][uri]" => $url1,
      "{$field_name}[1][uri]" => $url2,
579
      "{$field_name}[1][title]" => $title2,
580 581
      "{$field_name}[2][uri]" => $url3,
      "{$field_name}[2][title]" => $title3,
582
    ];
583
    $this->drupalPostForm(NULL, $edit, t('Save'));
584
    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
585
    $id = $match[1];
586
    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
587 588

    // Verify that the link is output according to the formatter settings.
589 590 591 592 593
    $options = [
      'trim_length' => [NULL, 6],
      'rel' => [NULL, 'nofollow'],
      'target' => [NULL, '_blank'],
    ];
594 595 596
    foreach ($options as $setting => $values) {
      foreach ($values as $new_value) {
        // Update the field formatter settings.
597
        $display_options['settings'] = [$setting => $new_value];
598
        $display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
599
          ->setComponent($field_name, $display_options)
600
          ->save();
601

602
        $output = $this->renderTestEntity($id);
603 604 605
        switch ($setting) {
          case 'trim_length':
            $url = $url1;
606
            $url_title = isset($new_value) ? Unicode::truncate($url, $new_value, FALSE, TRUE) : $url;
607
            $expected = '<div class="link-item">';
608
            $expected .= '<div class="link-url"><a href="' . Html::escape($url) . '">' . Html::escape($url_title) . '</a></div>';
609
            $expected .= '</div>';
610
            $this->assertStringContainsString($expected, $output);
611 612

            $url = $url2;
613 614
            $url_title = isset($new_value) ? Unicode::truncate($url, $new_value, FALSE, TRUE) : $url;
            $title = isset($new_value) ? Unicode::truncate($title2, $new_value, FALSE, TRUE) : $title2;
615
            $expected = '<div class="link-item">';
616 617
            $expected .= '<div class="link-title">' . Html::escape($title) . '</div>';
            $expected .= '<div class="link-url"><a href="' . Html::escape($url) . '">' . Html::escape($url_title) . '</a></div>';
618
            $expected .= '</div>';
619
            $this->assertStringContainsString($expected, $output);
620 621 622 623 624 625 626 627

            $url = $url3;
            $url_title = isset($new_value) ? Unicode::truncate($url, $new_value, FALSE, TRUE) : $url;
            $title = isset($new_value) ? Unicode::truncate($title3, $new_value, FALSE, TRUE) : $title3;
            $expected = '<div class="link-item">';
            $expected .= '<div class="link-title">' . Html::escape($title) . '</div>';
            $expected .= '<div class="link-url"><a href="' . Html::escape($url) . '">' . Html::escape($url_title) . '</a></div>';
            $expected .= '</div>';
628
            $this->assertStringContainsString($expected, $output);
629 630 631 632
            break;

          case 'rel':
            $rel = isset($new_value) ? ' rel="' . $new_value . '"' : '';
633 634 635
            $this->assertStringContainsString('<div class="link-url"><a href="' . Html::escape($url1) . '"' . $rel . '>' . Html::escape($url1) . '</a></div>', $output);
            $this->assertStringContainsString('<div class="link-url"><a href="' . Html::escape($url2) . '"' . $rel . '>' . Html::escape($url2) . '</a></div>', $output);
            $this->assertStringContainsString('<div class="link-url"><a href="' . Html::escape($url3) . '"' . $rel . '>' . Html::escape($url3) . '</a></div>', $output);
636 637 638 639
            break;

          case 'target':
            $target = isset($new_value) ? ' target="' . $new_value . '"' : '';
640 641 642
            $this->assertStringContainsString('<div class="link-url"><a href="' . Html::escape($url1) . '"' . $target . '>' . Html::escape($url1) . '</a></div>', $output);
            $this->assertStringContainsString('<div class="link-url"><a href="' . Html::escape($url2) . '"' . $target . '>' . Html::escape($url2) . '</a></div>', $output);
            $this->assertStringContainsString('<div class="link-url"><a href="' . Html::escape($url3) . '"' . $target . '>' . Html::escape($url3) . '</a></div>', $output);
643 644 645 646 647 648
            break;
        }
      }
    }
  }

649 650 651 652 653 654 655 656 657 658 659
  /**
   * Test '#link_type' property exists on 'link_default' widget.
   *
   * Make sure the 'link_default' widget exposes a '#link_type' property on
   * its element. Modules can use it to understand if a text form element is
   * a link and also which LinkItemInterface::LINK_* is (EXTERNAL, GENERIC,
   * INTERNAL).
   */
  public function testLinkTypeOnLinkWidget() {

    $link_type = LinkItemInterface::LINK_EXTERNAL;
660
    $field_name = mb_strtolower($this->randomMachineName());
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679

    // Create a field with settings to validate.
    $this->fieldStorage = FieldStorageConfig::create([
      'field_name' => $field_name,
      'entity_type' => 'entity_test',
      'type' => 'link',
      'cardinality' => 1,
    ]);
    $this->fieldStorage->save();
    FieldConfig::create([
      'field_storage' => $this->fieldStorage,
      'label' => 'Read more about this entity',
      'bundle' => 'entity_test',
      'settings' => [
        'title' => DRUPAL_OPTIONAL,
        'link_type' => $link_type,
      ],
    ])->save();

680
    $this->container->get('entity_type.manager')
681 682 683 684 685 686 687 688 689 690 691
      ->getStorage('entity_form_display')
      ->load('entity_test.entity_test.default')
      ->setComponent($field_name, [
        'type' => 'link_default',
      ])
      ->save();

    $form = \Drupal::service('entity.form_builder')->getForm(EntityTest::create());
    $this->assertEqual($form[$field_name]['widget'][0]['uri']['#link_type'], $link_type);
  }

692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708