diff --git a/core/includes/common.inc b/core/includes/common.inc index 03af03b..7bb70a2 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 d8c5833..22b1221 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 @@ -480,3 +480,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..695ac3a --- /dev/null +++ b/core/modules/system/tests/modules/form_test/src/Form/JavascriptStatesForm.php @@ -0,0 +1,273 @@ + 'textfield', + '#title' => $this->t('Name'), + '#states' => [ + 'invisible' => [ + ':input[name="anonymous"]' => ['checked' => TRUE], + ], + ], + ]; + + // Uncheck anonymous field when the name field is filled. + $form['anonymous'] = [ + '#type' => 'checkbox', + '#title' => $this->t('I prefer to remain anonymous'), + '#default_value' => '1', + '#states' => [ + 'unchecked' => [ + ':input[name="name"]' => ['filled' => TRUE], + ], + ], + ]; + + $form['student_type'] = [ + '#type' => 'radios', + '#options' => [ + 'high_school' => $this->t('High School'), + 'undergraduate' => $this->t('Undergraduate'), + 'graduate' => $this->t('Graduate'), + ], + '#title' => $this->t('What type of student are you?'), + ]; + $form['high_school'] = [ + '#type' => 'fieldset', + '#title' => $this->t('High School Information'), + // This #states rule says that the "high school" fieldset should only + // be shown if the "student_type" form element is set to "High School". + '#states' => [ + 'visible' => [ + ':input[name="student_type"]' => ['value' => 'high_school'], + ], + ], + ]; + + // High school information. + $form['high_school']['tests_taken'] = [ + '#type' => 'checkboxes', + '#options' => array_combine(['SAT', 'ACT'], [t('SAT'), t('ACT')]), + '#title' => $this->t('What standardized tests did you take?'), + // This #states rule says that this checkboxes array will be visible only + // when $form['student_type'] is set to t('High School'). + // It uses the jQuery selector :input[name=student_type] to choose the + // element which triggers the behavior, and then defines the "High School" + // value as the one that triggers visibility. + '#states' => [ + // Action to take. + 'visible' => [ + ':input[name="student_type"]' => ['value' => 'high_school'], + ], + ], + ]; + + $form['high_school']['sat_score'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your SAT score:'), + '#size' => 4, + + // This #states rule limits visibility to when the $form['tests_taken'] + // 'SAT' checkbox is checked." + '#states' => [ + // Action to take. + 'visible' => [ + ':input[name="tests_taken[SAT]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['high_school']['act_score'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your ACT score:'), + '#size' => 4, + + // Set this element visible if the ACT checkbox above is checked. + '#states' => [ + // Action to take. + 'visible' => [ + ':input[name="tests_taken[ACT]"]' => ['checked' => TRUE], + ], + ], + ]; + + // Undergrad information. + $form['undergraduate'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Undergraduate Information'), + // This #states rule says that the "undergraduate" fieldset should only + // be shown if the "student_type" form element is set to "Undergraduate". + '#states' => [ + 'visible' => [ + ':input[name="student_type"]' => ['value' => 'undergraduate'], + ], + ], + ]; + + $form['undergraduate']['how_many_years'] = [ + '#type' => 'select', + '#title' => $this->t('How many years have you completed?'), + // The options here are integers, but since all the action here happens + // using the DOM on the client, we will have to use strings to work with + // them. + '#options' => [ + 1 => $this->t('One'), + 2 => $this->t('Two'), + 3 => $this->t('Three'), + 4 => $this->t('Four'), + 5 => $this->t('Lots'), + ], + ]; + + $form['undergraduate']['comment'] = [ + '#type' => 'item', + '#description' => $this->t("Wow, that's a long time."), + '#states' => [ + 'visible' => [ + // Note that '5' must be used here instead of the integer 5. + // The information is coming from the DOM as a string. + ':input[name="how_many_years"]' => ['value' => '5'], + ], + ], + ]; + $form['undergraduate']['school_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your college or university:'), + ]; + $form['undergraduate']['school_country'] = [ + '#type' => 'select', + '#options' => array_combine(['UK', 'Other'], [t('UK'), t('Other')]), + '#title' => $this->t('In what country is your college or university located?'), + ]; + $form['undergraduate']['country_writein'] = [ + '#type' => 'textfield', + '#size' => 20, + '#title' => $this->t('Please enter the name of the country where your college or university is located.'), + + // Only show this field if school_country is set to 'Other'. + '#states' => [ + // Action to take: Make visible. + 'visible' => [ + ':input[name="school_country"]' => ['value' => $this->t('Other')], + ], + ], + ]; + + $form['undergraduate']['thanks'] = [ + '#type' => 'item', + '#description' => $this->t('Thanks for providing both your school and your country.'), + '#states' => [ + // Here visibility requires that two separate conditions be true. + 'visible' => [ + ':input[name="school_country"]' => ['value' => $this->t('Other')], + ':input[name="country_writein"]' => ['filled' => TRUE], + ], + ], + ]; + $form['undergraduate']['go_away'] = [ + '#type' => 'submit', + '#value' => $this->t('Done with form'), + '#states' => [ + // Here visibility requires that two separate conditions be true. + 'visible' => [ + ':input[name="school_country"]' => ['value' => $this->t('Other')], + ':input[name="country_writein"]' => ['filled' => TRUE], + ], + ], + ]; + + // Graduate student information. + $form['graduate'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Graduate School Information'), + // This #states rule says that the "graduate" fieldset should only + // be shown if the "student_type" form element is set to "Graduate". + '#states' => [ + 'visible' => [ + ':input[name="student_type"]' => ['value' => 'graduate'], + ], + ], + ]; + $form['graduate']['more_info'] = [ + '#type' => 'textarea', + '#title' => $this->t('Please describe your graduate studies'), + ]; + + $form['graduate']['info_provide'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Check here if you have provided information above'), + '#disabled' => TRUE, + '#states' => [ + // Mark this checkbox checked if the "more_info" textarea has something + // in it, if it's 'filled'. + 'checked' => [ + ':input[name="more_info"]' => ['filled' => TRUE], + ], + ], + ]; + + $form['average'] = [ + '#type' => 'textfield', + '#title' => $this->t('Enter your average'), + // To trigger a state when the same controlling element can have more than + // one possible value, put all values in a higher-level array. + '#states' => [ + 'visible' => [ + ':input[name="student_type"]' => [ + ['value' => 'high_school'], + ['value' => 'undergraduate'], + ], + ], + ], + ]; + + $form['expand_more_info'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Check here if you want to add more information.'), + ]; + $form['more_info'] = [ + '#type' => 'details', + '#title' => $this->t('Additional Information'), + // Expand the expand_more_info fieldset if the box is checked. + '#states' => [ + 'expanded' => [ + ':input[name="expand_more_info"]' => ['checked' => TRUE], + ], + ], + ]; + $form['more_info']['feedback'] = [ + '#type' => 'textarea', + '#title' => $this->t('What do you have to say?'), + ]; + 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..994d374 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Form/JavascriptStatesTest.php @@ -0,0 +1,107 @@ +drupalGet('form-test/javascript-states-form'); + $page = $this->getSession()->getPage(); + $web_session = $this->assertSession(); + + $web_session->assertFieldNotVisible('Name'); + + // This should make the name element visible. + $page->uncheckField('I prefer to remain anonymous'); + $web_session->assertFieldVisible('Name'); + + // This should make the name element invisible. + $page->checkField('I prefer to remain anonymous'); + $web_session->assertFieldNotVisible('Name'); + + // This should make the name element visible. + $page->uncheckField('I prefer to remain anonymous'); + $web_session->assertFieldVisible('Name'); + + // Ensure all the details elements are invisible. + $web_session->assertFieldsetNotVisible('High School Information'); + $web_session->assertFieldsetNotVisible('Undergraduate Information'); + $web_session->assertFieldsetNotVisible('Graduate School Information'); + $web_session->assertFieldNotVisible('Enter your average'); + + // High school form testing. + $page->selectFieldOption('edit-student-type-high-school', 'high_school'); + $web_session->assertFieldsetVisible('High School Information'); + $web_session->assertFieldsetNotVisible('Undergraduate Information'); + $web_session->assertFieldsetNotVisible('Graduate School Information'); + $web_session->assertFieldVisible('Enter your average'); + + $web_session->assertFieldNotVisible('Your SAT score:'); + $web_session->assertFieldNotVisible('Your ACT score:'); + $page->checkField('SAT'); + $web_session->assertFieldVisible('Your SAT score:'); + $web_session->assertFieldNotVisible('Your ACT score:'); + $page->checkField('ACT'); + $web_session->assertFieldVisible('Your ACT score:'); + $web_session->assertFieldVisible('Your SAT score:'); + $page->uncheckField('SAT'); + $page->uncheckField('ACT'); + $web_session->assertFieldNotVisible('Your SAT score:'); + $web_session->assertFieldNotVisible('Your ACT score:'); + + // Undergraduate form testing. + $page->selectFieldOption('edit-student-type-undergraduate', 'undergraduate'); + $web_session->assertFieldsetNotVisible('High School Information'); + $web_session->assertFieldsetVisible('Undergraduate Information'); + $web_session->assertFieldsetNotVisible('Graduate School Information'); + $web_session->assertFieldVisible('Enter your average'); + + $comment = $page->findById('edit-comment--description'); + $this->assertFalse($comment->isVisible(), 'Comment item is not visible'); + $page->selectFieldOption('How many years have you completed?', '5'); + $this->assertTrue($comment->isVisible(), 'Comment item is visible'); + $web_session->assertFieldNotVisible('Please enter the name of the country where your college or university is located.'); + $page->selectFieldOption('In what country is your college or university located?', 'Other'); + $web_session->assertFieldVisible('Please enter the name of the country where your college or university is located.'); + $page->selectFieldOption('In what country is your college or university located?', 'UK'); + $web_session->assertFieldNotVisible('Please enter the name of the country where your college or university is located.'); + + // Graduate form testing. + $page->selectFieldOption('edit-student-type-graduate', 'graduate'); + $web_session->assertFieldsetNotVisible('High School Information'); + $web_session->assertFieldsetNotVisible('Undergraduate Information'); + $web_session->assertFieldsetVisible('Graduate School Information'); + $web_session->assertFieldNotVisible('Enter your average'); + + $web_session->checkboxNotChecked('Check here if you have provided information above'); + $page->fillField('Please describe your graduate studies', 'Some text'); + $web_session->checkboxChecked('Check here if you have provided information above'); + $page->fillField('Please describe your graduate studies', ''); + // Empty and filled states are triggered on keyup event. + $page->findField('Please describe your graduate studies')->keyUp(PHP_EOL); + $web_session->checkboxNotChecked('Check here if you have provided information above'); + + // Feedback testing. + $web_session->assertFieldNotVisible('What do you have to say?'); + $page->checkField('Check here if you want to add more information.'); + $web_session->assertFieldVisible('What do you have to say?'); + $page->uncheckField('Check here if you want to add more information.'); + $web_session->assertFieldNotVisible('What do you have to say?'); + } + +} diff --git a/core/tests/Drupal/Tests/WebAssert.php b/core/tests/Drupal/Tests/WebAssert.php index d863de2..1cf82fb 100644 --- a/core/tests/Drupal/Tests/WebAssert.php +++ b/core/tests/Drupal/Tests/WebAssert.php @@ -2,6 +2,9 @@ namespace Drupal\Tests; +use Behat\Mink\Element\Element; +use Behat\Mink\Element\ElementInterface; +use Behat\Mink\Exception\ElementHtmlException; use Behat\Mink\WebAssert as MinkWebAssert; use Behat\Mink\Element\TraversableElement; use Behat\Mink\Exception\ElementNotFoundException; @@ -52,16 +55,152 @@ public function buttonExists($button, TraversableElement $container = NULL) { */ public function selectExists($select, TraversableElement $container = NULL) { $container = $container ?: $this->session->getPage(); - $node = $container->find('named', array( - 'select', - $this->session->getSelectorsHandler()->xpathLiteral($select), - )); - + $node = $container->find('named', ['select', $select]); if ($node === NULL) { throw new ElementNotFoundException($this->session, 'select', 'id|name|label|value', $select); } - return $node; } + /** + * 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->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->assertElementNotVisible('named', ['field', $locator], $container); + } + + /** + * Asserts that the given fieldset 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 assertFieldsetVisible($locator, TraversableElement $container = NULL) { + $this->assertElementVisible('named', ['fieldset', $locator], $container); + } + + /** + * Asserts that the given fieldset 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 assertFieldsetNotVisible($locator, TraversableElement $container = NULL) { + $this->assertElementNotVisible('named', ['fieldset', $locator], $container); + } + /** + * 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 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 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 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); + } + }