Commit a569d8ad authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '62980-username-availability-checker-breaks-inline-validation' into 'master'

Resolve "Username availability checker breaks inline validation"

Closes #62980

See merge request gitlab-org/gitlab-ce!29678
parents 7fa94651 78eeb3e0
...@@ -7,8 +7,8 @@ import OAuthRememberMe from './oauth_remember_me'; ...@@ -7,8 +7,8 @@ import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment'; import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new LengthValidator(); // eslint-disable-line no-new
new UsernameValidator(); // eslint-disable-line no-new new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new new NoEmojiValidator(); // eslint-disable-line no-new
......
/* eslint-disable consistent-return, class-methods-use-this */ import InputValidator from '~/validators/input_validator';
import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
const debounceTimeoutDuration = 1000; const debounceTimeoutDuration = 1000;
const rootUrl = gon.relative_url_root;
const invalidInputClass = 'gl-field-error-outline'; const invalidInputClass = 'gl-field-error-outline';
const successInputClass = 'gl-field-success-outline'; const successInputClass = 'gl-field-success-outline';
const unavailableMessageSelector = '.username .validation-error'; const successMessageSelector = '.validation-success';
const successMessageSelector = '.username .validation-success'; const pendingMessageSelector = '.validation-pending';
const pendingMessageSelector = '.username .validation-pending'; const unavailableMessageSelector = '.validation-error';
const invalidMessageSelector = '.username .gl-field-error';
export default class UsernameValidator { export default class UsernameValidator extends InputValidator {
constructor() { constructor(opts = {}) {
this.inputElement = $('#new_user_username'); super();
this.inputDomElement = this.inputElement.get(0);
this.state = {
available: false,
valid: false,
pending: false,
empty: true,
};
const debounceTimeout = _.debounce(username => { const container = opts.container || '';
this.validateUsername(username); const validateLengthElements = document.querySelectorAll(`${container} .js-validate-username`);
}, debounceTimeoutDuration);
this.inputElement.on('keyup.username_check', () => {
const username = this.inputElement.val();
this.state.valid = this.inputDomElement.validity.valid;
this.state.empty = !username.length;
if (this.state.valid) { this.debounceValidateInput = _.debounce(inputDomElement => {
return debounceTimeout(username); UsernameValidator.validateUsernameInput(inputDomElement);
} }, debounceTimeoutDuration);
this.renderState();
});
// Override generic field validation validateLengthElements.forEach(element =>
this.inputElement.on('invalid', this.interceptInvalid.bind(this)); element.addEventListener('input', this.eventHandler.bind(this)),
);
} }
renderState() { eventHandler(event) {
// Clear all state const inputDomElement = event.target;
this.clearFieldValidationState();
if (this.state.valid && this.state.available) {
return this.setSuccessState();
}
if (this.state.empty) {
return this.clearFieldValidationState();
}
if (this.state.pending) {
return this.setPendingState();
}
if (!this.state.valid) { UsernameValidator.resetInputState(inputDomElement);
return this.setInvalidState(); this.debounceValidateInput(inputDomElement);
}
if (!this.state.available) {
return this.setUnavailableState();
}
}
interceptInvalid(event) {
event.preventDefault();
event.stopPropagation();
} }
validateUsername(username) { static validateUsernameInput(inputDomElement) {
if (this.state.valid) { const username = inputDomElement.value;
this.state.pending = true;
this.state.available = false; if (inputDomElement.checkValidity() && username.length > 0) {
this.renderState(); UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
axios UsernameValidator.fetchUsernameAvailability(username)
.get(`${gon.relative_url_root}/users/${username}/exists`) .then(usernameTaken => {
.then(({ data }) => this.setAvailabilityState(data.exists)) UsernameValidator.setInputState(inputDomElement, !usernameTaken);
UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector, false);
UsernameValidator.setMessageVisibility(
inputDomElement,
usernameTaken ? unavailableMessageSelector : successMessageSelector,
);
})
.catch(() => flash(__('An error occurred while validating username'))); .catch(() => flash(__('An error occurred while validating username')));
} }
} }
setAvailabilityState(usernameTaken) { static fetchUsernameAvailability(username) {
if (usernameTaken) { return axios.get(`${rootUrl}/users/${username}/exists`).then(({ data }) => data.exists);
this.state.available = false;
} else {
this.state.available = true;
}
this.state.pending = false;
this.renderState();
} }
clearFieldValidationState() { static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) {
this.inputElement.siblings('p').hide(); const messageElement = inputDomElement.parentElement.querySelector(messageSelector);
messageElement.classList.toggle('hide', !isVisible);
this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass);
} }
setUnavailableState() { static setInputState(inputDomElement, success = true) {
const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); inputDomElement.classList.toggle(successInputClass, success);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); inputDomElement.classList.toggle(invalidInputClass, !success);
$usernameUnavailableMessage.show();
} }
setSuccessState() { static resetInputState(inputDomElement) {
const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); UsernameValidator.setMessageVisibility(inputDomElement, successMessageSelector, false);
this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); UsernameValidator.setMessageVisibility(inputDomElement, unavailableMessageSelector, false);
$usernameSuccessMessage.show();
}
setPendingState() { if (inputDomElement.checkValidity()) {
const $usernamePendingMessage = $(pendingMessageSelector); inputDomElement.classList.remove(successInputClass, invalidInputClass);
if (this.state.pending) {
$usernamePendingMessage.show();
} else {
$usernamePendingMessage.hide();
} }
} }
setInvalidState() {
const $inputErrorMessage = $(invalidMessageSelector);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
$inputErrorMessage.show();
}
} }
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
= f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length } }, required: true, title: _("This field is required.") = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length } }, required: true, title: _("This field is required.")
.username.form-group .username.form-group
= f.label :username, class: 'label-bold' = f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji js-validate-length", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length } }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.") = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length } }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
%p.validation-error.field-validation.hide= _('Username is already taken.') %p.validation-error.gl-field-error-ignore.field-validation.hide= _('Username is already taken.')
%p.validation-success.field-validation.hide= _('Username is available.') %p.validation-success.gl-field-error-ignore.field-validation.hide= _('Username is available.')
%p.validation-pending.field-validation.hide= _('Checking username availability...') %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking username availability...')
.form-group .form-group
= f.label :email, class: 'label-bold' = f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: _("Please provide a valid email address.") = f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: _("Please provide a valid email address.")
......
---
title: Fix the signup form's username validation messages not displaying
merge_request: 29678
author: Jiaan Louw
type: fixed
...@@ -19,7 +19,7 @@ describe 'Signup' do ...@@ -19,7 +19,7 @@ describe 'Signup' do
end end
it 'does not show an error border if the username contains dots (.)' do it 'does not show an error border if the username contains dots (.)' do
fill_in 'new_user_username', with: 'new.user.username' simulate_input('#new_user_username', 'new.user.username')
wait_for_requests wait_for_requests
expect(find('.username')).not_to have_css '.gl-field-error-outline' expect(find('.username')).not_to have_css '.gl-field-error-outline'
...@@ -41,7 +41,14 @@ describe 'Signup' do ...@@ -41,7 +41,14 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline' expect(find('.username')).to have_css '.gl-field-error-outline'
end end
it 'shows an error border if the username contains special characters' do it 'shows a success border if the username is available' do
fill_in 'new_user_username', with: 'new-user'
wait_for_requests
expect(find('.username')).to have_css '.gl-field-success-outline'
end
it 'shows an error border if the username contains special characters' do
fill_in 'new_user_username', with: 'new$user!username' fill_in 'new_user_username', with: 'new$user!username'
wait_for_requests wait_for_requests
...@@ -71,7 +78,7 @@ describe 'Signup' do ...@@ -71,7 +78,7 @@ describe 'Signup' do
expect(page).to have_content("Please create a username with only alphanumeric characters.") expect(page).to have_content("Please create a username with only alphanumeric characters.")
end end
it 'shows an error border if the username contains emojis' do it 'shows an error border if the username contains emojis' do
simulate_input('#new_user_username', 'ehsan😀') simulate_input('#new_user_username', 'ehsan😀')
expect(find('.username')).to have_css '.gl-field-error-outline' expect(find('.username')).to have_css '.gl-field-error-outline'
...@@ -82,6 +89,37 @@ describe 'Signup' do ...@@ -82,6 +89,37 @@ describe 'Signup' do
expect(page).to have_content("Invalid input, please avoid emojis") expect(page).to have_content("Invalid input, please avoid emojis")
end end
it 'shows a pending message if the username availability is being fetched' do
fill_in 'new_user_username', with: 'new-user'
expect(find('.username > .validation-pending')).not_to have_css '.hide'
end
it 'shows a success message if the username is available' do
fill_in 'new_user_username', with: 'new-user'
wait_for_requests
expect(find('.username > .validation-success')).not_to have_css '.hide'
end
it 'shows an error message if the username is unavailable' do
existing_user = create(:user)
fill_in 'new_user_username', with: existing_user.username
wait_for_requests
expect(find('.username > .validation-error')).not_to have_css '.hide'
end
it 'shows a success message if the username is corrected and then available' do
fill_in 'new_user_username', with: 'new-user$'
wait_for_requests
fill_in 'new_user_username', with: 'new-user'
wait_for_requests
expect(page).to have_content("Username is available.")
end
end end
describe 'user\'s full name validation', :js do describe 'user\'s full name validation', :js do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment