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