diff --git a/core/includes/common.inc b/core/includes/common.inc index 037c5b0..51b717f 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -568,7 +568,7 @@ function drupal_js_defaults($data = NULL) { * @param $elements * A renderable array element having a #states property as described above. * - * @see form_example_states_form() + * @see \Drupal\form_test\Form\JavascriptStatesForm */ function drupal_process_states(&$elements) { $elements['#attached']['library'][] = 'core/drupal.states'; diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml index 2250a0b..5d4e9c6 100644 --- a/core/modules/system/tests/modules/form_test/form_test.routing.yml +++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml @@ -489,3 +489,10 @@ form_test.get_form: _form: '\Drupal\form_test\Form\FormTestGetForm' requirements: _access: 'TRUE' + +form_test.javascript_states_form: + path: '/form-test/javascript-states-form' + defaults: + _form: '\Drupal\form_test\Form\JavascriptStatesForm' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php new file mode 100644 index 0000000..7b166f8 --- /dev/null +++ b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php @@ -0,0 +1,183 @@ + 'checkbox', + '#title' => 'Checkbox trigger', + ]; + $form['textfield_trigger'] = [ + '#type' => 'textfield', + '#title' => 'Textfield trigger', + ]; + $form['radios_trigger'] = [ + '#type' => 'radios', + '#title' => 'Radios trigger', + '#options' => [ + 'value1' => 'Value 1', + 'value2' => 'Value 2', + 'value3' => 'Value 3', + ], + ]; + $form['checkboxes_trigger'] = [ + '#type' => 'checkboxes', + '#title' => 'Checkboxes trigger', + '#options' => [ + 'value1' => 'Value 1', + 'value2' => 'Value 2', + 'value3' => 'Value 3', + ], + ]; + $form['select_trigger'] = [ + '#type' => 'select', + '#title' => 'Select trigger', + '#options' => [ + 'value1' => 'Value 1', + 'value2' => 'Value 2', + 'value3' => 'Value 3', + ], + '#empty_value' => '_none', + '#empty_option' => '- None -', + ]; + + $form['separator'] = [ + '#markup' => '
', + ]; + + // Tested fields. + $form['textfield_invisible_when_checkbox_trigger_checked'] = [ + '#type' => 'textfield', + '#title' => 'Texfield invisible when checkbox trigger checked', + '#states' => [ + 'invisible' => [ + ':input[name="checkbox_trigger"]' => ['checked' => TRUE], + ], + ], + ]; + $form['checkbox_unchecked_when_textfield_trigger_filled'] = [ + '#type' => 'checkbox', + '#title' => 'Checkbox unchecked when textfield trigger filled', + '#default_value' => '1', + '#states' => [ + 'unchecked' => [ + ':input[name="textfield_trigger"]' => ['filled' => TRUE], + ], + ], + ]; + $form['textfield_visible_when_checkboxes_trigger_value2_checked'] = [ + '#type' => 'textfield', + '#title' => 'Texfield visible when checkboxes trigger value2 checked', + '#states' => [ + 'visible' => [ + ':input[name="checkboxes_trigger[value2]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['textfield_visible_when_checkboxes_trigger_value3_checked'] = [ + '#type' => 'textfield', + '#title' => 'Texfield visible when checkboxes trigger value3 checked', + '#states' => [ + 'visible' => [ + ':input[name="checkboxes_trigger[value3]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['item_visible_when_select_trigger_has_given_value'] = [ + '#type' => 'item', + '#title' => 'Item visible when select trigger has given value', + '#states' => [ + 'visible' => [ + ':input[name="select_trigger"]' => ['value' => 'value2'], + ], + ], + ]; + $form['textfield_visible_when_select_trigger_has_given_value'] = [ + '#type' => 'textfield', + '#title' => 'Textfield visible when select trigger has given value', + '#states' => [ + 'visible' => [ + ':input[name="select_trigger"]' => ['value' => 'value3'], + ], + ], + ]; + $form['item_visible_when_select_trigger_has_given_value_and_textfield_trigger_filled'] = [ + '#type' => 'item', + '#title' => 'Item visible when select trigger has given value and textfield trigger filled', + '#states' => [ + 'visible' => [ + ':input[name="select_trigger"]' => ['value' => 'value2'], + ':input[name="textfield_trigger"]' => ['filled' => TRUE], + ], + ], + ]; + $form['textfield_visible_when_select_trigger_has_given_value_or_another'] = [ + '#type' => 'textfield', + '#title' => 'Textfield visible when select trigger has given value or another', + '#states' => [ + 'visible' => [ + ':input[name="select_trigger"]' => [ + ['value' => 'value2'], + ['value' => 'value3'], + ], + ], + ], + ]; + $form['fieldset_visible_when_radios_trigger_has_given_value'] = [ + '#type' => 'fieldset', + '#title' => 'Fieldset visible when radio trigger has given value', + '#states' => [ + 'visible' => [ + ':input[name="radios_trigger"]' => ['value' => 'value2'], + ], + ], + ]; + $form['fieldset_visible_when_radios_trigger_has_given_value']['textfield_in_fieldset'] = [ + '#type' => 'textfield', + '#title' => 'Textfield in fieldset', + ]; + $form['details_expanded_when_checkbox_trigger_checked'] = [ + '#type' => 'details', + '#title' => 'Details expanded when checkbox trigger checked', + '#states' => [ + 'expanded' => [ + ':input[name="checkbox_trigger"]' => ['checked' => TRUE], + ], + ], + ]; + $form['details_expanded_when_checkbox_trigger_checked']['textfield_in_details'] = [ + '#type' => 'textfield', + '#title' => 'Textfield in details', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php new file mode 100644 index 0000000..7d686ae --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php @@ -0,0 +1,271 @@ +drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + $trigger = $page->findField('checkbox_trigger'); + + /* Initial state: before the checkbox trigger is checked. */ + + // Test that the textfield is visible. + $this->assertFieldVisible('textfield_invisible_when_checkbox_trigger_checked'); + // Test that the details element is collapsed so the textfield inside is not + // visible. + $this->assertFieldNotVisible('textfield_in_details'); + + // Change state: check the checkbox. + $trigger->check(); + + // Test that the textfield is not visible anymore. + $this->assertFieldNotVisible('textfield_invisible_when_checkbox_trigger_checked'); + // Test that the details element is now open so the textfield inside is now + // visible. + $this->assertFieldVisible('textfield_in_details'); + + // Change back to the initial state to avoid issues running the next tests. + $trigger->uncheck(); + } + + /** + * Test states of elements triggered by a textfield element. + */ + public function testTextfieldTriggeredElements() { + $this->drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + $trigger = $page->findField('textfield_trigger'); + $target = $page->findField('checkbox_unchecked_when_textfield_trigger_filled'); + + /* Initial state: before the textfield is filled. */ + + // Test that the checkbox is checked. + $this->assertTrue($target->isChecked()); + + // Change state: fill the textfield. + $trigger->setValue('filled'); + + // Test that the checkbox is not checked anymore. + $this->assertFalse($target->isChecked()); + + // Change back to the initial state to avoid issues running the next tests. + $trigger->setValue(''); + // We need to send a backspace hit to trigger JS events. + $trigger->keyUp(8); + } + + /** + * Test states of elements triggered by a radios element. + */ + public function testRadiosTriggeredElements() { + $this->drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + $trigger = $page->findField('radios_trigger'); + + /* Initial state: before the radios is checked. */ + + // Test that the fieldset element is not visible so the textfield inside is + // not visible either. + $this->assertFieldNotVisible('textfield_in_fieldset'); + + // Change state: check the 'Value 2' radio. + $trigger->selectOption('value2'); + + // Test that the fieldset element is now visible so the textfield inside is + // also visible. + $this->assertFieldVisible('textfield_in_fieldset'); + + // Change back to the initial state to avoid issues running the next tests. + $trigger->selectOption('value1'); + } + + /** + * Test states of elements triggered by a checkboxes element. + */ + public function testCheckboxesTriggeredElements() { + $this->drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + $trigger1 = $page->findField('checkboxes_trigger[value1]'); + $trigger2 = $page->findField('checkboxes_trigger[value2]'); + $trigger3 = $page->findField('checkboxes_trigger[value3]'); + + /* Initial state: before any of the checkboxes is checked. */ + + // Test that textfield dependant on checkbox 'value2' is not visible. + $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value2_checked'); + // Test that textfield dependant on checkbox 'value3' is not visible. + $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value3_checked'); + + // Change state: check the 'Value 1' checkbox. + $trigger1->check(); + + // Test that textfield dependant on checkbox 'value2' is still not visible. + $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value2_checked'); + // Test that textfield dependant on checkbox 'value3' is still not visible. + $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value3_checked'); + + // Change state: check the 'Value 2' checkbox. + $trigger2->check(); + + // Test that textfield dependant on checkbox 'value2' is now visible. + $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value2_checked'); + // Test that textfield dependant on checkbox 'value3' is still not visible. + $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value3_checked'); + + // Change state: check the 'Value 3' checkbox. + $trigger3->check(); + + // Test that textfield dependant on checkbox 'value2' is still visible. + $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value2_checked'); + // Test that textfield dependant on checkbox 'value3' is now visible. + $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value3_checked'); + + // Change state: uncheck the 'Value 2' checkbox. + $trigger2->uncheck(); + + // Test that textfield dependant on checkbox 'value2' is now invisible. + $this->assertFieldNotVisible('textfield_visible_when_checkboxes_trigger_value2_checked'); + // Test that textfield dependant on checkbox 'value3' is still visible. + $this->assertFieldVisible('textfield_visible_when_checkboxes_trigger_value3_checked'); + + // Change back to the initial state to avoid issues running the next tests. + $trigger1->uncheck(); + $trigger2->uncheck(); + $trigger3->uncheck(); + } + + /** + * Test states of elements triggered by a select element. + */ + public function testSelectTriggeredElements() { + $this->drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + $trigger = $page->findField('select_trigger'); + + /* Initial state: before any option of the select box is selected. */ + + // Test that item element dependant on select 'Value 2' option is not + // visible. + $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value'); + // Test that textfield dependant on select 'Value 3' option is not visible. + $this->assertFieldNotVisible('textfield_visible_when_select_trigger_has_given_value'); + // Test that textfield dependant on select 'Value 2' or 'Value 3' options is + // not visible. + $this->assertFieldNotVisible('textfield_visible_when_select_trigger_has_given_value_or_another'); + + // Change state: select the 'Value 2' option. + $trigger->setValue('value2'); + + // Test that item element dependant on select 'Value 2' option is now + // visible. + $this->assertSession()->assertElementVisible('css', '#edit-item-visible-when-select-trigger-has-given-value'); + // Test that textfield dependant on select 'Value 3' option is not visible. + $this->assertFieldNotVisible('textfield_visible_when_select_trigger_has_given_value'); + // Test that textfield dependant on select 'Value 2' or 'Value 3' options is + // now visible. + $this->assertFieldVisible('textfield_visible_when_select_trigger_has_given_value_or_another'); + + // Change state: select the 'Value 3' option. + $trigger->setValue('value3'); + + // Test that item element dependant on select 'Value 2' option is not + // visible anymore. + $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value'); + // Test that textfield dependant on select 'Value 3' option is now visible. + $this->assertFieldVisible('textfield_visible_when_select_trigger_has_given_value'); + // Test that textfield dependant on select 'Value 2' or 'Value 3' options is + // still visible. + $this->assertFieldVisible('textfield_visible_when_select_trigger_has_given_value_or_another'); + + // Change back to the initial state to avoid issues running the next tests. + $trigger->setValue('_none'); + } + + /** + * Test states of elements triggered by a more than one element. + */ + public function testMultipleTriggeredElements() { + $this->drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + + $selectTrigger = $page->findField('select_trigger'); + $textfieldTrigger = $page->findField('textfield_trigger'); + + /* Initial state: before any option of the select box is selected. */ + + // Test that item element dependant on select 'Value 2' option and textfield + // is not visible. + $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value-and-textfield-trigger-filled'); + + // Change state: select the 'Value 2' option. + $selectTrigger->setValue('value2'); + + // Test that item element dependant on select 'Value 2' option and textfield + // is still not visible. + $this->assertSession()->assertElementNotVisible('css', '#edit-item-visible-when-select-trigger-has-given-value-and-textfield-trigger-filled'); + + // Change state: fill the textfield. + $textfieldTrigger->setValue('filled'); + + // Test that item element dependant on select 'Value 2' option and textfield + // is now visible. + $this->assertSession()->assertElementVisible('css', '#edit-item-visible-when-select-trigger-has-given-value-and-textfield-trigger-filled'); + + // Change back to the initial state to avoid issues running the next tests. + $selectTrigger->setValue('_none'); + $textfieldTrigger->setValue(''); + // We need to send a backspace hit to trigger JS events. + $textfieldTrigger->keyUp(8); + } + + /** + * Asserts that the given field is visible. + * + * @param string $locator + * One of id|name|label|value for the field. + * @param \Behat\Mink\Element\TraversableElement $container + * (optional) The document to check against. Defaults to the current page. + */ + public function assertFieldVisible($locator, TraversableElement $container = NULL) { + $this->assertSession()->assertElementVisible('named', ['field', $locator], $container); + } + + /** + * Asserts that the given field is not visible. + * + * @param string $locator + * One of id|name|label|value for the field. + * @param \Behat\Mink\Element\TraversableElement $container + * (optional) The document to check against. Defaults to the current page. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * When the element doesn't exist. + */ + public function assertFieldNotVisible($locator, TraversableElement $container = NULL) { + $this->assertSession()->assertElementNotVisible('named', ['field', $locator], $container); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php index 815a26b..a37bfbc 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php @@ -2,6 +2,8 @@ namespace Drupal\FunctionalJavascriptTests; +use Behat\Mink\Element\Element; +use Behat\Mink\Element\ElementInterface; use Behat\Mink\Element\NodeElement; use Behat\Mink\Exception\ElementHtmlException; use Behat\Mink\Exception\ElementNotFoundException; @@ -46,6 +48,94 @@ function isAjaxing(instance) { } /** + * Asserts that the specific element is visible on the current page. + * + * @param string $selector_type + * The element selector type (css, xpath). + * @param string|array $selector + * The element selector. + * @param \Behat\Mink\Element\ElementInterface $container + * The document to check against. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * When the element doesn't exist. + */ + public function assertElementVisible($selector_type, $selector, ElementInterface $container = NULL) { + $container = $container ?: $this->session->getPage(); + $node = $container->find($selector_type, $selector); + if ($node === NULL) { + if (is_array($selector)) { + $selector = implode(' ', $selector); + } + throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector); + } + $message = sprintf( + 'Element "%s" is not visible.', + $this->getMatchingElementRepresentation($selector_type, $selector) + ); + $this->assertElement($node->isVisible(), $message, $node); + } + + /** + * Asserts that the specific element is not visible on the current page. + * + * @param string $selector_type + * The element selector type (css, xpath). + * @param string|array $selector + * The element selector. + * @param \Behat\Mink\Element\ElementInterface $container + * The document to check against. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + * When the element doesn't exist. + */ + public function assertElementNotVisible($selector_type, $selector, ElementInterface $container = NULL) { + $container = $container ?: $this->session->getPage(); + $node = $container->find($selector_type, $selector); + if ($node === NULL) { + if (is_array($selector)) { + $selector = implode(' ', $selector); + } + throw new ElementNotFoundException($this->session->getDriver(), 'element', $selector_type, $selector); + } + $message = sprintf( + 'Element "%s" is not visible.', + $this->getMatchingElementRepresentation($selector_type, $selector) + ); + $this->assertElement(!$node->isVisible(), $message, $node); + } + + /** + * {@inheritdoc} + */ + protected function assertElement($condition, $message, Element $element) { + if ($condition) { + return; + } + + throw new ElementHtmlException($message, $this->session->getDriver(), $element); + } + + /** + * {@inheritdoc} + */ + protected function getMatchingElementRepresentation($selector_type, $selector, $plural = FALSE) { + $pluralization = $plural ? 's' : ''; + + if (in_array($selector_type, ['named', 'named_exact', 'named_partial']) + && is_array($selector) && 2 === count($selector) + ) { + return sprintf('%s%s matching locator "%s"', $selector[0], $pluralization, $selector[1]); + } + + if (is_array($selector)) { + $selector = implode(' ', $selector); + } + + return sprintf('element%s matching %s "%s"', $pluralization, $selector_type, $selector); + } + + /** * Waits for the specified selector and returns it when available. * * @param string $selector