From 97fde95e874250343ec4282c06abc3592b733c12 Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Tue, 12 Nov 2024 15:19:48 +0100 Subject: [PATCH] Add Rails 8.0 compatibility (#952) - Add class parameter to Uniqueness validator as per rails/rails#53598 - Test new edge methods textarea and checkbox - Allow new Rails 8 helper names like `textarea` (close #941) --- .github/workflows/ruby.yml | 11 ++++- .rubocop.yml | 2 +- Appraisals | 5 +- CHANGELOG.md | 6 +-- README.md | 2 +- client_side_validations.gemspec | 4 +- gemfiles/rails_7.1.gemfile | 2 +- gemfiles/rails_8.0.gemfile | 25 ++++++++++ .../action_view/form_builder.rb | 13 +++++- package.json | 14 +++--- .../cases/test_form_for_helpers.rb | 46 +++++++++++++++++++ .../cases/test_form_with_helpers.rb | 46 +++++++++++++++++++ .../cases/test_legacy_form_for_helpers.rb | 29 ++++++++++++ .../cases/test_legacy_form_with_helpers.rb | 29 ++++++++++++ .../cases/test_uniqueness_validator.rb | 24 +++++----- 15 files changed, 228 insertions(+), 30 deletions(-) create mode 100644 gemfiles/rails_8.0.gemfile diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 330eca5b..a3c97e5d 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3'] - gemfile: [rails_6.1, rails_7.0, rails_7.1, rails_7.2] + gemfile: [rails_6.1, rails_7.0, rails_7.1, rails_7.2, rails_8.0] channel: ['stable'] include: @@ -32,6 +32,9 @@ jobs: - ruby-version: 'head' gemfile: rails_7.2 channel: 'experimental' + - ruby-version: 'head' + gemfile: rails_8.0 + channel: 'experimental' - ruby-version: 'head' gemfile: rails_edge channel: 'experimental' @@ -39,8 +42,14 @@ jobs: exclude: - ruby-version: '2.7' gemfile: rails_7.2 + - ruby-version: '2.7' + gemfile: rails_8.0 - ruby-version: '3.0' gemfile: rails_7.2 + - ruby-version: '3.0' + gemfile: rails_8.0 + - ruby-version: '3.1' + gemfile: rails_8.0 - ruby-version: '3.2' gemfile: rails_6.1 - ruby-version: '3.3' diff --git a/.rubocop.yml b/.rubocop.yml index 790124c1..8abb2cfe 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -49,7 +49,7 @@ Metrics/MethodLength: - 'test/**/*' Metrics/ModuleLength: - Max: 101 + Max: 109 Exclude: - 'test/**/*' diff --git a/Appraisals b/Appraisals index 55955b66..bcbe6612 100644 --- a/Appraisals +++ b/Appraisals @@ -12,13 +12,16 @@ end appraise 'rails-7.1' do gem 'rails', '~> 7.1.0' - gem 'sqlite3', '~> 1.7' # FIXME: remove after rails/rails#51592 end appraise 'rails-7.2' do gem 'rails', '~> 7.2.0' end +appraise 'rails-8.0' do + gem 'rails', '~> 8.0.0' +end + appraise 'rails-edge' do gem 'rails', git: 'https://github.com/rails/rails.git', branch: 'main' end diff --git a/CHANGELOG.md b/CHANGELOG.md index ed669138..ac42c68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog -## main / unreleased +## 22.3.0 / 2024-11-08 +* [FEATURE] Rails 8.0 compatibility * [ENHANCEMENT] Update QUnit to 2.22.0 -* [ENHANCEMENT] Prefer `require_relative` for internal requires -## 22.2.0 / 2026-06-01 +## 22.2.0 / 2024-06-01 * [FEATURE] Allow Rails 8.0 alpha * [ENHANCEMENT] Test against Ruby 3.3 diff --git a/README.md b/README.md index 0c287169..c048150c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Coverage Status](https://coveralls.io/repos/github/DavyJonesLocker/client_side_validations/badge.svg?branch=main)](https://coveralls.io/github/DavyJonesLocker/client_side_validations?branch=main) -`ClientSideValidations` made easy for your Rails 6.1 / 7.x applications! +`ClientSideValidations` made easy for your Rails 6.1, 7.x, and 8.0 applications! ## Project Goals ## diff --git a/client_side_validations.gemspec b/client_side_validations.gemspec index 8cb42d41..d760c32e 100644 --- a/client_side_validations.gemspec +++ b/client_side_validations.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |spec| spec.email = ['tagliala.dev@gmail.com', 'bcardarella@gmail.com'] spec.summary = 'Client Side Validations' - spec.description = 'Client Side Validations made easy for your Rails 6.1 and 7.x applications' + spec.description = 'Client Side Validations made easy for your Rails 6.1, 7.x, and 8.0 applications' spec.homepage = 'https://github.com/DavyJonesLocker/client_side_validations' spec.license = 'MIT' @@ -28,5 +28,5 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.7' spec.add_dependency 'js_regex', '~> 3.7' - spec.add_dependency 'rails', '>= 6.1', '< 8.0' + spec.add_dependency 'rails', '>= 6.1' end diff --git a/gemfiles/rails_7.1.gemfile b/gemfiles/rails_7.1.gemfile index d6835f51..fd09126a 100644 --- a/gemfiles/rails_7.1.gemfile +++ b/gemfiles/rails_7.1.gemfile @@ -18,7 +18,7 @@ gem "shotgun" gem "simplecov" gem "simplecov-lcov" gem "sinatra" -gem "sqlite3", "~> 1.7" +gem "sqlite3" gem "webrick" gem "rails", "~> 7.1.0" diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile new file mode 100644 index 00000000..8f61fdf6 --- /dev/null +++ b/gemfiles/rails_8.0.gemfile @@ -0,0 +1,25 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "byebug" +gem "m" +gem "minitest" +gem "mocha" +gem "rake" +gem "rubocop" +gem "rubocop-minitest" +gem "rubocop-packaging" +gem "rubocop-performance" +gem "rubocop-rails" +gem "rubocop-rake" +gem "shotgun" +gem "simplecov" +gem "simplecov-lcov" +gem "sinatra" +gem "sqlite3" +gem "webrick" +gem "rails", "~> 8.0.0" + +gemspec path: "../" diff --git a/lib/client_side_validations/action_view/form_builder.rb b/lib/client_side_validations/action_view/form_builder.rb index 2dbaceb5..6ac62537 100644 --- a/lib/client_side_validations/action_view/form_builder.rb +++ b/lib/client_side_validations/action_view/form_builder.rb @@ -5,7 +5,9 @@ module ActionView module Helpers module FormBuilder def self.prepended(base) - (base.field_helpers - %i[label check_box radio_button fields_for fields hidden_field file_field]).each do |selector| + selectors = base.field_helpers - %i[label check_box checkbox radio_button fields_for fields hidden_field file_field] + + selectors.each do |selector| base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 # Cannot call super here, rewrite all def #{selector}(method, options = {}) # def text_field(method, options = {}) @@ -19,6 +21,10 @@ def #{selector}(method, options = {}) # def text_field(method, options = { end # end RUBY_EVAL end + + base.class_eval do + alias_method :text_area, :textarea if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:textarea) + end end def initialize(object_name, object, template, options) @@ -47,6 +53,7 @@ def check_box(method, options = {}, checked_value = '1', unchecked_value = '0') options.delete(:validate) super end + alias checkbox check_box if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:checkbox) %i[collection_check_boxes collection_radio_buttons].each do |method_name| define_method method_name do |method, collection, value_method, text_method, options = {}, html_options = {}, &block| @@ -56,6 +63,10 @@ def check_box(method, options = {}, checked_value = '1', unchecked_value = '0') end end + if ::ActionView::Helpers::FormBuilder.public_instance_methods.include?(:collection_checkboxes) + alias collection_checkboxes collection_check_boxes + end + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) build_validation_options(method, html_options.merge(name: options[:name])) html_options.delete(:validate) diff --git a/package.json b/package.json index bcd55136..bb66662c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@client-side-validations/client-side-validations", - "description": "Client Side Validations made easy for your Rails 6.1 / 7.x applications", + "description": "Client Side Validations made easy for your Rails 6.1, 7.x, and 8.0 applications", "repository": { "type": "git", "url": "git+https://github.com/DavyJonesLocker/client_side_validations.git" @@ -22,15 +22,15 @@ "test": "test/javascript/run-qunit.mjs" }, "devDependencies": { - "@babel/core": "^7.25.8", - "@babel/preset-env": "^7.25.8", + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-node-resolve": "^15.3.0", "chrome-launcher": "^1.1.2", - "eslint": "^9.12.0", - "neostandard": "^0.11.6", - "puppeteer-core": "^23.5.3", - "rollup": "^4.24.0", + "eslint": "^9.14.0", + "neostandard": "^0.11.7", + "puppeteer-core": "^23.7.1", + "rollup": "^4.25.0", "rollup-plugin-copy": "^3.5.0" }, "peerDependencies": { diff --git a/test/action_view/cases/test_form_for_helpers.rb b/test/action_view/cases/test_form_for_helpers.rb index d50f7057..4f727529 100644 --- a/test/action_view/cases/test_form_for_helpers.rb +++ b/test/action_view/cases/test_form_for_helpers.rb @@ -70,6 +70,21 @@ def test_text_area assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:textarea) + def test_textarea + form_for(@post, validate: true) do |f| + concat f.textarea(:cost) + end + + validators = { 'post[cost]' => { presence: [{ message: I18n.t('errors.messages.blank') }] } } + expected = whole_form_for('/posts', 'new_post', 'new_post', validators: validators) do + form_field('textarea', id: 'post_cost', name: 'post[cost]', tag_content: "\n") + end + + assert_dom_equal expected, output_buffer + end + end + def test_file_field form_for(@post, validate: true) do |f| concat f.file_field(:cost) @@ -97,6 +112,22 @@ def test_check_box assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:checkbox) + def test_checkbox + form_for(@post, validate: true) do |f| + concat f.checkbox(:cost) + end + + validators = { 'post[cost]' => { presence: [{ message: I18n.t('errors.messages.blank') }] } } + expected = whole_form_for('/posts', 'new_post', 'new_post', validators: validators) do + form_field('input', name: 'post[cost]', type: 'hidden', value: '0') + + form_field('input', id: 'post_cost', name: 'post[cost]', type: 'checkbox', value: '1') + end + + assert_dom_equal expected, output_buffer + end + end + def test_check_box_ensure_no_validate_attribute form_for(@post, validate: true) do |f| concat f.check_box(:cost, validate: true) @@ -425,6 +456,21 @@ def test_collection_check_boxes assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.public_instance_methods.include?(:collection_checkboxes) + def test_collection_checkboxes + form_for(@post, validate: true) do |f| + concat f.collection_checkboxes(:cost, [], :id, :name) + end + + validators = { 'post[cost]' => { presence: [{ message: I18n.t('errors.messages.blank') }] } } + expected = whole_form_for('/posts', 'new_post', 'new_post', validators: validators) do + form_field('input', name: 'post[cost][]', type: 'hidden', value: '') + end + + assert_dom_equal expected, output_buffer + end + end + def test_collection_check_boxes_with_validate_options form_for(@post, validate: true) do |f| concat f.collection_check_boxes(:cost, [], :id, :name, {}, validate: false) diff --git a/test/action_view/cases/test_form_with_helpers.rb b/test/action_view/cases/test_form_with_helpers.rb index 9a367c03..9cf5a3c3 100644 --- a/test/action_view/cases/test_form_with_helpers.rb +++ b/test/action_view/cases/test_form_with_helpers.rb @@ -95,6 +95,21 @@ def test_form_with_text_area assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:textarea) + def test_form_with_textarea + form_with(model: @post, validate: true) do |f| + concat f.textarea(:cost) + end + + validators = { 'post[cost]' => { presence: [{ message: I18n.t('errors.messages.blank') }] } } + expected = whole_form_with('/posts', validators: validators) do + form_field('textarea', id: 'post_cost', name: 'post[cost]', tag_content: "\n") + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_with_file_field form_with(model: @post, validate: true) do |f| concat f.file_field(:cost) @@ -122,6 +137,22 @@ def test_form_with_check_box assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:checkbox) + def test_form_with_checkbox + form_with(model: @post, validate: true) do |f| + concat f.checkbox(:cost) + end + + validators = { 'post[cost]' => { presence: [{ message: I18n.t('errors.messages.blank') }] } } + expected = whole_form_with('/posts', validators: validators) do + form_field('input', name: 'post[cost]', type: 'hidden', value: '0') + + form_field('input', id: 'post_cost', name: 'post[cost]', type: 'checkbox', value: '1') + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_with_check_box_ensure_no_validate_attribute form_with(model: @post, validate: true) do |f| concat f.check_box(:cost, validate: true) @@ -440,6 +471,21 @@ def test_form_with_collection_check_boxes assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.public_instance_methods.include?(:collection_checkboxes) + def test_form_with_collection_checkboxes + form_with(model: @post, validate: true) do |f| + concat f.collection_checkboxes(:cost, [], :id, :name) + end + + validators = { 'post[cost]' => { presence: [{ message: I18n.t('errors.messages.blank') }] } } + expected = whole_form_with('/posts', validators: validators) do + form_field('input', name: 'post[cost][]', type: 'hidden', value: '') + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_with_collection_check_boxes_with_validate_options form_with(model: @post, validate: true) do |f| concat f.collection_check_boxes(:cost, [], :id, :name, {}, validate: false) diff --git a/test/action_view/cases/test_legacy_form_for_helpers.rb b/test/action_view/cases/test_legacy_form_for_helpers.rb index 35e5e4ef..851e7d0f 100644 --- a/test/action_view/cases/test_legacy_form_for_helpers.rb +++ b/test/action_view/cases/test_legacy_form_for_helpers.rb @@ -32,6 +32,20 @@ def test_text_area assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:textarea) + def test_textarea + form_for(@post) do |f| + concat f.textarea(:cost) + end + + expected = whole_form_for('/posts', 'new_post', 'new_post') do + form_field('textarea', id: 'post_cost', name: 'post[cost]', tag_content: "\n") + end + + assert_dom_equal expected, output_buffer + end + end + def test_file_field form_for(@post) do |f| concat f.file_field(:cost) @@ -57,6 +71,21 @@ def test_check_box assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:checkbox) + def test_checkbox + form_for(@post) do |f| + concat f.checkbox(:cost) + end + + expected = whole_form_for('/posts', 'new_post', 'new_post') do + hidden_input_for_checkbox('post[cost]') + + form_field('input', id: 'post_cost', name: 'post[cost]', type: 'checkbox', value: '1') + end + + assert_dom_equal expected, output_buffer + end + end + def test_radio_button form_for(@post) do |f| concat f.radio_button(:cost, '10') diff --git a/test/action_view/cases/test_legacy_form_with_helpers.rb b/test/action_view/cases/test_legacy_form_with_helpers.rb index 1173950b..3d6513e3 100644 --- a/test/action_view/cases/test_legacy_form_with_helpers.rb +++ b/test/action_view/cases/test_legacy_form_with_helpers.rb @@ -33,6 +33,20 @@ def test_form_with_text_area assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:textarea) + def test_form_with_textarea + form_with(model: @post) do |f| + concat f.textarea(:cost) + end + + expected = whole_form_with('/posts') do + form_field('textarea', name: 'post[cost]', id: 'post_cost', tag_content: "\n") + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_with_file_field form_with(model: @post) do |f| concat f.file_field(:cost) @@ -58,6 +72,21 @@ def test_form_with_check_box assert_dom_equal expected, output_buffer end + if ::ActionView::Helpers::FormBuilder.field_helpers.include?(:checkbox) + def test_form_with_checkbox + form_with(model: @post) do |f| + concat f.checkbox(:cost) + end + + expected = whole_form_with('/posts') do + hidden_input_for_checkbox('post[cost]') + + form_field('input', name: 'post[cost]', id: 'post_cost', type: 'checkbox', value: '1') + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_with_radio_button form_with(model: @post) do |f| concat f.radio_button(:cost, '10') diff --git a/test/active_record/cases/test_uniqueness_validator.rb b/test/active_record/cases/test_uniqueness_validator.rb index f561d396..53c2c73a 100644 --- a/test/active_record/cases/test_uniqueness_validator.rb +++ b/test/active_record/cases/test_uniqueness_validator.rb @@ -7,37 +7,37 @@ class UniquenessValidatorTest < ClientSideValidations::ActiveRecordTestBase def test_uniqueness_client_side_hash expected_hash = { message: 'has already been taken' } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name]).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name]).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_allowing_blank expected_hash = { message: 'has already been taken', allow_blank: true } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], allow_blank: true).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], allow_blank: true).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_allowing_nil expected_hash = { message: 'has already been taken', allow_blank: true } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], allow_nil: true).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], allow_nil: true).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_case_insensitive expected_hash = { message: 'has already been taken' } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], case_sensitive: false).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], case_sensitive: false).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_case_sensitive expected_hash = { message: 'has already been taken', case_sensitive: true } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], case_sensitive: true).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], case_sensitive: true).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_custom_message expected_hash = { message: 'is not available' } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], message: 'is not available').client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], message: 'is not available').client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_existing_record @@ -45,14 +45,14 @@ def test_uniqueness_client_side_hash_with_existing_record @user.stubs(:id).returns(1) expected_hash = { message: 'has already been taken', id: 1 } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name]).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name]).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_single_scope_item @user.stubs(:age).returns(30) @user.stubs(:title).returns('test title') expected_hash = { message: 'has already been taken', scope: { title: 'test title' } } - result_hash = UniquenessValidator.new(attributes: [:name], scope: :title).client_side_hash(@user, :name) + result_hash = UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], scope: :title).client_side_hash(@user, :name) assert_equal expected_hash, result_hash end @@ -61,14 +61,14 @@ def test_uniqueness_client_side_hash_with_multiple_scope_items @user.stubs(:age).returns(30) @user.stubs(:title).returns('test title') expected_hash = { message: 'has already been taken', scope: { age: 30, title: 'test title' } } - result_hash = UniquenessValidator.new(attributes: [:name], scope: %i[age title]).client_side_hash(@user, :name) + result_hash = UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], scope: %i[age title]).client_side_hash(@user, :name) assert_equal expected_hash, result_hash end def test_uniqueness_client_side_hash_with_empty_scope_array expected_hash = { message: 'has already been taken' } - result_hash = UniquenessValidator.new(attributes: [:name], scope: []).client_side_hash(@user, :name) + result_hash = UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], scope: []).client_side_hash(@user, :name) assert_equal expected_hash, result_hash end @@ -77,14 +77,14 @@ def test_uniqueness_client_side_hash_when_nested_module @user = ActiveRecordTestModule::User2.new expected_hash = { message: 'has already been taken', class: 'active_record_test_module/user2' } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name]).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name]).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_class_from_options @user = UserForm.new expected_hash = { message: 'has already been taken', class: 'user' } - assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], client_validations: { class: 'User' }).client_side_hash(@user, :name) + assert_equal expected_hash, UniquenessValidator.new(class: ClientSideValidations::ActiveRecordTestBase, attributes: [:name], client_validations: { class: 'User' }).client_side_hash(@user, :name) end end end