diff --git a/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php b/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php
index 24a9bdb58b..98ddc7cb8b 100644
--- a/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php
+++ b/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php
@@ -64,6 +64,18 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#default_value' => 2,
];
+ $form['duplicate_button'] = [
+ '#type' => 'submit',
+ '#name' => 'duplicate_button',
+ '#value' => 'Duplicate button 1',
+ ];
+
+ $form['duplicate_button_2'] = [
+ '#type' => 'submit',
+ '#name' => 'duplicate_button',
+ '#value' => 'Duplicate button 2',
+ ];
+
$form['save'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
diff --git a/core/modules/user/tests/src/Functional/UserAdminLanguageTest.php b/core/modules/user/tests/src/Functional/UserAdminLanguageTest.php
new file mode 100644
index 0000000000..414f159770
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserAdminLanguageTest.php
@@ -0,0 +1,184 @@
+adminUser = $this->drupalCreateUser(['administer languages', 'access administration pages']);
+ // User to check non-admin access.
+ $this->regularUser = $this->drupalCreateUser();
+ }
+
+ /**
+ * Tests that admin language is not configurable in single language sites.
+ */
+ public function testUserAdminLanguageConfigurationNotAvailableWithOnlyOneLanguage() {
+ $this->drupalLogin($this->adminUser);
+ $this->setLanguageNegotiation();
+ $path = 'user/' . $this->adminUser->id() . '/edit';
+ $this->drupalGet($path);
+ // Ensure administration pages language settings widget is not available.
+ $this->assertNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode'));
+ }
+
+ /**
+ * Tests that admin language negotiation is configurable only if enabled.
+ */
+ public function testUserAdminLanguageConfigurationAvailableWithAdminLanguageNegotiation() {
+ $this->drupalLogin($this->adminUser);
+ $this->addCustomLanguage();
+ $path = 'user/' . $this->adminUser->id() . '/edit';
+
+ // Checks with user administration pages language negotiation disabled.
+ $this->drupalGet($path);
+ // Ensure administration pages language settings widget is not available.
+ $this->assertNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode'));
+
+ // Checks with user administration pages language negotiation enabled.
+ $this->setLanguageNegotiation();
+ $this->drupalGet($path);
+ // Ensure administration pages language settings widget is available.
+ $this->assertNotNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode'));
+ }
+
+ /**
+ * Tests that the admin language is configurable only for administrators.
+ *
+ * If a user has the permission "access administration pages", they should
+ * be able to see the setting to pick the language they want those pages in.
+ *
+ * If a user does not have that permission, it would confusing for them to
+ * have a setting for pages they cannot access, so they should not be able to
+ * set a language for those pages.
+ */
+ public function testUserAdminLanguageConfigurationAvailableIfAdminLanguageNegotiationIsEnabled() {
+ $this->drupalLogin($this->adminUser);
+ // Adds a new language, because with only one language, setting won't show.
+ $this->addCustomLanguage();
+ $this->setLanguageNegotiation();
+ $path = 'user/' . $this->adminUser->id() . '/edit';
+ $this->drupalGet($path);
+ // Ensure administration pages language setting is visible for admin.
+ $this->assertNotNull($this->getSession()->getPage()->findField( 'edit-preferred-admin-langcode'));
+
+ // Ensure administration pages language setting is hidden for non-admins.
+ $this->drupalLogin($this->regularUser);
+ $path = 'user/' . $this->regularUser->id() . '/edit';
+ $this->drupalGet($path);
+ $this->assertNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode'));
+ }
+
+ /**
+ * Tests the actual language negotiation.
+ */
+ public function testActualNegotiation() {
+ $this->drupalLogin($this->adminUser);
+ $this->addCustomLanguage();
+ $this->setLanguageNegotiation();
+
+ // Even though we have admin language negotiation, so long as the user has
+ // no preference set, negotiation will fall back further.
+ $path = 'user/' . $this->adminUser->id() . '/edit';
+ $this->drupalGet($path);
+ $this->assertText('Language negotiation method: language-default');
+ $this->drupalGet('xx/' . $path);
+ $this->assertText('Language negotiation method: language-url');
+
+ // Set a preferred language code for the user.
+ $edit = [];
+ $edit['preferred_admin_langcode'] = 'xx';
+ $this->drupalPostForm($path, $edit, t('Save'));
+
+ // Test negotiation with the URL method first. The admin method will only
+ // be used if the URL method did not match.
+ $this->drupalGet($path);
+ $this->assertText('Language negotiation method: language-user-admin');
+ $this->drupalGet('xx/' . $path);
+ $this->assertText('Language negotiation method: language-url');
+
+ // Test negotiation with the admin language method first. The admin method
+ // will be used at all times.
+ $this->setLanguageNegotiation(TRUE);
+ $this->drupalGet($path);
+ $this->assertText('Language negotiation method: language-user-admin');
+ $this->drupalGet('xx/' . $path);
+ $this->assertText('Language negotiation method: language-user-admin');
+
+ // Unset the preferred language code for the user.
+ $edit = [];
+ $edit['preferred_admin_langcode'] = '';
+ $this->drupalPostForm($path, $edit, t('Save'));
+ $this->drupalGet($path);
+ $this->assertText('Language negotiation method: language-default');
+ $this->drupalGet('xx/' . $path);
+ $this->assertText('Language negotiation method: language-url');
+ }
+
+ /**
+ * Sets the User interface negotiation detection method.
+ *
+ * Enables the "Account preference for administration pages" language
+ * detection method for the User interface language negotiation type.
+ *
+ * @param bool $admin_first
+ * Whether the admin negotiation should be first.
+ */
+ public function setLanguageNegotiation($admin_first = FALSE) {
+ $edit = [
+ 'language_interface[enabled][language-user-admin]' => TRUE,
+ 'language_interface[enabled][language-url]' => TRUE,
+ 'language_interface[weight][language-user-admin]' => ($admin_first ? -12 : -8),
+ 'language_interface[weight][language-url]' => -10,
+ ];
+ $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
+ }
+
+ /**
+ * Helper method for adding a custom language.
+ */
+ public function addCustomLanguage() {
+ $langcode = 'xx';
+ // The English name for the language.
+ $name = $this->randomMachineName(16);
+ $edit = [
+ 'predefined_langcode' => 'custom',
+ 'langcode' => $langcode,
+ 'label' => $name,
+ 'direction' => LanguageInterface::DIRECTION_LTR,
+ ];
+ $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
+ }
+
+}
diff --git a/core/modules/user/tests/src/Functional/UserBlocksTest.php b/core/modules/user/tests/src/Functional/UserBlocksTest.php
new file mode 100644
index 0000000000..6c2ae97609
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserBlocksTest.php
@@ -0,0 +1,124 @@
+adminUser = $this->drupalCreateUser(['administer blocks']);
+ $this->drupalLogin($this->adminUser);
+ $this->drupalPlaceBlock('user_login_block');
+ $this->drupalLogout($this->adminUser);
+ }
+
+ /**
+ * Tests that user login block is hidden from user/login.
+ */
+ public function testUserLoginBlockVisibility() {
+ // Array keyed list where key being the URL address and value being expected
+ // visibility as boolean type.
+ $paths = [
+ 'node' => TRUE,
+ 'user/login' => FALSE,
+ 'user/register' => TRUE,
+ 'user/password' => TRUE,
+ ];
+ foreach ($paths as $path => $expected_visibility) {
+ $this->drupalGet($path);
+ $elements = $this->xpath('//div[contains(@class,"block-user-login-block") and @role="form"]');
+ if ($expected_visibility) {
+ $this->assertTrue(!empty($elements), 'User login block in path "' . $path . '" should be visible');
+ }
+ else {
+ $this->assertTrue(empty($elements), 'User login block in path "' . $path . '" should not be visible');
+ }
+ }
+ }
+
+ /**
+ * Test the user login block.
+ */
+ public function testUserLoginBlock() {
+ // Create a user with some permission that anonymous users lack.
+ $user = $this->drupalCreateUser(['administer permissions']);
+
+ // Log in using the block.
+ $edit = [];
+ $edit['name'] = $user->getUsername();
+ $edit['pass'] = $user->passRaw;
+ $this->drupalPostForm('admin/people/permissions', $edit, t('Log in'));
+ $this->assertNoText(t('User login'), 'Logged in.');
+
+ // Check that we are still on the same page.
+ $this->assertUrl(\Drupal::url('user.admin_permissions', [], ['absolute' => TRUE]), [], 'Still on the same page after login for access denied page');
+
+ // Now, log out and repeat with a non-403 page.
+ $this->drupalLogout();
+ $this->drupalGet('filter/tips');
+ $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER));
+ $this->drupalPostForm(NULL, $edit, t('Log in'));
+ $this->assertNoText(t('User login'), 'Logged in.');
+ $this->assertPattern('!
!', 'Still on the same page after login for allowed page');
+
+ // Log out again and repeat with a non-403 page including query arguments.
+ $this->drupalLogout();
+ $this->drupalGet('filter/tips', ['query' => ['foo' => 'bar']]);
+ $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER));
+ $this->drupalPostForm(NULL, $edit, t('Log in'));
+ $this->assertNoText(t('User login'), 'Logged in.');
+ $this->assertPattern('!!', 'Still on the same page after login for allowed page');
+ $this->assertTrue(strpos($this->getUrl(), '/filter/tips?foo=bar') !== FALSE, 'Correct query arguments are displayed after login');
+
+ // Repeat with different query arguments.
+ $this->drupalLogout();
+ $this->drupalGet('filter/tips', ['query' => ['foo' => 'baz']]);
+ $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER));
+ $this->drupalPostForm(NULL, $edit, t('Log in'));
+ $this->assertNoText(t('User login'), 'Logged in.');
+ $this->assertPattern('!!', 'Still on the same page after login for allowed page');
+ $this->assertTrue(strpos($this->getUrl(), '/filter/tips?foo=baz') !== FALSE, 'Correct query arguments are displayed after login');
+
+ // Check that the user login block is not vulnerable to information
+ // disclosure to third party sites.
+ $this->drupalLogout();
+ $this->drupalPostForm('http://example.com/', $edit, t('Log in'), ['external' => FALSE]);
+ // Check that we remain on the site after login.
+ $this->assertUrl($user->url('canonical', ['absolute' => TRUE]), [], 'Redirected to user profile page after login from the frontpage');
+
+ // Verify that form validation errors are displayed immediately for forms
+ // in blocks and not on subsequent page requests.
+ $this->drupalLogout();
+ $edit = [];
+ $edit['name'] = 'foo';
+ $edit['pass'] = 'invalid password';
+ $this->drupalPostForm('filter/tips', $edit, t('Log in'));
+ $this->assertText(t('Unrecognized username or password. Forgot your password?'));
+ $this->drupalGet('filter/tips');
+ $this->assertNoText(t('Unrecognized username or password. Forgot your password?'));
+ }
+
+}
diff --git a/core/modules/user/tests/src/Functional/UserCreateTest.php b/core/modules/user/tests/src/Functional/UserCreateTest.php
new file mode 100644
index 0000000000..fb7e996e3f
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserCreateTest.php
@@ -0,0 +1,134 @@
+drupalCreateUser(['administer users']);
+ $this->drupalLogin($user);
+
+ $this->assertEqual($user->getCreatedTime(), REQUEST_TIME, 'Creating a user sets default "created" timestamp.');
+ $this->assertEqual($user->getChangedTime(), REQUEST_TIME, 'Creating a user sets default "changed" timestamp.');
+
+ // Create a field.
+ $field_name = 'test_field';
+ FieldStorageConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'user',
+ 'module' => 'image',
+ 'type' => 'image',
+ 'cardinality' => 1,
+ 'locked' => FALSE,
+ 'indexes' => ['target_id' => ['target_id']],
+ 'settings' => [
+ 'uri_scheme' => 'public',
+ ],
+ ])->save();
+
+ FieldConfig::create([
+ 'field_name' => $field_name,
+ 'entity_type' => 'user',
+ 'label' => 'Picture',
+ 'bundle' => 'user',
+ 'description' => t('Your virtual face or picture.'),
+ 'required' => FALSE,
+ 'settings' => [
+ 'file_extensions' => 'png gif jpg jpeg',
+ 'file_directory' => 'pictures',
+ 'max_filesize' => '30 KB',
+ 'alt_field' => 0,
+ 'title_field' => 0,
+ 'max_resolution' => '85x85',
+ 'min_resolution' => '',
+ ],
+ ])->save();
+
+ // Test user creation page for valid fields.
+ $this->drupalGet('admin/people/create');
+ $this->assertFieldbyId('edit-status-0', 0, 'The user status option Blocked exists.', 'User login');
+ $this->assertFieldbyId('edit-status-1', 1, 'The user status option Active exists.', 'User login');
+ $this->assertFieldByXPath('//input[@type="radio" and @id="edit-status-1" and @checked="checked"]', NULL, 'Default setting for user status is active.');
+
+ // Test that browser autocomplete behavior does not occur.
+ $this->assertNoRaw('data-user-info-from-browser', 'Ensure form attribute, data-user-info-from-browser, does not exist.');
+
+ // Test that the password strength indicator displays.
+ $config = $this->config('user.settings');
+
+ $config->set('password_strength', TRUE)->save();
+ $this->drupalGet('admin/people/create');
+ $this->assertRaw(t('Password strength:'), 'The password strength indicator is displayed.');
+
+ $config->set('password_strength', FALSE)->save();
+ $this->drupalGet('admin/people/create');
+ $this->assertNoRaw(t('Password strength:'), 'The password strength indicator is not displayed.');
+
+ // We create two users, notifying one and not notifying the other, to
+ // ensure that the tests work in both cases.
+ foreach ([FALSE, TRUE] as $notify) {
+ $name = $this->randomMachineName();
+ $edit = [
+ 'name' => $name,
+ 'mail' => $this->randomMachineName() . '@example.com',
+ 'pass[pass1]' => $pass = $this->randomString(),
+ 'pass[pass2]' => $pass,
+ 'notify' => $notify,
+ ];
+ $this->drupalPostForm('admin/people/create', $edit, t('Create new account'));
+
+ if ($notify) {
+ $this->assertText(t('A welcome message with further instructions has been emailed to the new user @name.', ['@name' => $edit['name']]), 'User created');
+ $this->assertEqual(count($this->drupalGetMails()), 1, 'Notification email sent');
+ }
+ else {
+ $this->assertText(t('Created a new user account for @name. No email has been sent.', ['@name' => $edit['name']]), 'User created');
+ $this->assertEqual(count($this->drupalGetMails()), 0, 'Notification email not sent');
+ }
+
+ $this->drupalGet('admin/people');
+ $this->assertText($edit['name'], 'User found in list of users');
+ $user = user_load_by_name($name);
+ $this->assertTrue($user->isActive(), 'User is not blocked');
+ }
+
+ // Test that the password '0' is considered a password.
+ // @see https://www.drupal.org/node/2563751.
+ $name = $this->randomMachineName();
+ $edit = [
+ 'name' => $name,
+ 'mail' => $this->randomMachineName() . '@example.com',
+ 'pass[pass1]' => 0,
+ 'pass[pass2]' => 0,
+ 'notify' => FALSE,
+ ];
+ $this->drupalPostForm('admin/people/create', $edit, t('Create new account'));
+ $this->assertText("Created a new user account for $name. No email has been sent");
+ $this->assertNoText('Password field is required');
+ }
+
+}
diff --git a/core/modules/user/tests/src/Functional/UserPasswordResetTest.php b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
new file mode 100644
index 0000000000..fb25317075
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
@@ -0,0 +1,349 @@
+drupalPlaceBlock('system_menu_block:account');
+
+ // Create a user.
+ $account = $this->drupalCreateUser();
+
+ // Activate user by logging in.
+ $this->drupalLogin($account);
+
+ $this->account = User::load($account->id());
+ $this->account->passRaw = $account->passRaw;
+ $this->drupalLogout();
+
+ // Set the last login time that is used to generate the one-time link so
+ // that it is definitely over a second ago.
+ $account->login = REQUEST_TIME - mt_rand(10, 100000);
+ db_update('users_field_data')
+ ->fields(['login' => $account->getLastLoginTime()])
+ ->condition('uid', $account->id())
+ ->execute();
+ }
+
+ /**
+ * Tests password reset functionality.
+ */
+ public function testUserPasswordReset() {
+ // Verify that accessing the password reset form without having the session
+ // variables set results in an access denied message.
+ $this->drupalGet(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()]));
+ $this->assertResponse(403);
+
+ // Try to reset the password for an invalid account.
+ $this->drupalGet('user/password');
+
+ $edit = ['name' => $this->randomMachineName(32)];
+ $this->drupalPostForm(NULL, $edit, t('Submit'));
+
+ $this->assertText(t('@name is not recognized as a username or an email address.', ['@name' => $edit['name']]), 'Validation error message shown when trying to request password for invalid account.');
+ $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), 0, 'No email was sent when requesting a password for an invalid account.');
+
+ // Reset the password by username via the password reset page.
+ $edit['name'] = $this->account->getUsername();
+ $this->drupalPostForm(NULL, $edit, t('Submit'));
+
+ // Verify that the user was sent an email.
+ $this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.');
+ $subject = t('Replacement login information for @username at @site', ['@username' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')]);
+ $this->assertMail('subject', $subject, 'Password reset email subject is correct.');
+
+ $resetURL = $this->getResetURL();
+ $this->drupalGet($resetURL);
+ // Ensure that the current url does not contain the hash and timestamp.
+ $this->assertUrl(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()]));
+
+ $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
+
+ // Ensure the password reset URL is not cached.
+ $this->drupalGet($resetURL);
+ $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
+
+ // Check the one-time login page.
+ $this->assertText($this->account->getUsername(), 'One-time login page contains the correct username.');
+ $this->assertText(t('This login can be used only once.'), 'Found warning about one-time login.');
+ $this->assertTitle(t('Reset password | Drupal'), 'Page title is "Reset password".');
+
+ // Check successful login.
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+ $this->assertLink(t('Log out'));
+ $this->assertTitle(t('@name | @site', ['@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')]), 'Logged in using password reset link.');
+
+ // Make sure the ajax request from uploading a user picture does not
+ // invalidate the reset token.
+ $image = current($this->drupalGetTestFiles('image'));
+ $edit = [
+ 'files[user_picture_0]' => drupal_realpath($image->uri),
+ ];
+ $this->drupalPostForm(NULL, $edit, 'Upload');
+
+ // Change the forgotten password.
+ $password = user_password();
+ $edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.');
+
+ // Verify that the password reset session has been destroyed.
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText(t('Your current password is missing or incorrect; it\'s required to change the Password.'), 'Password needed to make profile changes.');
+
+ // Log out, and try to log in again using the same one-time link.
+ $this->drupalLogout();
+ $this->drupalGet($resetURL);
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+ $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.');
+
+ // Request a new password again, this time using the email address.
+ $this->drupalGet('user/password');
+ // Count email messages before to compare with after.
+ $before = count($this->drupalGetMails(['id' => 'user_password_reset']));
+ $edit = ['name' => $this->account->getEmail()];
+ $this->drupalPostForm(NULL, $edit, t('Submit'));
+ $this->assertTrue( count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before + 1, 'Email sent when requesting password reset using email address.');
+
+ // Visit the user edit page without pass-reset-token and make sure it does
+ // not cause an error.
+ $resetURL = $this->getResetURL();
+ $this->drupalGet($resetURL);
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+ $this->drupalGet('user/' . $this->account->id() . '/edit');
+ $this->assertNoText('Expected user_string to be a string, NULL given');
+ $this->drupalLogout();
+
+ // Create a password reset link as if the request time was 60 seconds older than the allowed limit.
+ $timeout = $this->config('user.settings')->get('password_reset_timeout');
+ $bogus_timestamp = REQUEST_TIME - $timeout - 60;
+ $_uid = $this->account->id();
+ $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp));
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+ $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.');
+
+ // Create a user, block the account, and verify that a login link is denied.
+ $timestamp = REQUEST_TIME - 1;
+ $blocked_account = $this->drupalCreateUser()->block();
+ $blocked_account->save();
+ $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp));
+ $this->assertResponse(403);
+
+ // Verify a blocked user can not request a new password.
+ $this->drupalGet('user/password');
+ // Count email messages before to compare with after.
+ $before = count($this->drupalGetMails(['id' => 'user_password_reset']));
+ $edit = ['name' => $blocked_account->getUsername()];
+ $this->drupalPostForm(NULL, $edit, t('Submit'));
+ $this->assertRaw(t('%name is blocked or has not been activated yet.', ['%name' => $blocked_account->getUsername()]), 'Notified user blocked accounts can not request a new password');
+ $this->assertTrue(count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before, 'No email was sent when requesting password reset for a blocked account');
+
+ // Verify a password reset link is invalidated when the user's email address changes.
+ $this->drupalGet('user/password');
+ $edit = ['name' => $this->account->getUsername()];
+ $this->drupalPostForm(NULL, $edit, t('Submit'));
+ $old_email_reset_link = $this->getResetURL();
+ $this->account->setEmail("1" . $this->account->getEmail());
+ $this->account->save();
+ $this->drupalGet($old_email_reset_link);
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+ $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.');
+
+ // Verify a password reset link will automatically log a user when /login is
+ // appended.
+ $this->drupalGet('user/password');
+ $edit = ['name' => $this->account->getUsername()];
+ $this->drupalPostForm(NULL, $edit, t('Submit'));
+ $reset_url = $this->getResetURL();
+ $this->drupalGet($reset_url . '/login');
+ $this->assertLink(t('Log out'));
+ $this->assertTitle(t('@name | @site', ['@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')]), 'Logged in using password reset link.');
+
+ // Ensure blocked and deleted accounts can't access the user.reset.login
+ // route.
+ $this->drupalLogout();
+ $timestamp = REQUEST_TIME - 1;
+ $blocked_account = $this->drupalCreateUser()->block();
+ $blocked_account->save();
+ $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
+ $this->assertResponse(403);
+
+ $blocked_account->delete();
+ $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
+ $this->assertResponse(403);
+ }
+
+ /**
+ * Retrieves password reset email and extracts the login link.
+ */
+ public function getResetURL() {
+ // Assume the most recent email.
+ $_emails = $this->drupalGetMails();
+ $email = end($_emails);
+ $urls = [];
+ preg_match('#.+user/reset/.+#', $email['body'], $urls);
+
+ return $urls[0];
+ }
+
+ /**
+ * Test user password reset while logged in.
+ */
+ public function testUserPasswordResetLoggedIn() {
+ $another_account = $this->drupalCreateUser();
+ $this->drupalLogin($another_account);
+ $this->drupalGet('user/password');
+ $this->drupalPostForm(NULL, NULL, t('Submit'));
+
+ // Click the reset URL while logged and change our password.
+ $resetURL = $this->getResetURL();
+ // Log in as a different user.
+ $this->drupalLogin($this->account);
+ $this->drupalGet($resetURL);
+ $this->assertRaw(new FormattableMarkup(
+ 'Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please log out and try using the link again.',
+ ['%other_user' => $this->account->getUsername(), '%resetting_user' => $another_account->getUsername(), ':logout' => Url::fromRoute('user.logout')->toString()]
+ ));
+
+ $another_account->delete();
+ $this->drupalGet($resetURL);
+ $this->assertText('The one-time login link you clicked is invalid.');
+
+ // Log in.
+ $this->drupalLogin($this->account);
+
+ // Reset the password by username via the password reset page.
+ $this->drupalGet('user/password');
+ $this->drupalPostForm(NULL, NULL, t('Submit'));
+
+ // Click the reset URL while logged and change our password.
+ $resetURL = $this->getResetURL();
+ $this->drupalGet($resetURL);
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+
+ // Change the password.
+ $password = user_password();
+ $edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertText(t('The changes have been saved.'), 'Password changed.');
+
+ // Logged in users should not be able to access the user.reset.login or the
+ // user.reset.form routes.
+ $timestamp = REQUEST_TIME - 1;
+ $this->drupalGet("user/reset/" . $this->account->id() . "/$timestamp/" . user_pass_rehash($this->account, $timestamp) . '/login');
+ $this->assertResponse(403);
+ $this->drupalGet("user/reset/" . $this->account->id());
+ $this->assertResponse(403);
+ }
+
+ /**
+ * Prefill the text box on incorrect login via link to password reset page.
+ */
+ public function testUserResetPasswordTextboxFilled() {
+ $this->drupalGet('user/login');
+ $edit = [
+ 'name' => $this->randomMachineName(),
+ 'pass' => $this->randomMachineName(),
+ ];
+ $this->drupalPostForm('user/login', $edit, t('Log in'));
+ $this->assertRaw(t('Unrecognized username or password. Forgot your password?',
+ [':password' => \Drupal::url('user.pass', [], ['query' => ['name' => $edit['name']]])]));
+ unset($edit['pass']);
+ $this->drupalGet('user/password', ['query' => ['name' => $edit['name']]]);
+ $this->assertFieldByName('name', $edit['name'], 'User name found.');
+ // Ensure the name field value is not cached.
+ $this->drupalGet('user/password');
+ $this->assertNoFieldByName('name', $edit['name'], 'User name not found.');
+ }
+
+ /**
+ * Make sure that users cannot forge password reset URLs of other users.
+ */
+ public function testResetImpersonation() {
+ // Create two identical user accounts except for the user name. They must
+ // have the same empty password, so we can't use $this->drupalCreateUser().
+ $edit = [];
+ $edit['name'] = $this->randomMachineName();
+ $edit['mail'] = $edit['name'] . '@example.com';
+ $edit['status'] = 1;
+ $user1 = User::create($edit);
+ $user1->save();
+
+ $edit['name'] = $this->randomMachineName();
+ $user2 = User::create($edit);
+ $user2->save();
+
+ // Unique password hashes are automatically generated, the only way to
+ // change that is to update it directly in the database.
+ db_update('users_field_data')
+ ->fields(['pass' => NULL])
+ ->condition('uid', [$user1->id(), $user2->id()], 'IN')
+ ->execute();
+ \Drupal::entityManager()->getStorage('user')->resetCache();
+ $user1 = User::load($user1->id());
+ $user2 = User::load($user2->id());
+
+ $this->assertEqual($user1->getPassword(), $user2->getPassword(), 'Both users have the same password hash.');
+
+ // The password reset URL must not be valid for the second user when only
+ // the user ID is changed in the URL.
+ $reset_url = user_pass_reset_url($user1);
+ $attack_reset_url = str_replace("user/reset/{$user1->id()}", "user/reset/{$user2->id()}", $reset_url);
+ $this->drupalGet($attack_reset_url);
+ $this->drupalPostForm(NULL, NULL, t('Log in'));
+ $this->assertNoText($user2->getUsername(), 'The invalid password reset page does not show the user name.');
+ $this->assertUrl('user/password', [], 'The user is redirected to the password reset request page.');
+ $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
+ }
+
+}
diff --git a/core/modules/user/tests/src/Functional/UserRegistrationTest.php b/core/modules/user/tests/src/Functional/UserRegistrationTest.php
new file mode 100644
index 0000000000..2ea037b1b8
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserRegistrationTest.php
@@ -0,0 +1,282 @@
+config('user.settings');
+ // Require email verification.
+ $config->set('verify_mail', TRUE)->save();
+
+ // Set registration to administrator only.
+ $config->set('register', USER_REGISTER_ADMINISTRATORS_ONLY)->save();
+ $this->drupalGet('user/register');
+ $this->assertResponse(403, 'Registration page is inaccessible when only administrators can create accounts.');
+
+ // Allow registration by site visitors without administrator approval.
+ $config->set('register', USER_REGISTER_VISITORS)->save();
+ $edit = [];
+ $edit['name'] = $name = $this->randomMachineName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertText(t('A welcome message with further instructions has been sent to your email address.'), 'User registered successfully.');
+
+ /** @var EntityStorageInterface $storage */
+ $storage = $this->container->get('entity_type.manager')->getStorage('user');
+ $accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]);
+ $new_user = reset($accounts);
+ $this->assertTrue($new_user->isActive(), 'New account is active after registration.');
+ $resetURL = user_pass_reset_url($new_user);
+ $this->drupalGet($resetURL);
+ $this->assertTitle(t('Set password | Drupal'), 'Page title is "Set password".');
+
+ // Allow registration by site visitors, but require administrator approval.
+ $config->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save();
+ $edit = [];
+ $edit['name'] = $name = $this->randomMachineName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->container->get('entity.manager')->getStorage('user')->resetCache();
+ $accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]);
+ $new_user = reset($accounts);
+ $this->assertFalse($new_user->isActive(), 'New account is blocked until approved by an administrator.');
+ }
+
+ public function testRegistrationWithoutEmailVerification() {
+ $config = $this->config('user.settings');
+ // Don't require email verification and allow registration by site visitors
+ // without administrator approval.
+ $config
+ ->set('verify_mail', FALSE)
+ ->set('register', USER_REGISTER_VISITORS)
+ ->save();
+
+ $edit = [];
+ $edit['name'] = $name = $this->randomMachineName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+
+ // Try entering a mismatching password.
+ $edit['pass[pass1]'] = '99999.0';
+ $edit['pass[pass2]'] = '99999';
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertText(t('The specified passwords do not match.'), 'Typing mismatched passwords displays an error message.');
+
+ // Enter a correct password.
+ $edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
+ $edit['pass[pass2]'] = $new_pass;
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->container->get('entity.manager')->getStorage('user')->resetCache();
+ $accounts = $this->container->get('entity_type.manager')->getStorage('user')
+ ->loadByProperties(['name' => $name, 'mail' => $mail]);
+ $new_user = reset($accounts);
+ $this->assertNotNull($new_user, 'New account successfully created with matching passwords.');
+ $this->assertText(t('Registration successful. You are now logged in.'), 'Users are logged in after registering.');
+ $this->drupalLogout();
+
+ // Allow registration by site visitors, but require administrator approval.
+ $config->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save();
+ $edit = [];
+ $edit['name'] = $name = $this->randomMachineName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $edit['pass[pass1]'] = $pass = $this->randomMachineName();
+ $edit['pass[pass2]'] = $pass;
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertText(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.'), 'Users are notified of pending approval');
+
+ // Try to log in before administrator approval.
+ $auth = [
+ 'name' => $name,
+ 'pass' => $pass,
+ ];
+ $this->drupalPostForm('user/login', $auth, t('Log in'));
+ $this->assertText(t('The username @name has not been activated or is blocked.', ['@name' => $name]), 'User cannot log in yet.');
+
+ // Activate the new account.
+ $accounts = $this->container->get('entity_type.manager')->getStorage('user')
+ ->loadByProperties(['name' => $name, 'mail' => $mail]);
+ $new_user = reset($accounts);
+ $admin_user = $this->drupalCreateUser(['administer users']);
+ $this->drupalLogin($admin_user);
+ $edit = [
+ 'status' => 1,
+ ];
+ $this->drupalPostForm('user/' . $new_user->id() . '/edit', $edit, t('Save'));
+ $this->drupalLogout();
+
+ // Log in after administrator approval.
+ $this->drupalPostForm('user/login', $auth, t('Log in'));
+ $this->assertText(t('Member for'), 'User can log in after administrator approval.');
+ }
+
+ public function testRegistrationEmailDuplicates() {
+ // Don't require email verification and allow registration by site visitors
+ // without administrator approval.
+ $this->config('user.settings')
+ ->set('verify_mail', FALSE)
+ ->set('register', USER_REGISTER_VISITORS)
+ ->save();
+
+ // Set up a user to check for duplicates.
+ $duplicate_user = $this->drupalCreateUser();
+
+ $edit = [];
+ $edit['name'] = $this->randomMachineName();
+ $edit['mail'] = $duplicate_user->getEmail();
+
+ // Attempt to create a new account using an existing email address.
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertText(t('The email address @email is already taken.', ['@email' => $duplicate_user->getEmail()]), 'Supplying an exact duplicate email address displays an error message');
+
+ // Attempt to bypass duplicate email registration validation by adding spaces.
+ $edit['mail'] = ' ' . $duplicate_user->getEmail() . ' ';
+
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertText(t('The email address @email is already taken.', ['@email' => $duplicate_user->getEmail()]), 'Supplying a duplicate email address with added whitespace displays an error message');
+ }
+
+ /**
+ * Tests that UUID isn't cached in form state on register form.
+ *
+ * This is a regression test for https://www.drupal.org/node/2500527 to ensure
+ * that the form is not cached on GET requests.
+ */
+ public function testUuidFormState() {
+ \Drupal::service('module_installer')->install(['image']);
+ \Drupal::service('router.builder')->rebuild();
+
+ // Add a picture field in order to ensure that no form cache is written,
+ // which breaks registration of more than 1 user every 6 hours.
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => 'user_picture',
+ 'entity_type' => 'user',
+ 'type' => 'image',
+ ]);
+ $field_storage->save();
+
+ $field = FieldConfig::create([
+ 'field_name' => 'user_picture',
+ 'entity_type' => 'user',
+ 'bundle' => 'user',
+ ]);
+ $field->save();
+
+ $form_display = EntityFormDisplay::create([
+ 'targetEntityType' => 'user',
+ 'bundle' => 'user',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ]);
+ $form_display->setComponent('user_picture', [
+ 'type' => 'image_image',
+ ]);
+ $form_display->save();
+
+ // Don't require email verification and allow registration by site visitors
+ // without administrator approval.
+ $this->config('user.settings')
+ ->set('verify_mail', FALSE)
+ ->set('register', USER_REGISTER_VISITORS)
+ ->save();
+
+ $edit = [];
+ $edit['name'] = $this->randomMachineName();
+ $edit['mail'] = $edit['name'] . '@example.com';
+ $edit['pass[pass2]'] = $edit['pass[pass1]'] = $this->randomMachineName();
+
+ // Create one account.
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertResponse(200);
+
+ $user_storage = \Drupal::entityManager()->getStorage('user');
+
+ $this->assertTrue($user_storage->loadByProperties(['name' => $edit['name']]));
+ $this->drupalLogout();
+
+ // Create a second account.
+ $edit['name'] = $this->randomMachineName();
+ $edit['mail'] = $edit['name'] . '@example.com';
+ $edit['pass[pass2]'] = $edit['pass[pass1]'] = $this->randomMachineName();
+
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertResponse(200);
+
+ $this->assertTrue($user_storage->loadByProperties(['name' => $edit['name']]));
+ }
+
+ public function testRegistrationDefaultValues() {
+ // Don't require email verification and allow registration by site visitors
+ // without administrator approval.
+ $config_user_settings = $this->config('user.settings')
+ ->set('verify_mail', FALSE)
+ ->set('register', USER_REGISTER_VISITORS)
+ ->save();
+
+ // Set the default timezone to Brussels.
+ $config_system_date = $this->config('system.date')
+ ->set('timezone.user.configurable', 1)
+ ->set('timezone.default', 'Europe/Brussels')
+ ->save();
+
+ // Check the presence of expected cache tags.
+ $this->drupalGet('user/register');
+ $this->assertCacheTag('config:user.settings');
+
+ $edit = [];
+ $edit['name'] = $name = $this->randomMachineName();
+ $edit['mail'] = $mail = $edit['name'] . '@example.com';
+ $edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
+ $edit['pass[pass2]'] = $new_pass;
+ $this->drupalPostForm(NULL, $edit, t('Create new account'));
+
+ // Check user fields.
+ $accounts = $this->container->get('entity_type.manager')->getStorage('user')
+ ->loadByProperties(['name' => $name, 'mail' => $mail]);
+ $new_user = reset($accounts);
+ $this->assertEqual($new_user->getUsername(), $name, 'Username matches.');
+ $this->assertEqual($new_user->getEmail(), $mail, 'Email address matches.');
+ $this->assertTrue(($new_user->getCreatedTime() > REQUEST_TIME - 20), 'Correct creation time.');
+ $this->assertEqual($new_user->isActive(), $config_user_settings->get('register') == USER_REGISTER_VISITORS ? 1 : 0, 'Correct status field.');
+ $this->assertEqual($new_user->getTimezone(), $config_system_date->get('timezone.default'), 'Correct time zone field.');
+ $this->assertEqual($new_user->langcode->value, \Drupal::languageManager()->getDefaultLanguage()->getId(), 'Correct language field.');
+ $this->assertEqual($new_user->preferred_langcode->value, \Drupal::languageManager()->getDefaultLanguage()->getId(), 'Correct preferred language field.');
+ $this->assertEqual($new_user->init->value, $mail, 'Correct init field.');
+ }
+
+ /**
+ * Tests username and email field constraints on user registration.
+ *
+ * @see \Drupal\user\Plugin\Validation\Constraint\UserNameUnique
+ * @see \Drupal\user\Plugin\Validation\Constraint\UserMailUnique
+ */
+ public function testUniqueFields() {
+ $account = $this->drupalCreateUser();
+
+ $edit = ['mail' => 'test@example.com', 'name' => $account->getUsername()];
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertRaw(SafeMarkup::format('The username %value is already taken.', ['%value' => $account->getUsername()]));
+
+ $edit = ['mail' => $account->getEmail(), 'name' => $this->randomString()];
+ $this->drupalPostForm('user/register', $edit, t('Create new account'));
+ $this->assertRaw(SafeMarkup::format('The email address %value is already taken.', ['%value' => $account->getEmail()]));
+ }
+
+}
diff --git a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
index 08a2d8ea19..f768b71ad5 100644
--- a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
+++ b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php
@@ -2,7 +2,6 @@
namespace Drupal\FunctionalTests;
-use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\ExpectationException;
use Behat\Mink\Selector\Xpath\Escaper;
use Drupal\Component\Render\FormattableMarkup;
@@ -220,13 +219,11 @@ protected function assertResponse($code) {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldExists() or
+ * $this->assertSession()->buttonExists() or
* $this->assertSession()->fieldValueEquals() instead.
*/
protected function assertFieldByName($name, $value = NULL) {
- $this->assertSession()->fieldExists($name);
- if ($value !== NULL) {
- $this->assertSession()->fieldValueEquals($name, (string) $value);
- }
+ $this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value);
}
/**
@@ -242,15 +239,11 @@ protected function assertFieldByName($name, $value = NULL) {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldNotExists() or
+ * $this->assertSession()->buttonNotExists() or
* $this->assertSession()->fieldValueNotEquals() instead.
*/
protected function assertNoFieldByName($name, $value = '') {
- if ($this->getSession()->getPage()->findField($name) && isset($value)) {
- $this->assertSession()->fieldValueNotEquals($name, (string) $value);
- }
- else {
- $this->assertSession()->fieldNotExists($name);
- }
+ $this->assertNoFieldByXPath($this->constructFieldXpath('name', $name), $value);
}
/**
@@ -268,19 +261,11 @@ protected function assertNoFieldByName($name, $value = '') {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldExists() or
+ * $this->assertSession()->buttonExists() or
* $this->assertSession()->fieldValueEquals() instead.
*/
protected function assertFieldById($id, $value = '') {
- $xpath = $this->assertSession()->buildXPathQuery('//textarea[@id=:value]|//input[@id=:value]|//select[@id=:value]', [':value' => $id]);
- $field = $this->getSession()->getPage()->find('xpath', $xpath);
-
- if (empty($field)) {
- throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'id', $field);
- }
-
- if ($value !== NULL) {
- $this->assertEquals($value, $field->getValue());
- }
+ $this->assertFieldByXPath($this->constructFieldXpath('id', $id), $value);
}
/**
@@ -290,23 +275,25 @@ protected function assertFieldById($id, $value = '') {
* Name or ID of field to assert.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
- * Use $this->assertSession()->fieldExists() instead.
+ * Use $this->assertSession()->fieldExists() or
+ * $this->assertSession()->buttonExists() instead.
*/
protected function assertField($field) {
- $this->assertSession()->fieldExists($field);
+ $this->assertFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
}
/**
- * Asserts that a field exists with the given name or ID does NOT exist.
+ * Asserts that a field does NOT exist with the given name or ID.
*
* @param string $field
* Name or ID of field to assert.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
- * Use $this->assertSession()->fieldNotExists() instead.
+ * Use $this->assertSession()->fieldNotExists() or
+ * $this->assertSession()->buttonNotExists() instead.
*/
protected function assertNoField($field) {
- $this->assertSession()->fieldNotExists($field);
+ $this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
}
/**
@@ -427,23 +414,11 @@ protected function assertNoLinkByHref($href) {
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->assertSession()->fieldNotExists() or
+ * $this->assertSession()->buttonNotExists() or
* $this->assertSession()->fieldValueNotEquals() instead.
*/
protected function assertNoFieldById($id, $value = '') {
- $xpath = $this->assertSession()->buildXPathQuery('//textarea[@id=:value]|//input[@id=:value]|//select[@id=:value]', [':value' => $id]);
- $field = $this->getSession()->getPage()->find('xpath', $xpath);
-
- // Return early if the field could not be found as expected.
- if ($field === NULL) {
- return;
- }
-
- if (!isset($value)) {
- throw new ExpectationException(sprintf('Id "%s" appears on this page, but it should not.', $id), $this->getSession()->getDriver());
- }
- elseif ($value === $field->getValue()) {
- throw new ExpectationException(sprintf('Failed asserting that %s is not equal to %s', $field->getValue(), $value), $this->getSession()->getDriver());
- }
+ $this->assertNoFieldByXPath($this->constructFieldXpath('id', $id), $value);
}
/**
@@ -585,25 +560,32 @@ protected function assertFieldByXPath($xpath, $value = NULL, $message = '') {
* (optional) A message to display with the assertion. Do not translate
* messages with t().
*
+ * @throws \Behat\Mink\Exception\ExpectationException
+ *
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use $this->xpath() instead and assert that the result is empty.
*/
protected function assertNoFieldByXPath($xpath, $value = NULL, $message = '') {
$fields = $this->xpath($xpath);
- // If value specified then check array for match.
- $found = TRUE;
- if (isset($value)) {
- $found = FALSE;
- if ($fields) {
- foreach ($fields as $field) {
- if ($field->getAttribute('value') == $value) {
- $found = TRUE;
- }
+ if (!empty($fields)) {
+ if (isset($value)) {
+ $found = FALSE;
+ try {
+ $this->assertFieldsByValue($fields, $value);
+ $found = TRUE;
+ }
+ catch (\Exception $e) {
+ }
+
+ if ($found) {
+ throw new ExpectationException(sprintf('The field resulting from %s was found with the provided value %s.', $xpath, $value), $this->getSession()->getDriver());
}
}
+ else {
+ throw new ExpectationException(sprintf('The field resulting from %s was found.', $xpath), $this->getSession()->getDriver());
+ }
}
- return $this->assertFalse($fields && $found, $message);
}
/**
@@ -629,7 +611,15 @@ protected function assertFieldsByValue($fields, $value = NULL, $message = '') {
$found = FALSE;
if ($fields) {
foreach ($fields as $field) {
- if ($field->getAttribute('value') == $value) {
+ if ($field->getAttribute('type') == 'checkbox') {
+ if (is_bool($value)) {
+ $found = $field->isChecked() == $value;
+ }
+ else {
+ $found = TRUE;
+ }
+ }
+ elseif ($field->getAttribute('value') == $value) {
// Input element with correct value.
$found = TRUE;
}
@@ -637,7 +627,7 @@ protected function assertFieldsByValue($fields, $value = NULL, $message = '') {
// Select element with an option.
$found = TRUE;
}
- elseif ($field->getText() == $value) {
+ elseif ($field->getTagName() !== 'input' && $field->getText() == $value) {
// Text area with correct text.
$found = TRUE;
}
@@ -789,6 +779,25 @@ protected function buildXPathQuery($xpath, array $args = []) {
}
/**
+ * Helper: Constructs an XPath for the given set of attributes and value.
+ *
+ * @param string $attribute
+ * Field attributes.
+ * @param string $value
+ * Value of field.
+ *
+ * @return string
+ * XPath for specified values.
+ *
+ * @deprecated Scheduled for removal in Drupal 9.0.0.
+ * Use $this->getSession()->getPage()->findField() instead.
+ */
+ protected function constructFieldXpath($attribute, $value) {
+ $xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]';
+ return $this->buildXPathQuery($xpath, [':value' => $value]);
+ }
+
+ /**
* Gets the current raw content.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
index 3a0750113c..4a831d8764 100644
--- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
+++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
@@ -181,7 +181,7 @@ public function testLegacyXpathAsserts() {
$this->assertNoFieldByXPath("//input[@id = 'edit-name']");
$this->fail('The "edit-name" field was not found.');
}
- catch (\PHPUnit_Framework_ExpectationFailedException $e) {
+ catch (ExpectationException $e) {
$this->pass('assertNoFieldByXPath correctly failed. The "edit-name" field was found.');
}
@@ -197,7 +197,7 @@ public function testLegacyXpathAsserts() {
/**
* Tests legacy field asserts using textfields.
*/
- public function testLegacyFieldAssertsWithTextfields() {
+ public function testLegacyFieldAssertsForTextfields() {
$this->drupalGet('test-field-xpath');
// *** 1. assertNoField().
@@ -230,7 +230,7 @@ public function testLegacyFieldAssertsWithTextfields() {
$this->assertField('invalid_name_and_id');
$this->fail('The "invalid_name_and_id" field was found.');
}
- catch (ExpectationException $e) {
+ catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass('assertField correctly failed. The "invalid_name_and_id" field was not found.');
}
@@ -319,7 +319,7 @@ public function testLegacyFieldAssertsWithTextfields() {
$this->assertFieldByName('non-existing-name');
$this->fail('The "non-existing-name" field was found.');
}
- catch (ExpectationException $e) {
+ catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass('The "non-existing-name" field was not found');
}
@@ -328,15 +328,15 @@ public function testLegacyFieldAssertsWithTextfields() {
$this->assertFieldByName('name', 'not the value');
$this->fail('The "name" field with incorrect value was found.');
}
- catch (ExpectationException $e) {
+ catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass('assertFieldByName correctly failed. The "name" field with incorrect value was not found.');
}
}
/**
- * Tests legacy field asserts on other types of field.
+ * Tests legacy field asserts for options field type.
*/
- public function testLegacyFieldAssertsWithNonTextfields() {
+ public function testLegacyFieldAssertsForOptions() {
$this->drupalGet('test-field-xpath');
// Option field type.
@@ -383,8 +383,14 @@ public function testLegacyFieldAssertsWithNonTextfields() {
catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass($e->getMessage());
}
+ }
+
+ /**
+ * Tests legacy field asserts for button field type.
+ */
+ public function testLegacyFieldAssertsForButton() {
+ $this->drupalGet('test-field-xpath');
- // Button field type.
$this->assertFieldById('edit-save', NULL);
// Test that the assertion fails correctly if the field value is passed in
// rather than the id.
@@ -392,7 +398,7 @@ public function testLegacyFieldAssertsWithNonTextfields() {
$this->assertFieldById('Save', NULL);
$this->fail('The field with id of "Save" was found.');
}
- catch (ExpectationException $e) {
+ catch (\PHPUnit_Framework_ExpectationFailedException $e) {
$this->pass($e->getMessage());
}
@@ -407,7 +413,27 @@ public function testLegacyFieldAssertsWithNonTextfields() {
$this->pass($e->getMessage());
}
- // Checkbox field type.
+ // Test that multiple fields with the same name are validated correctly.
+ $this->assertFieldByName('duplicate_button', 'Duplicate button 1');
+ $this->assertFieldByName('duplicate_button', 'Duplicate button 2');
+ $this->assertNoFieldByName('duplicate_button', 'Rabbit');
+
+ try {
+ $this->assertNoFieldByName('duplicate_button', 'Duplicate button 2');
+ $this->fail('The "duplicate_button" field with the value Duplicate button 2 was not found.');
+ }
+ catch (ExpectationException $e) {
+ $this->pass('assertNoFieldByName correctly failed. The "duplicate_button" field with the value Duplicate button 2 was found.');
+ }
+ }
+
+ /**
+ * Tests legacy field asserts for checkbox field type.
+ */
+ public function testLegacyFieldAssertsForCheckbox() {
+ $this->drupalGet('test-field-xpath');
+
+ // Part 1 - Test by name.
// Test that checkboxes are found/not found correctly by name, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldByName('checkbox_enabled', TRUE);
@@ -420,6 +446,24 @@ public function testLegacyFieldAssertsWithNonTextfields() {
$this->assertFieldByName('checkbox_enabled', NULL);
$this->assertFieldByName('checkbox_disabled', NULL);
+ // Test that checkboxes are found by name when passing no second parameter.
+ $this->assertFieldByName('checkbox_enabled');
+ $this->assertFieldByName('checkbox_disabled');
+
+ // Test that we have legacy support.
+ $this->assertFieldByName('checkbox_enabled', '1');
+ $this->assertFieldByName('checkbox_disabled', '');
+
+ // Test that the assertion fails correctly when using NULL to ignore state.
+ try {
+ $this->assertNoFieldByName('checkbox_enabled', NULL);
+ $this->fail('The "checkbox_enabled" field was not found by name, using NULL value.');
+ }
+ catch (ExpectationException $e) {
+ $this->pass('assertNoFieldByName failed correctly. The "checkbox_enabled" field was found using NULL value.');
+ }
+
+ // Part 2 - Test by ID.
// Test that checkboxes are found/not found correctly by ID, when using
// TRUE or FALSE to match their 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', TRUE);
@@ -427,19 +471,18 @@ public function testLegacyFieldAssertsWithNonTextfields() {
$this->assertNoFieldById('edit-checkbox-enabled', FALSE);
$this->assertNoFieldById('edit-checkbox-disabled', TRUE);
- // Test that checkboxes are found by by ID, when using NULL to ignore the
+ // Test that checkboxes are found by ID, when using NULL to ignore the
// 'checked' state.
$this->assertFieldById('edit-checkbox-enabled', NULL);
$this->assertFieldById('edit-checkbox-disabled', NULL);
- // Test that the assertion fails correctly when using NULL to ignore state.
- try {
- $this->assertNoFieldByName('checkbox_enabled', NULL);
- $this->fail('The "checkbox_enabled" field was not found by name, using NULL value.');
- }
- catch (ExpectationException $e) {
- $this->pass('assertNoFieldByName failed correctly. The "checkbox_enabled" field was found using NULL value.');
- }
+ // Test that checkboxes are found by ID when passing no second parameter.
+ $this->assertFieldById('edit-checkbox-enabled');
+ $this->assertFieldById('edit-checkbox-disabled');
+
+ // Test that we have legacy support.
+ $this->assertFieldById('edit-checkbox-enabled', '1');
+ $this->assertFieldById('edit-checkbox-disabled', '');
// Test that the assertion fails correctly when using NULL to ignore state.
try {
@@ -450,7 +493,7 @@ public function testLegacyFieldAssertsWithNonTextfields() {
$this->pass('assertNoFieldById failed correctly. The "edit-checkbox-disabled" field was found by ID using NULL value.');
}
- // Test the specific 'checked' assertions.
+ // Part 3 - Test the specific 'checked' assertions.
$this->assertFieldChecked('edit-checkbox-enabled');
$this->assertNoFieldChecked('edit-checkbox-disabled');