From 83b064ef3c40b211a37c821db68119f0b9b49479 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 1 Mar 2026 19:56:47 +0100 Subject: [PATCH] Merge recover password/resend confirmation forms into sign in/register Closes #65, #66 --- app/assets/stylesheets/application.css | 23 ++- app/controllers/application_controller.rb | 12 ++ .../profiles_controller.rb} | 4 +- app/controllers/users_controller.rb | 2 +- app/helpers/application_helper.rb | 27 ++-- app/javascript/application.js | 12 ++ .../confirmations/create.turbo_stream.erb | 1 + app/views/users/confirmations/new.html.erb | 9 -- .../users/passwords/create.turbo_stream.erb | 2 + app/views/users/passwords/edit.html.erb | 2 +- app/views/users/passwords/new.html.erb | 8 -- .../{registrations => profiles}/edit.html.erb | 0 app/views/users/profiles/new.html.erb | 17 +++ app/views/users/registrations/new.html.erb | 16 --- app/views/users/sessions/new.html.erb | 29 ++-- config/initializers/devise.rb | 2 +- config/locales/devise.en.yml | 14 +- config/locales/en.yml | 3 +- config/routes.rb | 7 +- test/application_system_test_case.rb | 4 +- test/system/users_test.rb | 136 +++++++++++------- 21 files changed, 195 insertions(+), 135 deletions(-) rename app/controllers/{registrations_controller.rb => user/profiles_controller.rb} (79%) create mode 100644 app/views/users/confirmations/create.turbo_stream.erb delete mode 100644 app/views/users/confirmations/new.html.erb create mode 100644 app/views/users/passwords/create.turbo_stream.erb delete mode 100644 app/views/users/passwords/new.html.erb rename app/views/users/{registrations => profiles}/edit.html.erb (100%) create mode 100644 app/views/users/profiles/new.html.erb delete mode 100644 app/views/users/registrations/new.html.erb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 339400e..5455a38 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -113,6 +113,12 @@ textarea { border: solid 1px var(--color-gray); border-radius: 0.25em; } +[name=cancel], +.auxiliary { + border-color: var(--color-border-gray); + color: var(--color-nav-gray); + fill: var(--color-nav-gray); +} input[type=checkbox], svg, textarea { @@ -131,6 +137,7 @@ input[type=checkbox]:checked { -webkit-appearance: checkbox; } /* Hide spin buttons in input number fields */ +/* TODO: add spin buttons inside input[number]: before (-) and after (+) input */ input[type=number] { appearance: textfield; -moz-appearance: textfield; @@ -340,6 +347,7 @@ header { opacity: 1; } + /* TODO: Hover over invalid should work like in measurements (thin vs thick border) */ .labeled-form { align-items: center; @@ -371,9 +379,17 @@ header { } .labeled-form input[type=submit] { font-size: 1rem; - margin: 1.5em auto 0 auto; + margin: 1em auto 0 auto; padding: 0.75em; } +.labeled-form .auxiliary { + grid-column: 3; + /* If more buttons are needed, `grid-row` can be replaced with + * `reading-flow: grid-columns` to ensure proper tabindex order */ + grid-row: 1; + height: 100%; + padding-block: 0; +} /* TODO: remove .items class (?) and make 'form table' work properly */ @@ -532,11 +548,6 @@ table.items select:focus-within, table.items select:focus-visible { color: black; } -form a[name=cancel] { - border-color: var(--color-border-gray); - color: var(--color-nav-gray); - fill: var(--color-nav-gray); -} form table.items { border: none; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ed1d867..782583e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,6 +25,18 @@ class ApplicationController < ActionController::Base # Turbo will reload 2nd time with HTML format and flashes will be lost. rescue_from *ActionDispatch::ExceptionWrapper.rescue_responses.keys, with: :rescue_turbo + # Required by #respond_with (gem `responders`) used by Devise controllers. + respond_to :html, :turbo_stream + + def after_sign_in_path_for(resource) + # TODO: allow setting path per-user or save last path in session and restore + units_path + end + + def after_sign_out_path_for(resource) + new_user_session_path + end + protected def current_user_disguised? diff --git a/app/controllers/registrations_controller.rb b/app/controllers/user/profiles_controller.rb similarity index 79% rename from app/controllers/registrations_controller.rb rename to app/controllers/user/profiles_controller.rb index 6d6d4e5..e06155f 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/user/profiles_controller.rb @@ -1,6 +1,4 @@ -class RegistrationsController < Devise::RegistrationsController - before_action :authenticate_user!, only: [:edit, :update, :destroy] - +class User::ProfilesController < Devise::RegistrationsController def destroy # TODO: Disallow/disable deletion for last admin account; update :edit view super diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2c55b94..46f5036 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -37,7 +37,7 @@ class UsersController < ApplicationController end # NOTE: limited actions availabe to :admin by design. Users are meant to - # manage their accounts by themselves through registrations. :admin + # manage their accounts by themselves through profiles. :admin # is allowed to sign-in (disguise) as user and make changes from there. protected diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 71d4d4c..2ba0d3c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,13 +72,8 @@ module ApplicationHelper end def labeled_form_for(record, options = {}, &block) - extra_options = {builder: LabeledFormBuilder, - data: {turbo: false}, - html: {class: 'labeled-form'}} - options = options.deep_merge(extra_options) do |key, left, right| - key == :class ? class_names(left, right) : right - end - form_for(record, **options, &block) + extra_options = {builder: LabeledFormBuilder, html: {class: 'labeled-form'}} + form_for(record, **merge_attributes(options, extra_options), &block) end class TabularFormBuilder < ActionView::Helpers::FormBuilder @@ -135,16 +130,16 @@ module ApplicationHelper # [autofocus]. Otherwise IDs are not unique when multiple forms are open # and the first input gets focus. record_object, options = nil, record_object if record_object.is_a?(Hash) - options.merge!(builder: TabularFormBuilder, skip_default_ids: true) + extra_options = {builder: TabularFormBuilder, skip_default_ids: true} + options = merge_attributes(options, extra_options) # TODO: set error message with setCustomValidity instead of rendering to flash? render_errors(record_object || record_name) fields_for(record_name, record_object, **options, &block) end def tabular_form_with(**options, &block) - options = options.deep_merge(builder: TabularFormBuilder, - html: {autocomplete: 'off'}) - form_with(**options, &block) + extra_options = {builder: TabularFormBuilder, html: {autocomplete: 'off'}} + form_with(**merge_attributes(options, extra_options), &block) end def svg_tag(source, label = nil, options = {}) @@ -159,6 +154,7 @@ module ApplicationHelper ['measurements', 'scale-bathroom', :restricted], ['quantities', 'axis-arrow', :restricted, 'right'], ['units', 'weight-gram', :restricted], + # TODO: display users tab only if >1 user present; sole_user?/sole_admin? ['users', 'account-multiple-outline', :admin], ] @@ -206,6 +202,7 @@ module ApplicationHelper def render_errors(records) # Conversion of flash to Array only required because of Devise + # TODO: override Devise message setting to Array()? flash[:alert] = Array(flash[:alert]) Array(records).each { |record| flash[:alert] += record.errors.full_messages } end @@ -215,6 +212,7 @@ module ApplicationHelper # Conversion of flash to Array only required because of Devise Array(messages).map do |message| tag.div class: "flash #{entry}" do + # TODO: change button text to svg to make it aligned vertically tag.div(sanitize(message)) + tag.button(sanitize("×"), tabindex: -1, onclick: "this.parentElement.remove();") end @@ -252,4 +250,11 @@ module ApplicationHelper [name, html_options] end + + # Like Hash#deep_merge, but aware of HTML attributes. + def merge_attributes(left, right) + left.deep_merge(right) do |key, lvalue, rvalue| + key == :class ? class_names(lvalue, rvalue) : rvalue + end + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index c461da0..09ced32 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -37,6 +37,18 @@ window.detailsObserver = new MutationObserver((mutations) => { mutations[0].target.dispatchEvent(new Event('change', {bubbles: true})) }); +function formValidate(event) { + var id = event.submitter.getAttribute("data-validate") + if (!id) return; + + var input = document.getElementById(id) + if (!input.checkValidity()) { + input.reportValidity() + event.preventDefault() + } +} +window.formValidate = formValidate + /* Turbo stream actions */ Turbo.StreamElement.prototype.disableElement = function(element) { diff --git a/app/views/users/confirmations/create.turbo_stream.erb b/app/views/users/confirmations/create.turbo_stream.erb new file mode 100644 index 0000000..6e7b33a --- /dev/null +++ b/app/views/users/confirmations/create.turbo_stream.erb @@ -0,0 +1 @@ +<% flash.discard %> diff --git a/app/views/users/confirmations/new.html.erb b/app/views/users/confirmations/new.html.erb deleted file mode 100644 index 03853b5..0000000 --- a/app/views/users/confirmations/new.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%= labeled_form_for resource, url: user_confirmation_path, - html: {class: 'main-area'} do |f| %> - - <%= f.email_field :email, required: true, size: 30, autofocus: true, - autocomplete: 'email', value: - resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email %> - - <%= f.submit t(:resend_confirmation) %> -<% end %> diff --git a/app/views/users/passwords/create.turbo_stream.erb b/app/views/users/passwords/create.turbo_stream.erb new file mode 100644 index 0000000..99acbf4 --- /dev/null +++ b/app/views/users/passwords/create.turbo_stream.erb @@ -0,0 +1,2 @@ +<%# For some reason flash messages are duplicated in bot flash and flash.now %> +<% flash.discard %> diff --git a/app/views/users/passwords/edit.html.erb b/app/views/users/passwords/edit.html.erb index d79707d..e4189dd 100644 --- a/app/views/users/passwords/edit.html.erb +++ b/app/views/users/passwords/edit.html.erb @@ -1,5 +1,5 @@ <%= labeled_form_for resource, url: user_password_path, - html: {method: :put, class: 'main-area'} do |f| %> + html: {method: :put, class: 'main-area', data: {turbo: false}} do |f| %> <%= f.hidden_field :reset_password_token %> diff --git a/app/views/users/passwords/new.html.erb b/app/views/users/passwords/new.html.erb deleted file mode 100644 index b1a1a28..0000000 --- a/app/views/users/passwords/new.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= labeled_form_for resource, url: user_password_path, - html: {class: 'main-area'} do |f| %> - - <%= f.email_field :email, required: true, size: 30, autofocus: true, - autocomplete: 'email' %> - - <%= f.submit t(:recover_password) %> -<% end %> diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/profiles/edit.html.erb similarity index 100% rename from app/views/users/registrations/edit.html.erb rename to app/views/users/profiles/edit.html.erb diff --git a/app/views/users/profiles/new.html.erb b/app/views/users/profiles/new.html.erb new file mode 100644 index 0000000..9013a04 --- /dev/null +++ b/app/views/users/profiles/new.html.erb @@ -0,0 +1,17 @@ +<%= labeled_form_for resource, url: user_registration_path, + html: {class: 'main-area', onsubmit: 'formValidate(event)'} do |f| %> + + <%= f.email_field :email, required: true, size: 30, autofocus: true, + autocomplete: 'email' %> + <%= f.password_field :password, required: true, size: 30, + minlength: @minimum_password_length, autocomplete: 'new-password' %> + <%= f.password_field :password_confirmation, required: true, size: 30, + minlength: @minimum_password_length, autocomplete: 'off' %> + + <%= f.submit t(:register), data: {turbo: false} %> + + <%# TODO: fix button text color after change link -> button %> + <%= image_button_tag t(:resend_confirmation), 'email-sync-outline', + class: 'auxiliary', formaction: user_confirmation_path, formnovalidate: true, + data: {validate: f.field_id(:email)} %> +<% end %> diff --git a/app/views/users/registrations/new.html.erb b/app/views/users/registrations/new.html.erb deleted file mode 100644 index 7379e18..0000000 --- a/app/views/users/registrations/new.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
- <%= labeled_form_for resource, url: user_registration_path do |f| %> - <%= f.email_field :email, required: true, size: 30, autofocus: true, - autocomplete: 'email' %> - <%= f.password_field :password, required: true, size: 30, - minlength: @minimum_password_length, autocomplete: 'new-password' %> - <%= f.password_field :password_confirmation, required: true, size: 30, - minlength: @minimum_password_length, autocomplete: 'off' %> - - <%= f.submit t(:register) %> - <% end %> - - <%= content_tag :p, t(:or), style: 'text-align: center;' %> - <%= image_link_to t(:resend_confirmation), 'email-sync-outline', - new_user_confirmation_path, class: 'centered' %> -
diff --git a/app/views/users/sessions/new.html.erb b/app/views/users/sessions/new.html.erb index 3af3232..0678d95 100644 --- a/app/views/users/sessions/new.html.erb +++ b/app/views/users/sessions/new.html.erb @@ -1,18 +1,19 @@ -
- <%= labeled_form_for resource, url: user_session_path do |f| %> - <%= f.email_field :email, required: true, size: 30, autofocus: true, - autocomplete: 'email' %> - <%= f.password_field :password, required: true, size: 30, - minlength: @minimum_password_length, autocomplete: 'current-password' %> +<%= labeled_form_for resource, url: user_session_path, + html: {class: 'main-area', onsubmit: 'formValidate(event)'} do |f| %> - <% if devise_mapping.rememberable? %> - <%= f.check_box :remember_me %> - <% end %> + <%= f.email_field :email, required: true, size: 30, autofocus: true, + autocomplete: 'email' %> + <%= f.password_field :password, required: true, size: 30, + autocomplete: 'current-password' %> - <%= f.submit t(:sign_in) %> + <% if devise_mapping.rememberable? %> + <%= f.check_box :remember_me %> <% end %> - <%= content_tag :p, t(:or), style: 'text-align: center;' %> - <%= image_link_to t(:recover_password), 'lock-reset', new_user_password_path, - class: 'centered' %> -
+ <%# /sign_in as HTML; /password as TURBO_STREAM %> + <%= f.submit t(:sign_in), data: {turbo: false} %> + + <%= image_button_tag t(:recover_password), 'lock-reset', class: 'auxiliary', + formaction: user_password_path, formnovalidate: true, + data: {validate: f.field_id(:email)} %> +<% end %> diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5012fdc..d3522ac 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -91,7 +91,7 @@ Devise.setup do |config| # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + config.paranoid = true # By default Devise will store the user in session. You can skip storage for # particular strategies by setting this option. diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 260e1c4..f4ce4b2 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -4,15 +4,15 @@ en: devise: confirmations: confirmed: "Your email address has been successfully confirmed." - send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: > + If your email address is in our database, a message with instructions on how + to confirm your email address has been sent to you. failure: already_authenticated: "You are already signed in." inactive: "Your account is not activated yet." - invalid: "Invalid %{authentication_keys} or password." + invalid: "Invalid %{authentication_keys} or password." locked: "Your account is locked." last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid %{authentication_keys} or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your email address before continuing." @@ -32,8 +32,9 @@ en: success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + send_paranoid_instructions: > + If your email address is in our database, the password recovery link has been + sent to you. updated: "Your password has been changed successfully. You are now signed in." updated_not_active: "Your password has been changed successfully." registrations: @@ -50,7 +51,6 @@ en: signed_out: "Signed out successfully." already_signed_out: "Signed out successfully." unlocks: - send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: diff --git a/config/locales/en.yml b/config/locales/en.yml index 6d09873..24d5395 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -150,7 +150,7 @@ en: edit: password_html: 'New password:%{password_length_hint_html}' update_password: Update password - registrations: + profiles: new: password_html: 'Password:%{password_length_hint_html}' password_confirmation: 'Retype password:' @@ -169,7 +169,6 @@ en: cancel: Cancel delete: Delete :no: 'no' - or: or register: Register sign_in: Sign in recover_password: Recover password diff --git a/config/routes.rb b/config/routes.rb index 7d3035d..1d3f7b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,8 +24,9 @@ Rails.application.routes.draw do # https://github.com/heartcombo/devise/issues/5786 connection = ActiveRecord::Base.connection if connection.schema_version && connection.table_exists?(:users) + # NOTE: change helper prefix from *_registration to *_profile once possible devise_for :users, path: '', path_names: {registration: 'profile'}, - controllers: {registrations: :registrations} + controllers: {registrations: 'user/profiles'} end resources :users, only: [:index, :show, :update] do @@ -34,9 +35,7 @@ Rails.application.routes.draw do end unauthenticated do - as :user do - root to: redirect('/sign_in') - end + root to: redirect('/sign_in') end root to: redirect('/units'), as: :user_root diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 03617d3..246642c 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,6 +1,7 @@ require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::UrlHelper # NOTE: geckodriver installed with Firefox, ignore incompatibility warning @@ -32,7 +33,8 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase # Allow skipping interpolations when translating for testing purposes INTERPOLATION_PATTERNS = Regexp.union(I18n.config.interpolation_patterns) def translate(key, **options) - options.empty? ? super.split(INTERPOLATION_PATTERNS, 2).first : super + translation = options.empty? ? super.split(INTERPOLATION_PATTERNS, 2).first : super + sanitize(translation, tags: []) end alias :t :translate diff --git a/test/system/users_test.rb b/test/system/users_test.rb index ad362cd..070c514 100644 --- a/test/system/users_test.rb +++ b/test/system/users_test.rb @@ -5,8 +5,8 @@ class UsersTest < ApplicationSystemTestCase @admin = users(:admin) end - test "sign in" do - visit new_user_session_path + test 'sign in' do + visit root_url assert find_link(href: new_user_session_path)[:disabled] sign_in @@ -14,16 +14,23 @@ class UsersTest < ApplicationSystemTestCase assert_text t('devise.sessions.signed_in') end - test 'sign in fails with invalid password' do - sign_in password: random_password + test 'sign in fails with invalid credentials' do + label = User.human_attribute_name(:email) + # Both: valid and invalid emails should give the same (paranoid) error message. + email = [users.sample.email, random_email].sample + + visit root_url + fill_in label, with: email + fill_in User.human_attribute_name(:password), with: random_password + click_on t(:sign_in) + assert_current_path new_user_session_path - assert_text t('devise.failure.not_found_in_database', - authentication_keys: User.human_attribute_name(:email)) + assert_text t('devise.failure.invalid', authentication_keys: label.downcase_first) assert find_link(href: new_user_session_path)[:disabled] - assert_not_empty find_field(User.human_attribute_name(:email)).value + assert has_field?(label, with: email) end - test "sign out" do + test 'sign out' do sign_in visit root_url click_on t("layouts.application.sign_out") @@ -31,79 +38,106 @@ class UsersTest < ApplicationSystemTestCase assert_text t("devise.sessions.signed_out") end - test "recover password" do - visit new_user_session_url - click_on t(:recover_password) + test 'recover password' do + label = User.human_attribute_name(:email) + email = users.select(&:confirmed?).sample.email + + visit root_url + fill_in label, with: email + # Form validations should allow empty password. + assert has_field?(User.human_attribute_name(:password), with: nil) - fill_in User.human_attribute_name(:email), - with: users.select(&:confirmed?).sample.email assert_emails 1 do click_on t(:recover_password) - # Wait until redirected to make sure async request has been processed assert_current_path new_user_session_path + # Wait for flash message to make sure async request has been processed. + assert_text t("devise.passwords.send_paranoid_instructions") end - assert_text t("devise.passwords.send_instructions") + assert has_field?(label, with: email) with_last_email do |mail| visit Capybara.string(mail.body.to_s).find_link("Change my password")[:href] + assert_current_path edit_user_password_path, ignore_query: true + # Make sure flash message is not displayed twice. + assert_no_text t("devise.passwords.send_paranoid_instructions") end new_password = random_password fill_in t("users.passwords.edit.password_html"), with: new_password fill_in t("helpers.label.user.password_confirmation"), with: new_password assert_emails 1 do click_on t("users.passwords.edit.update_password") - # Wait until redirected to make sure async request has been processed assert_current_path units_path + assert_text t("devise.passwords.updated") end - assert_text t("devise.passwords.updated") end - test "register" do - visit new_user_session_url + test 'recover password for nonexistent user' do + label = User.human_attribute_name(:email) + email = random_email + + visit root_url + fill_in label, with: email + + assert_no_emails do + click_on t(:recover_password) + assert_current_path new_user_session_path + assert_text t("devise.passwords.send_paranoid_instructions") + end + end + + test 'register' do + visit root_url click_on t(:register) + assert find_link(href: new_user_registration_path)[:disabled] fill_in User.human_attribute_name(:email), with: random_email password = random_password fill_in User.human_attribute_name(:password), with: password - fill_in t("users.registrations.new.password_confirmation"), with: password - assert_difference ->{User.count}, 1 do + fill_in t("users.profiles.new.password_confirmation"), with: password + assert_difference ->{ User.count }, 1 do assert_emails 1 do click_on t(:register) - # Wait until redirected to make sure async request has been processed assert_current_path new_user_session_path + assert_text t("devise.registrations.signed_up_but_unconfirmed") end end - assert_text t("devise.registrations.signed_up_but_unconfirmed") - with_last_email do |mail| - visit Capybara.string(mail.body.to_s).find_link("Confirm my account")[:href] + assert_changes ->{ User.last.confirmed? }, from: false, to: true do + with_last_email do |mail| + visit Capybara.string(mail.body.to_s).find_link("Confirm my account")[:href] + assert_current_path new_user_session_path + assert_text t("devise.confirmations.confirmed") + end end - assert_current_path new_user_session_path - assert_text t("devise.confirmations.confirmed") - assert User.last.confirmed? end - test "resend confirmation" do - visit new_user_session_url - click_on t(:register) - click_on t(:resend_confirmation) + test 'resend confirmation' do + label = User.human_attribute_name(:email) + user = users.reject(&:confirmed?).sample + + visit root_url + click_on t(:register) + fill_in label, with: user.email + assert has_field?(User.human_attribute_name(:password), with: nil) - fill_in User.human_attribute_name(:email), - with: users.reject(&:confirmed?).sample.email assert_emails 1 do click_on t(:resend_confirmation) - # Wait until redirected to make sure async request has been processed - assert_current_path new_user_session_path + assert_current_path new_user_registration_path + assert_text t("devise.confirmations.send_paranoid_instructions") end - assert_current_path new_user_session_path - assert_text t("devise.confirmations.send_instructions") + assert has_field?(label, with: user.email) - with_last_email do |mail| - visit Capybara.string(mail.body.to_s).find_link("Confirm my account")[:href] + assert_changes ->{ user.reload.confirmed? }, from: false, to: true do + with_last_email do |mail| + visit Capybara.string(mail.body.to_s).find_link("Confirm my account")[:href] + assert_current_path new_user_session_path + assert_no_text t("devise.confirmations.send_paranoid_instructions") + assert_text t("devise.confirmations.confirmed") + end end end - test "show profile" do + test 'show profile' do sign_in user: users.select(&:admin?).select(&:confirmed?).sample click_on t("users.navigation") within all('tr').drop(1).sample do |tr| @@ -113,7 +147,7 @@ class UsersTest < ApplicationSystemTestCase end end - test "disguise" do + test 'disguise' do user = users.select(&:admin?).select(&:confirmed?).sample sign_in user: user @@ -129,7 +163,7 @@ class UsersTest < ApplicationSystemTestCase assert_link user.email end - test "disguise fails for admin when disallowed" do + test 'disguise fails for admin when disallowed' do user = users.select(&:admin?).select(&:confirmed?).sample sign_in user: user @@ -142,13 +176,13 @@ class UsersTest < ApplicationSystemTestCase assert_title 'The change you wanted was rejected (422)' end - test "disguise forbidden for non admin" do + test 'disguise forbidden for non admin' do sign_in user: users.reject(&:admin?).select(&:confirmed?).sample visit disguise_user_path(User.all.sample) assert_title 'Access is forbidden to this page (403)' end - test "delete profile" do + test 'delete profile' do user = sign_in # TODO: remove condition after root_url changed to different path than # profile in routes.rb @@ -156,23 +190,23 @@ class UsersTest < ApplicationSystemTestCase first(:link_or_button, user.email).click end assert_difference ->{ User.count }, -1 do - accept_confirm { click_on t("users.registrations.edit.delete") } + accept_confirm { click_on t("users.profiles.edit.delete") } assert_current_path new_user_session_path end assert_text t("devise.registrations.destroyed") end - test "index forbidden for non admin" do + test 'index forbidden for non admin' do sign_in user: users.reject(&:admin?).select(&:confirmed?).sample visit users_path assert_title "Access is forbidden to this page (403)" end - test "update profile" do + test 'update profile' do # TODO end - test "update status" do + test 'update status' do sign_in user: users.select(&:admin?).select(&:confirmed?).sample visit users_path @@ -187,7 +221,7 @@ class UsersTest < ApplicationSystemTestCase assert_current_path users_path end - test "update status fails for admin when disallowed" do + test 'update status fails for admin when disallowed' do sign_in user: users.select(&:admin?).select(&:confirmed?).sample visit users_path @@ -200,7 +234,7 @@ class UsersTest < ApplicationSystemTestCase assert_title 'The change you wanted was rejected (422)' end - test "update status forbidden for non admin" do + test 'update status forbidden for non admin' do sign_in user: users.reject(&:admin?).select(&:confirmed?).sample visit units_path inject_button_to find('body'), "update status", user_path(User.all.sample), method: :patch,