diff --git a/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.info.yml b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.info.yml new file mode 100644 index 0000000000..4d78cc81ff --- /dev/null +++ b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.info.yml @@ -0,0 +1,6 @@ +name: 'Entity reference autocomplete test module' +type: module +description: 'For testing autocomplete functionality' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.libraries.yml b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.libraries.yml new file mode 100644 index 0000000000..0b2c1d4ce4 --- /dev/null +++ b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.libraries.yml @@ -0,0 +1,5 @@ +autocomplete_events: + js: + js/autocomplete_events.js: {} + dependencies: + - core/drupal.autocomplete diff --git a/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.module b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.module new file mode 100644 index 0000000000..5e265ea3db --- /dev/null +++ b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/entity_reference_autocomplete_test.module @@ -0,0 +1,15 @@ + { + /** + * Checks if the argument has the expected properties of the ui object passed + * by jQueryUI's autocomplete events. + * + * @param {object} ui + * An object with the contents of the selected option. + * @return {boolean} + * If the object has properties that match the expected structure. + */ + const uiHasExpectedProperties = ui => { + if (ui.hasOwnProperty('item') && ui.item !== null) { + if (ui.item.hasOwnProperty('value') && ui.item.hasOwnProperty('label')) { + return true; + } + } + return false; + }; + + // Listens to the autocomplete close event. + // Triggered when the suggestions are hidden. + $(document).on('autocompleteclose', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompleteclose-event-happened'); + } + }); + + // Listens to the autocomplete create event. + // Triggered when the autocomplete is created. + $(document).on('autocompletecreate', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompletecreate-event-happened'); + } + }); + + // Listens to the autocomplete focus event. + // Triggered when focus is moved to an item (not selecting). + $(document).on('autocompletefocus', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if (uiHasExpectedProperties(ui)) { + $('body').addClass('autocompletefocus-event-happened'); + } + }); + + // Listens to the autocomplete open event. + // Triggered when the suggestion menu is opened or updated. + $(document).on('autocompleteopen', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompleteopen-event-happened'); + } + }); + + // Listens to the autocomplete response event. + // Triggered after a search completes, before the menu is shown. + $(document).on('autocompleteresponse', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + + // The ui variable has a different structure than it does other events. + // Typically it is null or represents a single selection. In this case it + // is an object containing all suggestions. + if ( + ui.hasOwnProperty('content') && + ui.content.length > 0 && + ui.content[0] !== null && + ui.content[0].hasOwnProperty('value') && + ui.content[0].hasOwnProperty('label') + ) { + $('body').addClass('autocompleteresponse-event-happened'); + } + }); + + // Listens to the autocomplete search event. + // Triggered before a search is performed, after minLength and delay are met. + $(document).on('autocompletesearch', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompletesearch-event-happened'); + } + }); + + // Listens to the autocomplete search event. + // Triggered when an item is selected from the menu. + $(document).on('autocompleteselect', (event, ui) => { + // The event target should be the autocomplete text input. + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if (uiHasExpectedProperties(ui)) { + $('body').addClass('autocompleteselect-event-happened'); + } + }); + + // Listens to the autocomplete change event. + // Triggered when the field is blurred, only if the value has changed. + $(document).on('autocompletechange', (event, ui) => { + // Unlike other listeners in this file, the ui variable is not checked as + // its structure can vary depending on what triggers the event. Any + // listners to this event would not be able to make assumptions about the + // ui variable's structure. + // For similar reasons, event.target is not checked either. + $('body').addClass('autocompletechange-event-happened'); + }); +})(jQuery, Drupal); diff --git a/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/js/autocomplete_events.js b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/js/autocomplete_events.js new file mode 100644 index 0000000000..314789e506 --- /dev/null +++ b/core/modules/entity_reference/tests/modules/entity_reference_autocomplete_test/js/autocomplete_events.js @@ -0,0 +1,85 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal) { + var uiHasExpectedProperties = function uiHasExpectedProperties(ui) { + if (ui.hasOwnProperty('item') && ui.item !== null) { + if (ui.item.hasOwnProperty('value') && ui.item.hasOwnProperty('label')) { + return true; + } + } + return false; + }; + + $(document).on('autocompleteclose', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompleteclose-event-happened'); + } + }); + + $(document).on('autocompletecreate', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompletecreate-event-happened'); + } + }); + + $(document).on('autocompletefocus', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if (uiHasExpectedProperties(ui)) { + $('body').addClass('autocompletefocus-event-happened'); + } + }); + + $(document).on('autocompleteopen', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompleteopen-event-happened'); + } + }); + + $(document).on('autocompleteresponse', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + + if (ui.hasOwnProperty('content') && ui.content.length > 0 && ui.content[0] !== null && ui.content[0].hasOwnProperty('value') && ui.content[0].hasOwnProperty('label')) { + $('body').addClass('autocompleteresponse-event-happened'); + } + }); + + $(document).on('autocompletesearch', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if ($.isEmptyObject(ui)) { + $('body').addClass('autocompletesearch-event-happened'); + } + }); + + $(document).on('autocompleteselect', function (event, ui) { + if (!$(event.target).hasClass('form-autocomplete')) { + return; + } + if (uiHasExpectedProperties(ui)) { + $('body').addClass('autocompleteselect-event-happened'); + } + }); + + $(document).on('autocompletechange', function (event, ui) { + $('body').addClass('autocompletechange-event-happened'); + }); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php b/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php index 582c569015..dc50ba9173 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php @@ -24,7 +24,11 @@ class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'taxonomy']; + public static $modules = [ + 'node', + 'taxonomy', + 'entity_reference_autocomplete_test', + ]; /** * The test vocabulary. @@ -117,39 +121,128 @@ public function testEntityReferenceAutocompleteWidget() { } /** - * Test that spaces and commas work properly with autocomplete fields. + * Test that selecting terms with spaces and commas work properly. + * + * @dataProvider providerTestSeparators */ - public function testSeparators() { + public function testExistingSeparators($term_to_select) { $this->createTagsFieldOnPage(); - $term_commas = 'I,love,commas'; - $term_spaces = 'Just a fan of spaces'; - $term_commas_spaces = 'I dig both commas and spaces, apparently'; + $terms = ['I,love,commas', 'Just a fan of spaces', 'Commas, and spaces']; + $terms_not_selected = array_values(array_diff($terms, [$term_to_select])); - $this->createTerm($this->vocabulary, ['name' => $term_commas]); - $this->createTerm($this->vocabulary, ['name' => $term_spaces]); - $this->createTerm($this->vocabulary, ['name' => $term_commas_spaces]); + $this->createTerm($this->vocabulary, ['name' => $terms[0]]); + $this->createTerm($this->vocabulary, ['name' => $terms[1]]); + $this->createTerm($this->vocabulary, ['name' => $terms[2]]); $this->drupalGet('node/add/page'); $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); $autocomplete_field = $assert_session->waitForElement('css', '[name="taxonomy_reference[target_id]"]'); + $autocomplete_field->setValue('a'); $assert_session->waitOnAutocomplete(); $results = $page->findAll('css', '.ui-autocomplete li'); $this->assertCount(3, $results); - $assert_session->elementExists('css', '.ui-autocomplete li:contains("' . $term_commas_spaces . '")')->click(); - $assert_session->pageTextNotContains($term_commas); - $assert_session->pageTextNotContains($term_spaces); + $assert_session->elementExists('css', '.ui-autocomplete li:contains("' . $term_to_select . '")')->click(); + $assert_session->pageTextNotContains($terms_not_selected[0]); + $assert_session->pageTextNotContains($terms_not_selected[1]); $current_value = $autocomplete_field->getValue(); - $this->assertContains($term_commas_spaces, $current_value); + $this->assertContains($term_to_select, $current_value); + $page->fillField('Title', 'Just a node in a test'); + $page->pressButton('Save'); + + // The term as it appeared in the autocomplete suggestions should be the + // same term on the page. + $assert_session->pageTextContains($term_to_select); } /** - * Test that markup matches that of jQueryUI autocomplete. + * Test that adding terms with spaces and commas work properly. + * + * @dataProvider providerTestSeparators + */ + public function testAddingSeparators($term_to_add) { + $this->createTagsFieldOnPage(); + $this->drupalGet('node/add/page'); + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + // If the term has a comma, it must be enclosed in quotes. + $term_for_input = strpos($term_to_add, ',') !== FALSE ? '"' . $term_to_add . '"' : $term_to_add; + $page->fillField('Tags', $term_for_input); + $page->fillField('Title', 'Just a node in a test'); + $page->pressButton('Save'); + + // Confirm term was added in its expected formatting. + $assert_session->pageTextContains($term_to_add); + + // Confirm term is not wrapped in quotes. + $assert_session->pageTextNotContains('"' . $term_to_add . '"'); + } + + /** + * Data provider for testSeparators(). + */ + public function providerTestSeparators() { + return [ + ['I,love,commas'], + ['Just a fan of spaces'], + ['Commas, and spaces'], + ]; + } + + /** + * Provides data to self::testSetMethod(). + */ + public function providerSetRequestMethod() { + return [ + ['GET'], + ['POST'], + ]; + } + + /** + * Checks that an autocomplete event has triggered. + * + * @param string|array $event + * The autocomplete event/events expected to trigger. + */ + protected function assertEventHappened($event) { + $assert_session = $this->assertSession(); + if (is_array($event)) { + foreach ($event as $an_event) { + $this->assertNotNull($assert_session->waitForElementVisible('css', ".autocomplete$an_event-event-happened")); + } + } + else { + $this->assertNotNull($assert_session->waitForElementVisible('css', ".autocomplete$event-event-happened")); + } + } + + /** + * Checks that an autocomplete event has not triggered. + * + * @param string|array $event + * The autocomplete event/events that should not have triggered. + */ + protected function assertEventNotHappened($event) { + $assert_session = $this->assertSession(); + if (is_array($event)) { + foreach ($event as $an_event) { + $this->assertNull($assert_session->waitForElementVisible('css', ".autocomplete$an_event-event-happened", 5000)); + } + } + else { + $this->assertNull($assert_session->waitForElementVisible('css', ".autocomplete$event-event-happened", 5000)); + } + } + + /** + * Test that markup and events match that of jQueryUI autocomplete. */ public function testMarkup() { $this->createTagsFieldOnPage(); @@ -161,15 +254,19 @@ public function testMarkup() { $this->drupalGet('node/add/page'); $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); + $this->assertNotNull($page->find('css', '.autocompletecreate-event-happened')); - $autocomplete_field = $assert_session->waitForElement('css', '[name="taxonomy_reference[target_id]"]'); + $autocomplete_field = $assert_session->elementExists('css', '[name="taxonomy_reference[target_id]"]'); $this->assertTrue($autocomplete_field->hasClass('form-autocomplete')); $this->assertTrue($autocomplete_field->hasClass('ui-autocomplete-input')); + $this->assertEventNotHappened(['open', 'response', 'search']); + $autocomplete_field->setValue('a'); $assert_session->waitOnAutocomplete(); - $this->assertNull($page->find('css', '.ui-state-active')); - $suggestion_list = $page->find('css', 'body > .ui-autocomplete'); + $this->assertEventHappened(['open', 'response', 'search']); + $this->assertNull($page->find('css', '.ui-state-active')); + $suggestion_list = $page->find('css', '.ui-autocomplete'); foreach (['ui-menu', 'ui-widget', 'ui-widget-content', 'ui-front'] as $class) { $this->assertTrue($suggestion_list->hasClass($class)); } @@ -183,11 +280,27 @@ public function testMarkup() { $this->assertEqual($link->getAttribute('tabindex'), '-1'); } - // Arrow down to the first suggestion and confirm the ui-state-active - // class is added. + $this->assertEventNotHappened('focus'); + // Arrow down to the first suggestion. $autocomplete_field->keyDown($autocomplete_field->getXpath(), 40); $active_selection = $page->find('css', '.ui-menu-item .ui-state-active'); $this->assertNotNull($active_selection); + $this->assertEventHappened('focus'); + $this->assertEventNotHappened(['select', 'close']); + + $active_selection->click(); + + // Wait for suggestions to become hidden -- cant use + // JSWebAssert::assertNoElementAfterWait() because the suggestions are + // still part of the DOM, just hidden. + $condition = "jQuery('.ui-autocomplete:visible').length === 0"; + $this->assertJsCondition($condition); + $this->assertEventHappened(['select', 'close']); + + // The change event only happens when the autocomplete input is blurred. + $this->assertEventNotHappened('change'); + $assert_session->elementExists('css', '[name="title[0][value]"]')->setValue('FORCING A BLUR OF THE AUTOCOMPLETE FIELD'); + $this->assertEventHappened('change'); } /**