From 5f497e23b8642eebbbf475f039e34fe7f23e33c9 Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Sun, 19 Apr 2026 16:51:31 +0200 Subject: [PATCH] Align Simple Form with the DOM-first runtime Update the Simple Form JavaScript hooks to match the jQuery-free ClientSideValidations runtime and stop assuming jQuery wrappers in the browser integration. SimpleForm::FormBuilder add/remove hooks now work with native DOM elements. Use closest(), querySelector(), parentElement, and classList to find wrappers and feedback nodes while preserving the existing error markup for the default, Bootstrap 4, and Bootstrap 5 builders. Refresh the QUnit coverage to build fixtures and assert behavior through plain DOM APIs, regenerate the published and vendored assets, switch the browser harness to a neutral QUnit CDN, and remove the remaining jQuery-specific assumptions from the test setup and documentation. Raise the compatibility floor to ClientSideValidations 24.x by requiring client_side_validations >= 24.0 in the gem, setting the npm peer dependency to @client-side-validations/client-side-validations >= 24.0.0, and updating the appraisal matrix to csv-24.0. Bump the gem and npm package versions to 18.0.0 for the release. --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/workflows/ruby.yml | 4 +- .rubocop.yml | 4 + Appraisals | 4 +- CHANGELOG.md | 5 + README.md | 18 +- client_side_validations-simple_form.gemspec | 2 +- dist/simple-form.bootstrap4.esm.js | 10 +- dist/simple-form.bootstrap4.js | 10 +- dist/simple-form.esm.js | 10 +- dist/simple-form.js | 10 +- .../{csv_23.0.gemfile => csv_24.0.gemfile} | 2 +- .../simple_form/version.rb | 2 +- package.json | 14 +- src/index.bootstrap4.js | 8 +- src/index.js | 8 +- .../test/form_builders/validateSimpleForm.js | 202 ++++++++------ .../validateSimpleFormBootstrap.js | 224 +++++++++------- .../validateSimpleFormBootstrap4.js | 246 +++++++++++------- .../validateSimpleFormBootstrap5.js | 241 ++++++++++------- test/javascript/public/test/settings.js | 17 -- test/javascript/server.rb | 8 +- test/javascript/views/index.erb | 3 +- test/javascript/views/layout.erb | 2 +- ...ails.validations.simple_form.bootstrap4.js | 10 +- .../rails.validations.simple_form.js | 10 +- 26 files changed, 624 insertions(+), 454 deletions(-) rename gemfiles/{csv_23.0.gemfile => csv_24.0.gemfile} (90%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5b63966..401ce06 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -38,9 +38,9 @@ _Tell us what happens instead_ - [ ] I confirm that my browser's development console output does not contain errors ### Additional JavaScript Libraries* -_If your issue depends on other JavaScript libraries, please list them here. E.g: *Bootstrap Modal v3.3.7, jQuery UI Datepicker 1.12.4*._ +_If your issue depends on other JavaScript libraries, please list them here. E.g. Bootstrap Modal v3.3.7, flatpickr 4.6.13._ -### Repository demostrating the issue +### Repository demonstrating the issue Debugging CSV issues is a time consuming task. If you want to speed up things, please provide a link to a repository showing the issue. diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 60297bf..5bff5b3 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -16,12 +16,12 @@ jobs: strategy: matrix: ruby-version: ['3.2', '3.3', '3.4', '4.0'] - gemfile: [ csv_23.0 ] + gemfile: [ csv_24.0 ] channel: ['stable'] include: - ruby-version: 'head' - gemfile: csv_23.0 + gemfile: csv_24.0 channel: 'experimental' - ruby-version: '3.2' gemfile: csv_edge diff --git a/.rubocop.yml b/.rubocop.yml index 248033a..4f08b2e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -61,3 +61,7 @@ Style/Documentation: Style/IfUnlessModifier: Enabled: false + +Style/OneClassPerFile: + Exclude: + - 'test/**/*' diff --git a/Appraisals b/Appraisals index aebb5d4..ab2f369 100644 --- a/Appraisals +++ b/Appraisals @@ -1,7 +1,7 @@ # frozen_string_literal: true -appraise 'csv-23.0' do - gem 'client_side_validations', '~> 23.0' +appraise 'csv-24.0' do + gem 'client_side_validations', '~> 24.0' end appraise 'csv-edge' do diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bd369..f83be72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 18.0.0 / 2026-04-19 + +* [FEATURE] Breaking change: Align the Simple Form JavaScript hooks with the DOM-first `ClientSideValidations` runtime +* [ENHANCEMENT] Remove jQuery-specific assumptions from the browser test harness and documentation + ## 17.0.0 / 2026-01-07 * [FEATURE] Drop Internet Explorer and other older browsers support diff --git a/README.md b/README.md index 1181f7b..3fc0def 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,11 @@ required **before** `client_side_validations-simple_form`. Instructions depend on your technology stack. +This package extends the jQuery-free, DOM-first `ClientSideValidations` runtime. + #### When using Webpacker #### -Make sure that you are requiring jQuery and Client Side Validations. +Make sure that you are requiring Client Side Validations before the Simple Form plugin. Add the following package: @@ -58,7 +60,7 @@ require('@client-side-validations/simple-form/dist/simple-form.bootstrap4.esm') #### When using Sprockets #### -Make sure that you are requiring jQuery and Client Side Validations. +Make sure that you are requiring Client Side Validations before the Simple Form plugin. According to the web framework you are using, add **one** of the following lines to your `app/assets/javascripts/application.js`, **after** @@ -80,6 +82,18 @@ rails g client_side_validations:copy_assets Note: If you run `copy_assets`, you will need to run it again each time you update this project. +## Migration Guide ## + +### Removing jQuery Dependency ### + +`client_side_validations-simple_form` now plugs into the DOM-first `ClientSideValidations` runtime and no longer assumes jQuery is present. + +Follow the main `ClientSideValidations` migration guide for the public API changes. In particular, load the current DOM-first `ClientSideValidations` bundle before loading this package, and update any custom code that still expects jQuery-wrapped objects. + +Custom Simple Form builders now receive native DOM elements in their `add` and `remove` hooks, so custom overrides should use DOM APIs such as `.closest()`, `.querySelector()`, and `.classList`. + +If you vendor the compiled assets with `rails g client_side_validations:copy_assets`, run that generator again after upgrading so the copied Simple Form assets stay in sync with the current `ClientSideValidations` bundle. + ## Usage ## The usage is the same as `ClientSideValidations`, just pass `validate: true` to the form builder diff --git a/client_side_validations-simple_form.gemspec b/client_side_validations-simple_form.gemspec index 5ee7ba4..b8971df 100644 --- a/client_side_validations-simple_form.gemspec +++ b/client_side_validations-simple_form.gemspec @@ -27,6 +27,6 @@ Gem::Specification.new do |spec| spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 3.2' - spec.add_dependency 'client_side_validations', '>= 23.0' + spec.add_dependency 'client_side_validations', '>= 24.0' spec.add_dependency 'simple_form', '>= 5.4' end diff --git a/dist/simple-form.bootstrap4.esm.js b/dist/simple-form.bootstrap4.esm.js index 51a41d0..e1d6ebd 100644 --- a/dist/simple-form.bootstrap4.esm.js +++ b/dist/simple-form.bootstrap4.esm.js @@ -1,5 +1,5 @@ /*! - * Client Side Validations Simple Form JS (Default) - v17.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) + * Client Side Validations Simple Form JS (Default) - v18.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) * Copyright (c) 2026 Geremia Taglialatela, Brian Cardarella * Licensed under MIT (https://opensource.org/licenses/mit-license.php) */ @@ -18,11 +18,11 @@ const removeClass = (element, customClass) => { }; ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { - add: function ($element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); + add: function (element, settings, message) { + this.wrapper(settings.wrapper).add.call(this, element, settings, message); }, - remove: function ($element, settings) { - this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); + remove: function (element, settings) { + this.wrapper(settings.wrapper).remove.call(this, element, settings); }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default; diff --git a/dist/simple-form.bootstrap4.js b/dist/simple-form.bootstrap4.js index 9660d50..4044985 100644 --- a/dist/simple-form.bootstrap4.js +++ b/dist/simple-form.bootstrap4.js @@ -1,5 +1,5 @@ /*! - * Client Side Validations Simple Form JS (Bootstrap 4+) - v17.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) + * Client Side Validations Simple Form JS (Bootstrap 4+) - v18.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) * Copyright (c) 2026 Geremia Taglialatela, Brian Cardarella * Licensed under MIT (https://opensource.org/licenses/mit-license.php) */ @@ -22,11 +22,11 @@ }; ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { - add: function ($element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); + add: function (element, settings, message) { + this.wrapper(settings.wrapper).add.call(this, element, settings, message); }, - remove: function ($element, settings) { - this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); + remove: function (element, settings) { + this.wrapper(settings.wrapper).remove.call(this, element, settings); }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default; diff --git a/dist/simple-form.esm.js b/dist/simple-form.esm.js index d8c469d..25ca17e 100644 --- a/dist/simple-form.esm.js +++ b/dist/simple-form.esm.js @@ -1,5 +1,5 @@ /*! - * Client Side Validations Simple Form JS (Default) - v17.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) + * Client Side Validations Simple Form JS (Default) - v18.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) * Copyright (c) 2026 Geremia Taglialatela, Brian Cardarella * Licensed under MIT (https://opensource.org/licenses/mit-license.php) */ @@ -18,11 +18,11 @@ const removeClass = (element, customClass) => { }; ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { - add: function ($element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); + add: function (element, settings, message) { + this.wrapper(settings.wrapper).add.call(this, element, settings, message); }, - remove: function ($element, settings) { - this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); + remove: function (element, settings) { + this.wrapper(settings.wrapper).remove.call(this, element, settings); }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default; diff --git a/dist/simple-form.js b/dist/simple-form.js index bdc9814..51e0017 100644 --- a/dist/simple-form.js +++ b/dist/simple-form.js @@ -1,5 +1,5 @@ /*! - * Client Side Validations Simple Form JS (Default) - v17.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) + * Client Side Validations Simple Form JS (Default) - v18.0.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) * Copyright (c) 2026 Geremia Taglialatela, Brian Cardarella * Licensed under MIT (https://opensource.org/licenses/mit-license.php) */ @@ -22,11 +22,11 @@ }; ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { - add: function ($element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); + add: function (element, settings, message) { + this.wrapper(settings.wrapper).add.call(this, element, settings, message); }, - remove: function ($element, settings) { - this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); + remove: function (element, settings) { + this.wrapper(settings.wrapper).remove.call(this, element, settings); }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default; diff --git a/gemfiles/csv_23.0.gemfile b/gemfiles/csv_24.0.gemfile similarity index 90% rename from gemfiles/csv_23.0.gemfile rename to gemfiles/csv_24.0.gemfile index 384ac3a..fddb0eb 100644 --- a/gemfiles/csv_23.0.gemfile +++ b/gemfiles/csv_24.0.gemfile @@ -19,6 +19,6 @@ gem "simplecov" gem "simplecov-lcov" gem "sinatra" gem "webrick" -gem "client_side_validations", "~> 23.0" +gem "client_side_validations", "~> 24.0" gemspec path: "../" diff --git a/lib/client_side_validations/simple_form/version.rb b/lib/client_side_validations/simple_form/version.rb index dd3a8c9..e469b80 100644 --- a/lib/client_side_validations/simple_form/version.rb +++ b/lib/client_side_validations/simple_form/version.rb @@ -2,6 +2,6 @@ module ClientSideValidations module SimpleForm - VERSION = '17.0.0' + VERSION = '18.0.0' end end diff --git a/package.json b/package.json index 3a7b6ba..804726a 100644 --- a/package.json +++ b/package.json @@ -23,20 +23,20 @@ "test": "test/javascript/run-qunit.mjs" }, "devDependencies": { - "@babel/core": "^7.28.5", - "@babel/preset-env": "^7.28.5", + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.2", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "chrome-launcher": "^1.2.1", "eslint": "^9.39.2", "eslint-plugin-compat": "^6.0.2", - "neostandard": "^0.12.2", - "puppeteer-core": "^24.34.0", - "rollup": "^4.55.1", + "neostandard": "^0.13.0", + "puppeteer-core": "^24.41.0", + "rollup": "^4.60.2", "rollup-plugin-copy": "^3.5.0" }, "peerDependencies": { - "@client-side-validations/client-side-validations": ">=23.0.0" + "@client-side-validations/client-side-validations": ">=24.0.0" }, "main": "dist/simple-form.js", "module": "dist/simple-form.esm.js", @@ -66,7 +66,7 @@ "/vendor/" ] }, - "version": "17.0.0", + "version": "18.0.0", "directories": { "lib": "lib", "test": "test" diff --git a/src/index.bootstrap4.js b/src/index.bootstrap4.js index 2b747ae..e8b7cb3 100644 --- a/src/index.bootstrap4.js +++ b/src/index.bootstrap4.js @@ -2,11 +2,11 @@ import ClientSideValidations from '@client-side-validations/client-side-validati import { addClass, removeClass } from './utils' ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { - add: function ($element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message) + add: function (element, settings, message) { + this.wrapper(settings.wrapper).add.call(this, element, settings, message) }, - remove: function ($element, settings) { - this.wrapper(settings.wrapper).remove.call(this, $element[0], settings) + remove: function (element, settings) { + this.wrapper(settings.wrapper).remove.call(this, element, settings) }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default diff --git a/src/index.js b/src/index.js index eac92fa..3df2711 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,11 @@ import ClientSideValidations from '@client-side-validations/client-side-validati import { addClass, removeClass } from './utils' ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { - add: function ($element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message) + add: function (element, settings, message) { + this.wrapper(settings.wrapper).add.call(this, element, settings, message) }, - remove: function ($element, settings) { - this.wrapper(settings.wrapper).remove.call(this, $element[0], settings) + remove: function (element, settings) { + this.wrapper(settings.wrapper).remove.call(this, element, settings) }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default diff --git a/test/javascript/public/test/form_builders/validateSimpleForm.js b/test/javascript/public/test/form_builders/validateSimpleForm.js index a322e42..c03b093 100644 --- a/test/javascript/public/test/form_builders/validateSimpleForm.js +++ b/test/javascript/public/test/form_builders/validateSimpleForm.js @@ -1,3 +1,5 @@ +var currentFormBuilder + QUnit.module('Validate SimpleForm', { before: function () { currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] @@ -9,6 +11,12 @@ QUnit.module('Validate SimpleForm', { }, beforeEach: function () { + var fixture = document.getElementById('qunit-fixture') + var form = document.createElement('form') + var wrapper = document.createElement('div') + var input = document.createElement('input') + var label = document.createElement('label') + dataCsv = { html_settings: { type: 'SimpleForm::FormBuilder', @@ -20,70 +28,92 @@ QUnit.module('Validate SimpleForm', { wrapper: 'default' }, validators: { - 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] } + 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', with: { options: 'g', source: '\\d+' } }] } } } - $('#qunit-fixture') - .append($('
', { - action: '/users', - 'data-client-side-validations': JSON.stringify(dataCsv), - method: 'post', - id: 'new_user' - })) - .find('form') - .append($('
')) - .find('div') - .append($('', { - name: 'user[name]', - id: 'user_name', - type: 'text' - })) - .append($('')) - $('form#new_user').validate() + form.action = '/users' + form.dataset.clientSideValidations = JSON.stringify(dataCsv) + form.method = 'post' + form.id = 'new_user' + + wrapper.className = 'input' + + input.name = 'user[name]' + input.id = 'user_name' + input.type = 'text' + + label.htmlFor = 'user_name' + label.textContent = 'Name' + + wrapper.appendChild(input) + wrapper.appendChild(label) + form.appendChild(wrapper) + fixture.appendChild(form) + + ClientSideValidations.validate(form) + }, + + afterEach: function () { + document.getElementById('qunit-fixture').replaceChildren() } }) QUnit.test('Validate error attaching and detaching', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - - input.trigger('focusout') - assert.ok(input.parent().hasClass('field_with_errors')) - assert.ok(label.parent().hasClass('field_with_errors')) - assert.ok(input.parent().find('span.error:contains("must be present")')[0]) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().hasClass('field_with_errors')) - assert.ok(label.parent().hasClass('field_with_errors')) - assert.ok(input.parent().find('span.error:contains("is invalid")')[0]) - - input.val('123') - input.trigger('change') - input.trigger('focusout') - assert.notOk(input.parent().hasClass('field_with_errors')) - assert.notOk(label.parent().hasClass('field_with_errors')) - assert.notOk(form.find('span.error')[0]) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(input.parentElement.classList.contains('field_with_errors')) + assert.ok(label.parentElement.classList.contains('field_with_errors')) + assert.ok(input.parentElement.querySelector('span.error').textContent.includes('must be present')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(input.parentElement.classList.contains('field_with_errors')) + assert.ok(label.parentElement.classList.contains('field_with_errors')) + assert.ok(input.parentElement.querySelector('span.error').textContent.includes('is invalid')) + + input.value = '123' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.notOk(input.parentElement.classList.contains('field_with_errors')) + assert.notOk(label.parentElement.classList.contains('field_with_errors')) + assert.notOk(form.querySelector('span.error')) }) QUnit.test('Validate pre-existing error blocks are re-used', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - - input.parent().append($('Error from Server')) - assert.ok(input.parent().find('span.error:contains("Error from Server")')[0]) - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().hasClass('field_with_errors')) - assert.ok(label.parent().hasClass('field_with_errors')) - assert.ok(input.parent().find('span.error:contains("is invalid")').length === 1) - assert.ok(form.find('span.error').length === 1) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var errorElement = document.createElement('span') + + errorElement.className = 'error small' + errorElement.textContent = 'Error from Server' + input.parentElement.appendChild(errorElement) + + assert.ok(input.parentElement.querySelector('span.error').textContent.includes('Error from Server')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + + assert.ok(input.parentElement.classList.contains('field_with_errors')) + assert.ok(label.parentElement.classList.contains('field_with_errors')) + assert.equal(input.parentElement.querySelectorAll('span.error').length, 1) + assert.equal(form.querySelectorAll('span.error').length, 1) + assert.ok(input.parentElement.querySelector('span.error').textContent.includes('is invalid')) }) -QUnit.test("Display error messages when wrapper and error tags have more than two css classes", function (assert) { +QUnit.test('Display error messages when wrapper and error tags have more than two css classes', function (assert) { + var fixture = document.getElementById('qunit-fixture') + var form = document.createElement('form') + var wrapper = document.createElement('div') + var input = document.createElement('input') + var label = document.createElement('label') + dataCsv = { html_settings: { type: 'SimpleForm::FormBuilder', @@ -95,41 +125,41 @@ QUnit.test("Display error messages when wrapper and error tags have more than tw wrapper: 'default' }, validators: { - 'user_2[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] } + 'user_2[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', with: { options: 'g', source: '\\d+' } }] } } } - $('#qunit-fixture') - .html('') - .append($('', { - action: '/users', - 'data-client-side-validations': JSON.stringify(dataCsv), - method: 'post', - id: 'new_user_2' - })) - .find('form') - .append($('
')) - .find('div') - .append($('', { - name: 'user_2[name]', - id: 'user_2_name', - type: 'text' - })) - .append($('')) - $('form#new_user_2').validate() - - var form = $('form#new_user_2') - var input = form.find('input#user_2_name') - - input.val('') - input.trigger('focusout') - - assert.ok(input.parent().hasClass('field_with_errors')) - - input.val('123') - input.trigger('change') - input.trigger('focusout') - - assert.notOk(input.parent().hasClass('field_with_errors')) - assert.notOk(form.find('span.error').length) + fixture.replaceChildren() + + form.action = '/users' + form.dataset.clientSideValidations = JSON.stringify(dataCsv) + form.method = 'post' + form.id = 'new_user_2' + + wrapper.className = 'input wrapper_class_one wrapper_class_two' + + input.name = 'user_2[name]' + input.id = 'user_2_name' + input.type = 'text' + + label.htmlFor = 'user_2_name' + label.textContent = 'Name' + + wrapper.appendChild(input) + wrapper.appendChild(label) + form.appendChild(wrapper) + fixture.appendChild(form) + + ClientSideValidations.validate(form) + + input.dispatchEvent(new Event('focusout', { bubbles: true })) + + assert.ok(input.parentElement.classList.contains('field_with_errors')) + + input.value = '123' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + + assert.notOk(input.parentElement.classList.contains('field_with_errors')) + assert.notOk(form.querySelector('span.error')) }) diff --git a/test/javascript/public/test/form_builders/validateSimpleFormBootstrap.js b/test/javascript/public/test/form_builders/validateSimpleFormBootstrap.js index 23793df..66ffe3b 100644 --- a/test/javascript/public/test/form_builders/validateSimpleFormBootstrap.js +++ b/test/javascript/public/test/form_builders/validateSimpleFormBootstrap.js @@ -1,3 +1,5 @@ +var currentFormBuilder + QUnit.module('Validate SimpleForm Bootstrap', { before: function () { currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] @@ -9,6 +11,19 @@ QUnit.module('Validate SimpleForm Bootstrap', { }, beforeEach: function () { + var fixture = document.getElementById('qunit-fixture') + var form = document.createElement('form') + var formInputs = document.createElement('div') + var nameGroup = document.createElement('div') + var nameLabel = document.createElement('label') + var nameControls = document.createElement('div') + var nameInput = document.createElement('input') + var usernameGroup = document.createElement('div') + var usernameLabel = document.createElement('label') + var inputGroup = document.createElement('div') + var inputGroupAddon = document.createElement('span') + var usernameInput = document.createElement('input') + dataCsv = { html_settings: { type: 'SimpleForm::FormBuilder', @@ -20,118 +35,133 @@ QUnit.module('Validate SimpleForm Bootstrap', { wrapper: 'bootstrap' }, validators: { - 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, + 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', with: { options: 'g', source: '\\d+' } }] }, 'user[username]': { presence: [{ message: 'must be present' }] } } } - $('#qunit-fixture') - .append($('', { - action: '/users', - 'data-client-side-validations': JSON.stringify(dataCsv), - method: 'post', - id: 'new_user' - })) - .find('form') - .append($('
', { - 'class': 'form-inputs' - })) - .find('div') - .append($('
', { - 'class': 'control-group control-group-2 control-group-3 control-group-user-name' - })) - .find('div.control-group-user-name') - .append($('')) - .append($('
', { - 'class': 'controls' - })) - .find('div') - .append($('', { - name: 'user[name]', - id: 'user_name', - type: 'text' - })) - .append($('
', { - 'class': 'control-group control-group-2 control-group-3 control-group-user-username' - })) - .find('div.control-group-user-username') - .append($('')) - .append($('
', { - 'class': 'input-group' - })) - .find('div') - .append($('', { - 'class': 'input-group-addon', - text: '@' - })) - .append($('', { - name: 'user[username]', - id: 'user_username', - type: 'text' - })) - - $('form#new_user').validate() + form.action = '/users' + form.dataset.clientSideValidations = JSON.stringify(dataCsv) + form.method = 'post' + form.id = 'new_user' + + formInputs.className = 'form-inputs' + + nameGroup.className = 'control-group control-group-2 control-group-3 control-group-user-name' + nameLabel.htmlFor = 'user_name' + nameLabel.className = 'string control-label' + nameLabel.textContent = 'Name' + nameControls.className = 'controls' + nameInput.name = 'user[name]' + nameInput.id = 'user_name' + nameInput.type = 'text' + + usernameGroup.className = 'control-group control-group-2 control-group-3 control-group-user-username' + usernameLabel.htmlFor = 'user_username' + usernameLabel.className = 'string control-label' + usernameLabel.textContent = 'Username' + inputGroup.className = 'input-group' + inputGroupAddon.className = 'input-group-addon' + inputGroupAddon.textContent = '@' + usernameInput.name = 'user[username]' + usernameInput.id = 'user_username' + usernameInput.type = 'text' + + nameControls.appendChild(nameInput) + nameGroup.appendChild(nameLabel) + nameGroup.appendChild(nameControls) + + inputGroup.appendChild(inputGroupAddon) + inputGroup.appendChild(usernameInput) + usernameGroup.appendChild(usernameLabel) + usernameGroup.appendChild(inputGroup) + + formInputs.appendChild(nameGroup) + formInputs.appendChild(usernameGroup) + form.appendChild(formInputs) + fixture.appendChild(form) + + ClientSideValidations.validate(form) + }, + + afterEach: function () { + document.getElementById('qunit-fixture').replaceChildren() } }) var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] -for (var i = 0; i < wrappers.length; i++) { - var wrapper = wrappers[i] - +for (const wrapper of wrappers) { QUnit.test(wrapper + ' - Validate error attaching and detaching', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.trigger('focusout') - assert.ok(input.parent().parent().hasClass('error')) - assert.ok(label.parent().hasClass('error')) - assert.ok(input.parent().parent().find('span.help-inline:contains("must be present")')[0]) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().parent().hasClass('error')) - assert.ok(input.parent().parent().find('span.help-inline:contains("is invalid")')[0]) - assert.ok(label.parent().hasClass('error')) - - input.val('123') - input.trigger('change') - input.trigger('focusout') - assert.notOk(input.parent().parent().hasClass('error')) - assert.notOk(input.parent().parent().find('span.help-inline')[0]) - assert.notOk(label.parent().hasClass('error')) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var wrapperElement = input.closest('.control-group-user-name') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(wrapperElement.classList.contains('error')) + assert.ok(label.parentElement.classList.contains('error')) + assert.ok(wrapperElement.querySelector('span.help-inline').textContent.includes('must be present')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(wrapperElement.classList.contains('error')) + assert.ok(wrapperElement.querySelector('span.help-inline').textContent.includes('is invalid')) + assert.ok(label.parentElement.classList.contains('error')) + + input.value = '123' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.notOk(wrapperElement.classList.contains('error')) + assert.notOk(wrapperElement.querySelector('span.help-inline')) + assert.notOk(label.parentElement.classList.contains('error')) }) QUnit.test(wrapper + ' - Validate pre-existing error blocks are re-used', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.parent().append($('Error from Server')) - assert.ok(input.parent().find('span.help-inline:contains("Error from Server")')[0]) - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().parent().hasClass('error')) - assert.ok(label.parent().hasClass('error')) - assert.ok(input.parent().find('span.help-inline:contains("is invalid")').length === 1) - assert.ok(form.find('span.help-inline').length === 1) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var wrapperElement = input.closest('.control-group-user-name') + var errorElement = document.createElement('span') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + errorElement.className = 'help-inline' + errorElement.textContent = 'Error from Server' + input.parentElement.appendChild(errorElement) + + assert.ok(input.parentElement.querySelector('span.help-inline').textContent.includes('Error from Server')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + + assert.ok(wrapperElement.classList.contains('error')) + assert.ok(label.parentElement.classList.contains('error')) + assert.equal(wrapperElement.querySelectorAll('span.help-inline').length, 1) + assert.equal(form.querySelectorAll('span.help-inline').length, 1) + assert.ok(wrapperElement.querySelector('span.help-inline').textContent.includes('is invalid')) }) QUnit.test(wrapper + ' - Validate input-group', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_username') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.trigger('change') - input.trigger('focusout') - assert.ok(input.closest('.input-group').find('span.help-inline').length === 0) - assert.ok(input.closest('.control-group').find('span.help-inline').length === 1) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.closest('.control-group').find('span.help-inline').length === 0) + var form = document.getElementById('new_user') + var input = document.getElementById('user_username') + var wrapperElement = input.closest('.control-group-user-username') + var inputGroup = input.closest('.input-group') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.equal(inputGroup.querySelectorAll('span.help-inline').length, 0) + assert.equal(wrapperElement.querySelectorAll('span.help-inline').length, 1) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.equal(wrapperElement.querySelectorAll('span.help-inline').length, 0) }) } diff --git a/test/javascript/public/test/form_builders/validateSimpleFormBootstrap4.js b/test/javascript/public/test/form_builders/validateSimpleFormBootstrap4.js index 51b8205..28a8bf1 100644 --- a/test/javascript/public/test/form_builders/validateSimpleFormBootstrap4.js +++ b/test/javascript/public/test/form_builders/validateSimpleFormBootstrap4.js @@ -1,3 +1,6 @@ +var currentFormBuilder +var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] + QUnit.module('Validate SimpleForm Bootstrap 4', { before: function () { currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] @@ -9,6 +12,22 @@ QUnit.module('Validate SimpleForm Bootstrap 4', { }, beforeEach: function () { + var fixture = document.getElementById('qunit-fixture') + var form = document.createElement('form') + var nameGroup = document.createElement('div') + var nameLabel = document.createElement('label') + var nameInput = document.createElement('input') + var passwordGroup = document.createElement('div') + var passwordLabel = document.createElement('label') + var passwordInput = document.createElement('input') + var passwordHelp = document.createElement('small') + var usernameGroup = document.createElement('div') + var usernameLabel = document.createElement('label') + var inputGroup = document.createElement('div') + var prepend = document.createElement('div') + var inputGroupText = document.createElement('span') + var usernameInput = document.createElement('input') + dataCsv = { html_settings: { type: 'SimpleForm::FormBuilder', @@ -19,119 +38,156 @@ QUnit.module('Validate SimpleForm Bootstrap 4', { wrapper_class: 'form-group' }, validators: { - 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, + 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', with: { options: 'g', source: '\\d+' } }] }, 'user[username]': { presence: [{ message: 'must be present' }] }, 'user[password]': { presence: [{ message: 'must be present' }] } } } - $('#qunit-fixture') - .append( - $('', { - action: '/users', - 'data-client-side-validations': JSON.stringify(dataCsv), - method: 'post', - id: 'new_user' - }) - .append( - $('
', { 'class': 'form-group' }) - .append( - $('')) - .append( - $('', { 'class': 'form-control', name: 'user[name]', id: 'user_name', type: 'text' }))) - .append( - $('
', { 'class': 'form-group' }) - .append( - $('')) - .append( - $('', { 'class': 'form-control', name: 'user[password]', id: 'user_password', type: 'password' })) - .append( - $('', { 'class': 'form-text text-muted', text: 'Minimum 8 characters' }))) - .append( - $('
', { 'class': 'form-group' }) - .append( - $('')) - .append( - $('
', { 'class': 'input-group' }) - .append( - $('
', { 'class': 'input-group-prepend' }) - .append( - $('', { 'class': 'input-group-text', text: '@' }))) - .append( - $('', { 'class': 'form-control', name: 'user[username]', id: 'user_username', type: 'text' }))))) - - $('form#new_user').validate() + form.action = '/users' + form.dataset.clientSideValidations = JSON.stringify(dataCsv) + form.method = 'post' + form.id = 'new_user' + + nameGroup.className = 'form-group' + nameLabel.htmlFor = 'user_name' + nameLabel.className = 'string form-control-label' + nameLabel.textContent = 'Name' + nameInput.className = 'form-control' + nameInput.name = 'user[name]' + nameInput.id = 'user_name' + nameInput.type = 'text' + + passwordGroup.className = 'form-group' + passwordLabel.htmlFor = 'user_password' + passwordLabel.className = 'string form-control-label' + passwordLabel.textContent = 'Password' + passwordInput.className = 'form-control' + passwordInput.name = 'user[password]' + passwordInput.id = 'user_password' + passwordInput.type = 'password' + passwordHelp.className = 'form-text text-muted' + passwordHelp.textContent = 'Minimum 8 characters' + + usernameGroup.className = 'form-group' + usernameLabel.htmlFor = 'user_username' + usernameLabel.className = 'string control-label' + usernameLabel.textContent = 'Username' + inputGroup.className = 'input-group' + prepend.className = 'input-group-prepend' + inputGroupText.className = 'input-group-text' + inputGroupText.textContent = '@' + usernameInput.className = 'form-control' + usernameInput.name = 'user[username]' + usernameInput.id = 'user_username' + usernameInput.type = 'text' + + nameGroup.appendChild(nameLabel) + nameGroup.appendChild(nameInput) + + passwordGroup.appendChild(passwordLabel) + passwordGroup.appendChild(passwordInput) + passwordGroup.appendChild(passwordHelp) + + prepend.appendChild(inputGroupText) + inputGroup.appendChild(prepend) + inputGroup.appendChild(usernameInput) + usernameGroup.appendChild(usernameLabel) + usernameGroup.appendChild(inputGroup) + + form.appendChild(nameGroup) + form.appendChild(passwordGroup) + form.appendChild(usernameGroup) + fixture.appendChild(form) + + ClientSideValidations.validate(form) + }, + + afterEach: function () { + document.getElementById('qunit-fixture').replaceChildren() } }) -var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] - -for (var i = 0; i < wrappers.length; i++) { - var wrapper = wrappers[i] - +for (const wrapper of wrappers) { QUnit.test(wrapper + ' - Validate error attaching and detaching', function (assert) { - var form = $('form#new_user') - var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.trigger('focusout') - assert.ok(input.parent().hasClass('form-group-invalid')) - assert.ok(label.parent().hasClass('form-group-invalid')) - assert.ok(input.parent().find('div.invalid-feedback:contains("must be present")')[0]) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().hasClass('form-group-invalid')) - assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")')[0]) - assert.ok(input.hasClass('is-invalid')) - - input.val('123') - input.trigger('change') - input.trigger('focusout') - assert.notOk(input.parent().parent().hasClass('form-group-invalid')) - assert.notOk(input.parent().parent().find('span.help-inline')[0]) - assert.notOk(input.hasClass('is-invalid')) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var wrapperElement = input.parentElement + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(wrapperElement.classList.contains('form-group-invalid')) + assert.ok(label.parentElement.classList.contains('form-group-invalid')) + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('must be present')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(wrapperElement.classList.contains('form-group-invalid')) + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('is invalid')) + assert.ok(input.classList.contains('is-invalid')) + + input.value = '123' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.notOk(wrapperElement.classList.contains('form-group-invalid')) + assert.notOk(wrapperElement.querySelector('div.invalid-feedback')) + assert.notOk(input.classList.contains('is-invalid')) }) QUnit.test(wrapper + ' - Validate pre-existing error blocks are re-used', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.parent().append($('
Error from Server')) - assert.ok(input.parent().find('div.invalid-feedback:contains("Error from Server")')[0]) - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().hasClass('form-group-invalid')) - assert.ok(label.parent().hasClass('form-group-invalid')) - assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")').length === 1) - assert.ok(form.find('div.invalid-feedback').length === 1) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var wrapperElement = input.parentElement + var errorElement = document.createElement('div') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + errorElement.className = 'invalid-feedback' + errorElement.textContent = 'Error from Server' + wrapperElement.appendChild(errorElement) + + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('Error from Server')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + + assert.ok(wrapperElement.classList.contains('form-group-invalid')) + assert.ok(label.parentElement.classList.contains('form-group-invalid')) + assert.equal(wrapperElement.querySelectorAll('div.invalid-feedback').length, 1) + assert.equal(form.querySelectorAll('div.invalid-feedback').length, 1) + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('is invalid')) }) QUnit.test(wrapper + ' - Validate input-group', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_username') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.trigger('change') - input.trigger('focusout') - assert.ok(input.closest('.input-group-prepend').find('div.invalid-feedback').length === 0) - assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 1) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 0) + var form = document.getElementById('new_user') + var input = document.getElementById('user_username') + var inputGroup = input.closest('.input-group') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.equal(inputGroup.querySelectorAll('.input-group-prepend .invalid-feedback').length, 0) + assert.equal(inputGroup.querySelectorAll('div.invalid-feedback').length, 1) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.equal(inputGroup.querySelectorAll('div.invalid-feedback').length, 0) }) QUnit.test(wrapper + ' - Inserts before form texts', function (assert) { - var form = $('form#new_user') - var input = form.find('input#user_password') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper + var form = document.getElementById('new_user') + var input = document.getElementById('user_password') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper - input.trigger('focusout') - assert.ok(input.parent().find('.invalid-feedback:contains("must be present") + .form-text')[0]) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(input.parentElement.querySelector('.invalid-feedback + .form-text')) }) } diff --git a/test/javascript/public/test/form_builders/validateSimpleFormBootstrap5.js b/test/javascript/public/test/form_builders/validateSimpleFormBootstrap5.js index 7417547..f7d8a27 100644 --- a/test/javascript/public/test/form_builders/validateSimpleFormBootstrap5.js +++ b/test/javascript/public/test/form_builders/validateSimpleFormBootstrap5.js @@ -1,3 +1,6 @@ +var currentFormBuilder +var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] + QUnit.module('Validate SimpleForm Bootstrap 5', { before: function () { currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] @@ -9,6 +12,21 @@ QUnit.module('Validate SimpleForm Bootstrap 5', { }, beforeEach: function () { + var fixture = document.getElementById('qunit-fixture') + var form = document.createElement('form') + var nameGroup = document.createElement('div') + var nameLabel = document.createElement('label') + var nameInput = document.createElement('input') + var passwordGroup = document.createElement('div') + var passwordLabel = document.createElement('label') + var passwordInput = document.createElement('input') + var passwordHelp = document.createElement('div') + var usernameGroup = document.createElement('div') + var usernameLabel = document.createElement('label') + var inputGroup = document.createElement('div') + var inputGroupText = document.createElement('span') + var usernameInput = document.createElement('input') + dataCsv = { html_settings: { type: 'SimpleForm::FormBuilder', @@ -19,117 +37,154 @@ QUnit.module('Validate SimpleForm Bootstrap 5', { wrapper_class: 'mb-3' }, validators: { - 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, + 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', with: { options: 'g', source: '\\d+' } }] }, 'user[username]': { presence: [{ message: 'must be present' }] }, 'user[password]': { presence: [{ message: 'must be present' }] } } } - $('#qunit-fixture') - .append( - $('', { - action: '/users', - 'data-client-side-validations': JSON.stringify(dataCsv), - method: 'post', - id: 'new_user' - }) - .append( - $('
', { 'class': 'mb-3' }) - .append( - $('')) - .append( - $('', { 'class': 'form-control', name: 'user[name]', id: 'user_name', type: 'text' }))) - .append( - $('
', { 'class': 'mb-3' }) - .append( - $('')) - .append( - $('', { 'class': 'form-control', name: 'user[password]', id: 'user_password', type: 'password' })) - .append( - $('
', { 'class': 'form-text', text: 'Minimum 8 characters' }))) - .append( - $('
', { 'class': 'mb-3' }) - .append( - $('')) - .append( - $('
', { 'class': 'input-group' }) - .append( - $('', { 'class': 'input-group-text', text: '@' })) - .append( - $('', { 'class': 'form-control', name: 'user[username]', id: 'user_username', type: 'text' }))))) - - $('form#new_user').validate() + form.action = '/users' + form.dataset.clientSideValidations = JSON.stringify(dataCsv) + form.method = 'post' + form.id = 'new_user' + + nameGroup.className = 'mb-3' + nameLabel.htmlFor = 'user_name' + nameLabel.className = 'string form-label' + nameLabel.textContent = 'Name' + nameInput.className = 'form-control' + nameInput.name = 'user[name]' + nameInput.id = 'user_name' + nameInput.type = 'text' + + passwordGroup.className = 'mb-3' + passwordLabel.htmlFor = 'user_password' + passwordLabel.className = 'string form-label' + passwordLabel.textContent = 'Password' + passwordInput.className = 'form-control' + passwordInput.name = 'user[password]' + passwordInput.id = 'user_password' + passwordInput.type = 'password' + passwordHelp.className = 'form-text' + passwordHelp.textContent = 'Minimum 8 characters' + + usernameGroup.className = 'mb-3' + usernameLabel.htmlFor = 'user_username' + usernameLabel.className = 'string form-label' + usernameLabel.textContent = 'Username' + inputGroup.className = 'input-group' + inputGroupText.className = 'input-group-text' + inputGroupText.textContent = '@' + usernameInput.className = 'form-control' + usernameInput.name = 'user[username]' + usernameInput.id = 'user_username' + usernameInput.type = 'text' + + nameGroup.appendChild(nameLabel) + nameGroup.appendChild(nameInput) + + passwordGroup.appendChild(passwordLabel) + passwordGroup.appendChild(passwordInput) + passwordGroup.appendChild(passwordHelp) + + inputGroup.appendChild(inputGroupText) + inputGroup.appendChild(usernameInput) + usernameGroup.appendChild(usernameLabel) + usernameGroup.appendChild(inputGroup) + + form.appendChild(nameGroup) + form.appendChild(passwordGroup) + form.appendChild(usernameGroup) + fixture.appendChild(form) + + ClientSideValidations.validate(form) + }, + + afterEach: function () { + document.getElementById('qunit-fixture').replaceChildren() } }) -var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] - -for (var i = 0; i < wrappers.length; i++) { - var wrapper = wrappers[i] - +for (const wrapper of wrappers) { QUnit.test(wrapper + ' - Validate error attaching and detaching', function (assert) { - var form = $('form#new_user') - var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.trigger('focusout') - assert.ok(input.parent().hasClass('form-group-invalid')) - assert.ok(label.parent().hasClass('form-group-invalid')) - assert.ok(input.parent().find('div.invalid-feedback:contains("must be present")')[0]) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().hasClass('form-group-invalid')) - assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")')[0]) - assert.ok(input.hasClass('is-invalid')) - - input.val('123') - input.trigger('change') - input.trigger('focusout') - assert.notOk(input.parent().parent().hasClass('form-group-invalid')) - assert.notOk(input.parent().parent().find('span.help-inline')[0]) - assert.notOk(input.hasClass('is-invalid')) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var wrapperElement = input.parentElement + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(wrapperElement.classList.contains('form-group-invalid')) + assert.ok(label.parentElement.classList.contains('form-group-invalid')) + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('must be present')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(wrapperElement.classList.contains('form-group-invalid')) + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('is invalid')) + assert.ok(input.classList.contains('is-invalid')) + + input.value = '123' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.notOk(wrapperElement.classList.contains('form-group-invalid')) + assert.notOk(wrapperElement.querySelector('div.invalid-feedback')) + assert.notOk(input.classList.contains('is-invalid')) }) QUnit.test(wrapper + ' - Validate pre-existing error blocks are re-used', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_name') - var label = $('label[for="user_name"]') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.parent().append($('
Error from Server')) - assert.ok(input.parent().find('div.invalid-feedback:contains("Error from Server")')[0]) - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.parent().hasClass('form-group-invalid')) - assert.ok(label.parent().hasClass('form-group-invalid')) - assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")').length === 1) - assert.ok(form.find('div.invalid-feedback').length === 1) + var form = document.getElementById('new_user') + var input = document.getElementById('user_name') + var label = form.querySelector('label[for="user_name"]') + var wrapperElement = input.parentElement + var errorElement = document.createElement('div') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + errorElement.className = 'invalid-feedback' + errorElement.textContent = 'Error from Server' + wrapperElement.appendChild(errorElement) + + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('Error from Server')) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + + assert.ok(wrapperElement.classList.contains('form-group-invalid')) + assert.ok(label.parentElement.classList.contains('form-group-invalid')) + assert.equal(wrapperElement.querySelectorAll('div.invalid-feedback').length, 1) + assert.equal(form.querySelectorAll('div.invalid-feedback').length, 1) + assert.ok(wrapperElement.querySelector('div.invalid-feedback').textContent.includes('is invalid')) }) QUnit.test(wrapper + ' - Validate input-group', function (assert) { - var form = $('form#new_user'); var input = form.find('input#user_username') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper - - input.trigger('change') - input.trigger('focusout') - assert.ok(input.closest('.input-group-prepend').find('div.invalid-feedback').length === 0) - assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 1) - - input.val('abc') - input.trigger('change') - input.trigger('focusout') - assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 0) + var form = document.getElementById('new_user') + var input = document.getElementById('user_username') + var inputGroup = input.closest('.input-group') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper + + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.equal(inputGroup.querySelectorAll('.input-group-text .invalid-feedback').length, 0) + assert.equal(inputGroup.querySelectorAll('div.invalid-feedback').length, 1) + + input.value = 'abc' + input.dispatchEvent(new Event('change', { bubbles: true })) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.equal(inputGroup.querySelectorAll('div.invalid-feedback').length, 0) }) QUnit.test(wrapper + ' - Inserts before form texts', function (assert) { - var form = $('form#new_user') - var input = form.find('input#user_password') - form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper + var form = document.getElementById('new_user') + var input = document.getElementById('user_password') + + form.ClientSideValidations.settings.html_settings.wrapper = wrapper - input.trigger('focusout') - assert.ok(input.parent().find('.invalid-feedback:contains("must be present") + .form-text')[0]) + input.dispatchEvent(new Event('focusout', { bubbles: true })) + assert.ok(input.parentElement.querySelector('.invalid-feedback + .form-text')) }) } diff --git a/test/javascript/public/test/settings.js b/test/javascript/public/test/settings.js index 0b40f98..0ab0669 100644 --- a/test/javascript/public/test/settings.js +++ b/test/javascript/public/test/settings.js @@ -1,18 +1 @@ QUnit.config.autostart = window.location.search.search('autostart=false') < 0 - -/* Hijacks normal form submit; lets it submit to an iframe to prevent - * navigating away from the test suite - */ -$(document).on('submit', function (e) { - if (!e.isDefaultPrevented()) { - var form = $(e.target) - var action = form.attr('action') - var name = 'form-frame' + jQuery.guid++ - var iframe = $('