Pull merge.
[yaffs-website] / web / core / modules / field_ui / tests / src / Functional / ManageFieldsFunctionalTest.php
1 <?php
2
3 namespace Drupal\Tests\field_ui\Functional;
4
5 use Drupal\Component\Render\FormattableMarkup;
6 use Drupal\Core\Field\FieldStorageDefinitionInterface;
7 use Drupal\Core\Language\LanguageInterface;
8 use Drupal\field\Entity\FieldConfig;
9 use Drupal\field\Entity\FieldStorageConfig;
10 use Drupal\taxonomy\Entity\Vocabulary;
11 use Drupal\Tests\BrowserTestBase;
12 use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
13 use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
14
15 /**
16  * Tests the Field UI "Manage fields" screen.
17  *
18  * @group field_ui
19  */
20 class ManageFieldsFunctionalTest extends BrowserTestBase {
21
22   use FieldUiTestTrait;
23   use EntityReferenceTestTrait;
24
25   /**
26    * Modules to install.
27    *
28    * @var array
29    */
30   public static $modules = ['node', 'field_ui', 'field_test', 'taxonomy', 'image', 'block'];
31
32   /**
33    * The ID of the custom content type created for testing.
34    *
35    * @var string
36    */
37   protected $contentType;
38
39   /**
40    * The label for a random field to be created for testing.
41    *
42    * @var string
43    */
44   protected $fieldLabel;
45
46   /**
47    * The input name of a random field to be created for testing.
48    *
49    * @var string
50    */
51   protected $fieldNameInput;
52
53   /**
54    * The name of a random field to be created for testing.
55    *
56    * @var string
57    */
58   protected $fieldName;
59
60   /**
61    * {@inheritdoc}
62    */
63   protected function setUp() {
64     parent::setUp();
65
66     $this->drupalPlaceBlock('system_breadcrumb_block');
67     $this->drupalPlaceBlock('local_actions_block');
68     $this->drupalPlaceBlock('local_tasks_block');
69     $this->drupalPlaceBlock('page_title_block');
70
71     // Create a test user.
72     $admin_user = $this->drupalCreateUser(['access content', 'administer content types', 'administer node fields', 'administer node form display', 'administer node display', 'administer taxonomy', 'administer taxonomy_term fields', 'administer taxonomy_term display', 'administer users', 'administer account settings', 'administer user display', 'bypass node access']);
73     $this->drupalLogin($admin_user);
74
75     // Create content type, with underscores.
76     $type_name = strtolower($this->randomMachineName(8)) . '_test';
77     $type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
78     $this->contentType = $type->id();
79
80     // Create random field name with markup to test escaping.
81     $this->fieldLabel = '<em>' . $this->randomMachineName(8) . '</em>';
82     $this->fieldNameInput = strtolower($this->randomMachineName(8));
83     $this->fieldName = 'field_' . $this->fieldNameInput;
84
85     // Create Basic page and Article node types.
86     $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
87     $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
88
89     // Create a vocabulary named "Tags".
90     $vocabulary = Vocabulary::create([
91       'name' => 'Tags',
92       'vid' => 'tags',
93       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
94     ]);
95     $vocabulary->save();
96
97     $handler_settings = [
98       'target_bundles' => [
99         $vocabulary->id() => $vocabulary->id(),
100       ],
101     ];
102     $this->createEntityReferenceField('node', 'article', 'field_' . $vocabulary->id(), 'Tags', 'taxonomy_term', 'default', $handler_settings);
103
104     entity_get_form_display('node', 'article', 'default')
105       ->setComponent('field_' . $vocabulary->id())
106       ->save();
107   }
108
109   /**
110    * Runs the field CRUD tests.
111    *
112    * In order to act on the same fields, and not create the fields over and over
113    * again the following tests create, update and delete the same fields.
114    */
115   public function testCRUDFields() {
116     $this->manageFieldsPage();
117     $this->createField();
118     $this->updateField();
119     $this->addExistingField();
120     $this->cardinalitySettings();
121     $this->fieldListAdminPage();
122     $this->deleteField();
123     $this->addPersistentFieldStorage();
124   }
125
126   /**
127    * Tests the manage fields page.
128    *
129    * @param string $type
130    *   (optional) The name of a content type.
131    */
132   public function manageFieldsPage($type = '') {
133     $type = empty($type) ? $this->contentType : $type;
134     $this->drupalGet('admin/structure/types/manage/' . $type . '/fields');
135     // Check all table columns.
136     $table_headers = [
137       t('Label'),
138       t('Machine name'),
139       t('Field type'),
140       t('Operations'),
141     ];
142     foreach ($table_headers as $table_header) {
143       // We check that the label appear in the table headings.
144       $this->assertRaw($table_header . '</th>', format_string('%table_header table header was found.', ['%table_header' => $table_header]));
145     }
146
147     // Test the "Add field" action link.
148     $this->assertLink('Add field');
149
150     // Assert entity operations for all fields.
151     $number_of_links = 3;
152     $number_of_links_found = 0;
153     $operation_links = $this->xpath('//ul[@class = "dropbutton"]/li/a');
154     $url = base_path() . "admin/structure/types/manage/$type/fields/node.$type.body";
155
156     foreach ($operation_links as $link) {
157       switch ($link->getAttribute('title')) {
158         case 'Edit field settings.':
159           $this->assertIdentical($url, $link->getAttribute('href'));
160           $number_of_links_found++;
161           break;
162         case 'Edit storage settings.':
163           $this->assertIdentical("$url/storage", $link->getAttribute('href'));
164           $number_of_links_found++;
165           break;
166         case 'Delete field.':
167           $this->assertIdentical("$url/delete", $link->getAttribute('href'));
168           $number_of_links_found++;
169           break;
170       }
171     }
172
173     $this->assertEqual($number_of_links, $number_of_links_found);
174   }
175
176   /**
177    * Tests adding a new field.
178    *
179    * @todo Assert properties can be set in the form and read back in
180    * $field_storage and $fields.
181    */
182   public function createField() {
183     // Create a test field.
184     $this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
185   }
186
187   /**
188    * Tests editing an existing field.
189    */
190   public function updateField() {
191     $field_id = 'node.' . $this->contentType . '.' . $this->fieldName;
192     // Go to the field edit page.
193     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id . '/storage');
194     $this->assertEscaped($this->fieldLabel);
195
196     // Populate the field settings with new settings.
197     $string = 'updated dummy test string';
198     $edit = [
199       'settings[test_field_storage_setting]' => $string,
200     ];
201     $this->drupalPostForm(NULL, $edit, t('Save field settings'));
202
203     // Go to the field edit page.
204     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
205     $edit = [
206       'settings[test_field_setting]' => $string,
207     ];
208     $this->assertText(t('Default value'), 'Default value heading is shown');
209     $this->drupalPostForm(NULL, $edit, t('Save settings'));
210
211     // Assert the field settings are correct.
212     $this->assertFieldSettings($this->contentType, $this->fieldName, $string);
213
214     // Assert redirection back to the "manage fields" page.
215     $this->assertUrl('admin/structure/types/manage/' . $this->contentType . '/fields');
216   }
217
218   /**
219    * Tests adding an existing field in another content type.
220    */
221   public function addExistingField() {
222     // Check "Re-use existing field" appears.
223     $this->drupalGet('admin/structure/types/manage/page/fields/add-field');
224     $this->assertRaw(t('Re-use an existing field'), '"Re-use existing field" was found.');
225
226     // Check that fields of other entity types (here, the 'comment_body' field)
227     // do not show up in the "Re-use existing field" list.
228     $this->assertFalse($this->xpath('//select[@id="edit-existing-storage-name"]//option[@value="comment"]'), 'The list of options respects entity type restrictions.');
229     // Validate the FALSE assertion above by also testing a valid one.
230     $this->assertTrue($this->xpath('//select[@id="edit-existing-storage-name"]//option[@value=:field_name]', [':field_name' => $this->fieldName]), 'The list of options shows a valid option.');
231
232     // Add a new field based on an existing field.
233     $this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->fieldName, $this->fieldLabel . '_2');
234   }
235
236   /**
237    * Tests the cardinality settings of a field.
238    *
239    * We do not test if the number can be submitted with anything else than a
240    * numeric value. That is tested already in FormTest::testNumber().
241    */
242   public function cardinalitySettings() {
243     $field_edit_path = 'admin/structure/types/manage/article/fields/node.article.body/storage';
244
245     // Assert the cardinality other field cannot be empty when cardinality is
246     // set to 'number'.
247     $edit = [
248       'cardinality' => 'number',
249       'cardinality_number' => '',
250     ];
251     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
252     $this->assertText('Number of values is required.');
253
254     // Submit a custom number.
255     $edit = [
256       'cardinality' => 'number',
257       'cardinality_number' => 6,
258     ];
259     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
260     $this->assertText('Updated field Body field settings.');
261     $this->drupalGet($field_edit_path);
262     $this->assertFieldByXPath("//select[@name='cardinality']", 'number');
263     $this->assertFieldByXPath("//input[@name='cardinality_number']", 6);
264
265     // Check that tabs displayed.
266     $this->assertLink(t('Edit'));
267     $this->assertLinkByHref('admin/structure/types/manage/article/fields/node.article.body');
268     $this->assertLink(t('Field settings'));
269     $this->assertLinkByHref($field_edit_path);
270
271     // Add two entries in the body.
272     $edit = ['title[0][value]' => 'Cardinality', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2'];
273     $this->drupalPostForm('node/add/article', $edit, 'Save');
274
275     // Assert that you can't set the cardinality to a lower number than the
276     // highest delta of this field.
277     $edit = [
278       'cardinality' => 'number',
279       'cardinality_number' => 1,
280     ];
281     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
282     $this->assertRaw(t('There is @count entity with @delta or more values in this field.', ['@count' => 1, '@delta' => 2]), 'Correctly failed to set cardinality lower than highest delta.');
283
284     // Create a second entity with three values.
285     $edit = ['title[0][value]' => 'Cardinality 3', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2', 'body[2][value]' => 'Body 3'];
286     $this->drupalPostForm('node/add/article', $edit, 'Save');
287
288     // Set to unlimited.
289     $edit = [
290       'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
291     ];
292     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
293     $this->assertText('Updated field Body field settings.');
294     $this->drupalGet($field_edit_path);
295     $this->assertFieldByXPath("//select[@name='cardinality']", FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
296     $this->assertFieldByXPath("//input[@name='cardinality_number']", 1);
297
298     // Assert that you can't set the cardinality to a lower number then the
299     // highest delta of this field but can set it to the same.
300     $edit = [
301       'cardinality' => 'number',
302       'cardinality_number' => 1,
303     ];
304     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
305     $this->assertRaw(t('There are @count entities with @delta or more values in this field.', ['@count' => 2, '@delta' => 2]), 'Correctly failed to set cardinality lower than highest delta.');
306
307     $edit = [
308       'cardinality' => 'number',
309       'cardinality_number' => 2,
310     ];
311     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
312     $this->assertRaw(t('There is @count entity with @delta or more values in this field.', ['@count' => 1, '@delta' => 3]), 'Correctly failed to set cardinality lower than highest delta.');
313
314     $edit = [
315       'cardinality' => 'number',
316       'cardinality_number' => 3,
317     ];
318     $this->drupalPostForm($field_edit_path, $edit, t('Save field settings'));
319   }
320
321   /**
322    * Tests deleting a field from the field edit form.
323    */
324   protected function deleteField() {
325     // Delete the field.
326     $field_id = 'node.' . $this->contentType . '.' . $this->fieldName;
327     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
328     $this->clickLink(t('Delete'));
329     $this->assertResponse(200);
330   }
331
332   /**
333    * Tests that persistent field storage appears in the field UI.
334    */
335   protected function addPersistentFieldStorage() {
336     $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
337     // Persist the field storage even if there are no fields.
338     $field_storage->set('persist_with_no_fields', TRUE)->save();
339     // Delete all instances of the field.
340     foreach ($field_storage->getBundles() as $node_type) {
341       // Delete all the body field instances.
342       $this->drupalGet('admin/structure/types/manage/' . $node_type . '/fields/node.' . $node_type . '.' . $this->fieldName);
343       $this->clickLink(t('Delete'));
344       $this->drupalPostForm(NULL, [], t('Delete'));
345     }
346     // Check "Re-use existing field" appears.
347     $this->drupalGet('admin/structure/types/manage/page/fields/add-field');
348     $this->assertRaw(t('Re-use an existing field'), '"Re-use existing field" was found.');
349
350     // Ensure that we test with a label that contains HTML.
351     $label = $this->randomString(4) . '<br/>' . $this->randomString(4);
352     // Add a new field for the orphaned storage.
353     $this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->fieldName, $label);
354   }
355
356   /**
357    * Asserts field settings are as expected.
358    *
359    * @param $bundle
360    *   The bundle name for the field.
361    * @param $field_name
362    *   The field name for the field.
363    * @param $string
364    *   The settings text.
365    * @param $entity_type
366    *   The entity type for the field.
367    */
368   public function assertFieldSettings($bundle, $field_name, $string = 'dummy test string', $entity_type = 'node') {
369     // Assert field storage settings.
370     $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
371     $this->assertTrue($field_storage->getSetting('test_field_storage_setting') == $string, 'Field storage settings were found.');
372
373     // Assert field settings.
374     $field = FieldConfig::loadByName($entity_type, $bundle, $field_name);
375     $this->assertTrue($field->getSetting('test_field_setting') == $string, 'Field settings were found.');
376   }
377
378   /**
379    * Tests that the 'field_prefix' setting works on Field UI.
380    */
381   public function testFieldPrefix() {
382     // Change default field prefix.
383     $field_prefix = strtolower($this->randomMachineName(10));
384     $this->config('field_ui.settings')->set('field_prefix', $field_prefix)->save();
385
386     // Create a field input and label exceeding the new maxlength, which is 22.
387     $field_exceed_max_length_label = $this->randomString(23);
388     $field_exceed_max_length_input = $this->randomMachineName(23);
389
390     // Try to create the field.
391     $edit = [
392       'label' => $field_exceed_max_length_label,
393       'field_name' => $field_exceed_max_length_input,
394     ];
395     $this->drupalPostForm('admin/structure/types/manage/' . $this->contentType . '/fields/add-field', $edit, t('Save and continue'));
396     $this->assertText('Machine-readable name cannot be longer than 22 characters but is currently 23 characters long.');
397
398     // Create a valid field.
399     $this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
400     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/node.' . $this->contentType . '.' . $field_prefix . $this->fieldNameInput);
401     $this->assertText(format_string('@label settings for @type', ['@label' => $this->fieldLabel, '@type' => $this->contentType]));
402   }
403
404   /**
405    * Tests that default value is correctly validated and saved.
406    */
407   public function testDefaultValue() {
408     // Create a test field storage and field.
409     $field_name = 'test';
410     FieldStorageConfig::create([
411       'field_name' => $field_name,
412       'entity_type' => 'node',
413       'type' => 'test_field',
414     ])->save();
415     $field = FieldConfig::create([
416       'field_name' => $field_name,
417       'entity_type' => 'node',
418       'bundle' => $this->contentType,
419     ]);
420     $field->save();
421
422     entity_get_form_display('node', $this->contentType, 'default')
423       ->setComponent($field_name)
424       ->save();
425
426     $admin_path = 'admin/structure/types/manage/' . $this->contentType . '/fields/' . $field->id();
427     $element_id = "edit-default-value-input-$field_name-0-value";
428     $element_name = "default_value_input[{$field_name}][0][value]";
429     $this->drupalGet($admin_path);
430     $this->assertFieldById($element_id, '', 'The default value widget was empty.');
431
432     // Check that invalid default values are rejected.
433     $edit = [$element_name => '-1'];
434     $this->drupalPostForm($admin_path, $edit, t('Save settings'));
435     $this->assertText("$field_name does not accept the value -1", 'Form validation failed.');
436
437     // Check that the default value is saved.
438     $edit = [$element_name => '1'];
439     $this->drupalPostForm($admin_path, $edit, t('Save settings'));
440     $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
441     $field = FieldConfig::loadByName('node', $this->contentType, $field_name);
442     $this->assertEqual($field->getDefaultValueLiteral(), [['value' => 1]], 'The default value was correctly saved.');
443
444     // Check that the default value shows up in the form
445     $this->drupalGet($admin_path);
446     $this->assertFieldById($element_id, '1', 'The default value widget was displayed with the correct value.');
447
448     // Check that the default value can be emptied.
449     $edit = [$element_name => ''];
450     $this->drupalPostForm(NULL, $edit, t('Save settings'));
451     $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
452     $field = FieldConfig::loadByName('node', $this->contentType, $field_name);
453     $this->assertEqual($field->getDefaultValueLiteral(), [], 'The default value was correctly saved.');
454
455     // Check that the default value can be empty when the field is marked as
456     // required and can store unlimited values.
457     $field_storage = FieldStorageConfig::loadByName('node', $field_name);
458     $field_storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
459     $field_storage->save();
460
461     $this->drupalGet($admin_path);
462     $edit = [
463       'required' => 1,
464     ];
465     $this->drupalPostForm(NULL, $edit, t('Save settings'));
466
467     $this->drupalGet($admin_path);
468     $this->drupalPostForm(NULL, [], t('Save settings'));
469     $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
470     $field = FieldConfig::loadByName('node', $this->contentType, $field_name);
471     $this->assertEqual($field->getDefaultValueLiteral(), [], 'The default value was correctly saved.');
472
473     // Check that the default widget is used when the field is hidden.
474     entity_get_form_display($field->getTargetEntityTypeId(), $field->getTargetBundle(), 'default')
475       ->removeComponent($field_name)->save();
476     $this->drupalGet($admin_path);
477     $this->assertFieldById($element_id, '', 'The default value widget was displayed when field is hidden.');
478   }
479
480   /**
481    * Tests that deletion removes field storages and fields as expected.
482    */
483   public function testDeleteField() {
484     // Create a new field.
485     $bundle_path1 = 'admin/structure/types/manage/' . $this->contentType;
486     $this->fieldUIAddNewField($bundle_path1, $this->fieldNameInput, $this->fieldLabel);
487
488     // Create an additional node type.
489     $type_name2 = strtolower($this->randomMachineName(8)) . '_test';
490     $type2 = $this->drupalCreateContentType(['name' => $type_name2, 'type' => $type_name2]);
491     $type_name2 = $type2->id();
492
493     // Add a field to the second node type.
494     $bundle_path2 = 'admin/structure/types/manage/' . $type_name2;
495     $this->fieldUIAddExistingField($bundle_path2, $this->fieldName, $this->fieldLabel);
496
497     // Delete the first field.
498     $this->fieldUIDeleteField($bundle_path1, "node.$this->contentType.$this->fieldName", $this->fieldLabel, $this->contentType);
499
500     // Check that the field was deleted.
501     $this->assertNull(FieldConfig::loadByName('node', $this->contentType, $this->fieldName), 'Field was deleted.');
502     // Check that the field storage was not deleted
503     $this->assertNotNull(FieldStorageConfig::loadByName('node', $this->fieldName), 'Field storage was not deleted.');
504
505     // Delete the second field.
506     $this->fieldUIDeleteField($bundle_path2, "node.$type_name2.$this->fieldName", $this->fieldLabel, $type_name2);
507
508     // Check that the field was deleted.
509     $this->assertNull(FieldConfig::loadByName('node', $type_name2, $this->fieldName), 'Field was deleted.');
510     // Check that the field storage was deleted too.
511     $this->assertNull(FieldStorageConfig::loadByName('node', $this->fieldName), 'Field storage was deleted.');
512   }
513
514   /**
515    * Tests that Field UI respects disallowed field names.
516    */
517   public function testDisallowedFieldNames() {
518     // Reset the field prefix so we can test properly.
519     $this->config('field_ui.settings')->set('field_prefix', '')->save();
520
521     $label = 'Disallowed field';
522     $edit = [
523       'label' => $label,
524       'new_storage_type' => 'test_field',
525     ];
526
527     // Try with an entity key.
528     $edit['field_name'] = 'title';
529     $bundle_path = 'admin/structure/types/manage/' . $this->contentType;
530     $this->drupalPostForm("$bundle_path/fields/add-field", $edit, t('Save and continue'));
531     $this->assertText(t('The machine-readable name is already in use. It must be unique.'));
532
533     // Try with a base field.
534     $edit['field_name'] = 'sticky';
535     $bundle_path = 'admin/structure/types/manage/' . $this->contentType;
536     $this->drupalPostForm("$bundle_path/fields/add-field", $edit, t('Save and continue'));
537     $this->assertText(t('The machine-readable name is already in use. It must be unique.'));
538   }
539
540   /**
541    * Tests that Field UI respects locked fields.
542    */
543   public function testLockedField() {
544     // Create a locked field and attach it to a bundle. We need to do this
545     // programmatically as there's no way to create a locked field through UI.
546     $field_name = strtolower($this->randomMachineName(8));
547     $field_storage = FieldStorageConfig::create([
548       'field_name' => $field_name,
549       'entity_type' => 'node',
550       'type' => 'test_field',
551       'cardinality' => 1,
552       'locked' => TRUE,
553     ]);
554     $field_storage->save();
555     FieldConfig::create([
556       'field_storage' => $field_storage,
557       'bundle' => $this->contentType,
558     ])->save();
559     entity_get_form_display('node', $this->contentType, 'default')
560       ->setComponent($field_name, [
561         'type' => 'test_field_widget',
562       ])
563       ->save();
564
565     // Check that the links for edit and delete are not present.
566     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields');
567     $locked = $this->xpath('//tr[@id=:field_name]/td[4]', [':field_name' => $field_name]);
568     $this->assertSame('Locked', $locked[0]->getHtml(), 'Field is marked as Locked in the UI');
569     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/node.' . $this->contentType . '.' . $field_name . '/delete');
570     $this->assertResponse(403);
571   }
572
573   /**
574    * Tests that Field UI respects the 'no_ui' flag in the field type definition.
575    */
576   public function testHiddenFields() {
577     // Check that the field type is not available in the 'add new field' row.
578     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field');
579     $this->assertFalse($this->xpath('//select[@id="edit-new-storage-type"]//option[@value="hidden_test_field"]'), "The 'add new field' select respects field types 'no_ui' property.");
580     $this->assertTrue($this->xpath('//select[@id="edit-new-storage-type"]//option[@value="shape"]'), "The 'add new field' select shows a valid option.");
581
582     // Create a field storage and a field programmatically.
583     $field_name = 'hidden_test_field';
584     FieldStorageConfig::create([
585       'field_name' => $field_name,
586       'entity_type' => 'node',
587       'type' => $field_name,
588     ])->save();
589     $field = [
590       'field_name' => $field_name,
591       'bundle' => $this->contentType,
592       'entity_type' => 'node',
593       'label' => t('Hidden field'),
594     ];
595     FieldConfig::create($field)->save();
596     entity_get_form_display('node', $this->contentType, 'default')
597       ->setComponent($field_name)
598       ->save();
599     $this->assertTrue(FieldConfig::load('node.' . $this->contentType . '.' . $field_name), format_string('A field of the field storage %field was created programmatically.', ['%field' => $field_name]));
600
601     // Check that the newly added field appears on the 'Manage Fields'
602     // screen.
603     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields');
604     $this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="hidden-test-field"]//td[1]', $field['label'], 'Field was created and appears in the overview page.');
605
606     // Check that the field does not appear in the 're-use existing field' row
607     // on other bundles.
608     $this->drupalGet('admin/structure/types/manage/page/fields/add-field');
609     $this->assertFalse($this->xpath('//select[@id="edit-existing-storage-name"]//option[@value=:field_name]', [':field_name' => $field_name]), "The 're-use existing field' select respects field types 'no_ui' property.");
610     $this->assertTrue($this->xpath('//select[@id="edit-existing-storage-name"]//option[@value=:field_name]', [':field_name' => 'field_tags']), "The 're-use existing field' select shows a valid option.");
611
612     // Check that non-configurable fields are not available.
613     $field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
614     foreach ($field_types as $field_type => $definition) {
615       if (empty($definition['no_ui'])) {
616         $this->assertTrue($this->xpath('//select[@id="edit-new-storage-type"]//option[@value=:field_type]', [':field_type' => $field_type]), new FormattableMarkup('Configurable field type @field_type is available.', ['@field_type' => $field_type]));
617       }
618       else {
619         $this->assertFalse($this->xpath('//select[@id="edit-new-storage-type"]//option[@value=:field_type]', [':field_type' => $field_type]), new FormattableMarkup('Non-configurable field type @field_type is not available.', ['@field_type' => $field_type]));
620       }
621     }
622   }
623
624   /**
625    * Tests that a duplicate field name is caught by validation.
626    */
627   public function testDuplicateFieldName() {
628     // field_tags already exists, so we're expecting an error when trying to
629     // create a new field with the same name.
630     $edit = [
631       'field_name' => 'tags',
632       'label' => $this->randomMachineName(),
633       'new_storage_type' => 'entity_reference',
634     ];
635     $url = 'admin/structure/types/manage/' . $this->contentType . '/fields/add-field';
636     $this->drupalPostForm($url, $edit, t('Save and continue'));
637
638     $this->assertText(t('The machine-readable name is already in use. It must be unique.'));
639     $this->assertUrl($url, [], 'Stayed on the same page.');
640   }
641
642   /**
643    * Tests that external URLs in the 'destinations' query parameter are blocked.
644    */
645   public function testExternalDestinations() {
646     $options = [
647       'query' => ['destinations' => ['http://example.com']],
648     ];
649     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.body/storage', [], 'Save field settings', $options);
650     // The external redirect should not fire.
651     $this->assertUrl('admin/structure/types/manage/article/fields/node.article.body/storage', $options);
652     $this->assertResponse(200);
653     $this->assertRaw('Attempt to update field <em class="placeholder">Body</em> failed: <em class="placeholder">The internal path component &#039;http://example.com&#039; is external. You are not allowed to specify an external URL together with internal:/.</em>.');
654   }
655
656   /**
657    * Tests that deletion removes field storages and fields as expected for a term.
658    */
659   public function testDeleteTaxonomyField() {
660     // Create a new field.
661     $bundle_path = 'admin/structure/taxonomy/manage/tags/overview';
662
663     $this->fieldUIAddNewField($bundle_path, $this->fieldNameInput, $this->fieldLabel);
664
665     // Delete the field.
666     $this->fieldUIDeleteField($bundle_path, "taxonomy_term.tags.$this->fieldName", $this->fieldLabel, 'Tags');
667
668     // Check that the field was deleted.
669     $this->assertNull(FieldConfig::loadByName('taxonomy_term', 'tags', $this->fieldName), 'Field was deleted.');
670     // Check that the field storage was deleted too.
671     $this->assertNull(FieldStorageConfig::loadByName('taxonomy_term', $this->fieldName), 'Field storage was deleted.');
672   }
673
674   /**
675    * Tests that help descriptions render valid HTML.
676    */
677   public function testHelpDescriptions() {
678     // Create an image field
679     FieldStorageConfig::create([
680       'field_name' => 'field_image',
681       'entity_type' => 'node',
682       'type' => 'image',
683     ])->save();
684
685     FieldConfig::create([
686       'field_name' => 'field_image',
687       'entity_type' => 'node',
688       'label' => 'Image',
689       'bundle' => 'article',
690     ])->save();
691
692     entity_get_form_display('node', 'article', 'default')->setComponent('field_image')->save();
693
694     $edit = [
695       'description' => '<strong>Test with an upload field.',
696     ];
697     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image', $edit, t('Save settings'));
698
699     // Check that hook_field_widget_form_alter() does believe this is the
700     // default value form.
701     $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_tags');
702     $this->assertText('From hook_field_widget_form_alter(): Default form is true.', 'Default value form in hook_field_widget_form_alter().');
703
704     $edit = [
705       'description' => '<em>Test with a non upload field.',
706     ];
707     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_tags', $edit, t('Save settings'));
708
709     $this->drupalGet('node/add/article');
710     $this->assertRaw('<strong>Test with an upload field.</strong>');
711     $this->assertRaw('<em>Test with a non upload field.</em>');
712   }
713
714   /**
715    * Tests that the field list administration page operates correctly.
716    */
717   public function fieldListAdminPage() {
718     $this->drupalGet('admin/reports/fields');
719     $this->assertText($this->fieldName, 'Field name is displayed in field list.');
720     $this->assertLinkByHref('admin/structure/types/manage/' . $this->contentType . '/fields');
721   }
722
723   /**
724    * Tests the "preconfigured field" functionality.
725    *
726    * @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface
727    */
728   public function testPreconfiguredFields() {
729     $this->drupalGet('admin/structure/types/manage/article/fields/add-field');
730
731     // Check that the preconfigured field option exist alongside the regular
732     // field type option.
733     $this->assertOption('edit-new-storage-type', 'field_ui:test_field_with_preconfigured_options:custom_options');
734     $this->assertOption('edit-new-storage-type', 'test_field_with_preconfigured_options');
735
736     // Add a field with every possible preconfigured value.
737     $this->fieldUIAddNewField(NULL, 'test_custom_options', 'Test label', 'field_ui:test_field_with_preconfigured_options:custom_options');
738     $field_storage = FieldStorageConfig::loadByName('node', 'field_test_custom_options');
739     $this->assertEqual($field_storage->getCardinality(), FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
740     $this->assertEqual($field_storage->getSetting('test_field_storage_setting'), 'preconfigured_storage_setting');
741
742     $field = FieldConfig::loadByName('node', 'article', 'field_test_custom_options');
743     $this->assertTrue($field->isRequired());
744     $this->assertEqual($field->getSetting('test_field_setting'), 'preconfigured_field_setting');
745
746     $form_display = entity_get_form_display('node', 'article', 'default');
747     $this->assertEqual($form_display->getComponent('field_test_custom_options')['type'], 'test_field_widget_multiple');
748     $view_display = entity_get_display('node', 'article', 'default');
749     $this->assertEqual($view_display->getComponent('field_test_custom_options')['type'], 'field_test_multiple');
750     $this->assertEqual($view_display->getComponent('field_test_custom_options')['settings']['test_formatter_setting_multiple'], 'altered dummy test string');
751   }
752
753   /**
754    * Tests the access to non-existent field URLs.
755    */
756   public function testNonExistentFieldUrls() {
757     $field_id = 'node.foo.bar';
758
759     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
760     $this->assertResponse(404);
761
762     $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id . '/storage');
763     $this->assertResponse(404);
764   }
765
766 }