From d0e531cc70328d0f32ebfb7be20d269c59eeab6a Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Mon, 10 Feb 2025 12:02:18 -0800 Subject: [PATCH 1/4] Forms: Improve the frontend date validation --- .../src/contact-form/js/accessible-form.js | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/projects/packages/forms/src/contact-form/js/accessible-form.js b/projects/packages/forms/src/contact-form/js/accessible-form.js index f22ae96e4de87..94f761a1b14eb 100644 --- a/projects/packages/forms/src/contact-form/js/accessible-form.js +++ b/projects/packages/forms/src/contact-form/js/accessible-form.js @@ -256,6 +256,42 @@ const isMultipleChoiceFieldValid = fieldset => { return false; }; +/** + * Validate the date Value based on the format of the date field. + * + * @param {string} value Date value + * @param {string} format Date format + * + * @returns {boolean} + */ +const validateDate = ( value, format ) => { + let year, month, day; + + switch ( format ) { + case 'mm/dd/yy': + [ month, day, year ] = value.split( '/' ).map( Number ); + break; + + case 'dd/mm/yy': + [ day, month, year ] = value.split( '/' ).map( Number ); + break; + + case 'yy-mm-dd': + [ year, month, day ] = value.split( '-' ).map( Number ); + break; + + default: + return false; + } + if ( isNaN( year ) || isNaN( month ) || isNaN( day ) ) { + return false; + } + + const date = new Date( year, month - 1, day ); + + return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; +}; + /** * Check if a Date Picker field is valid. * @param {HTMLInputElement} input Input element @@ -264,15 +300,12 @@ const isMultipleChoiceFieldValid = fieldset => { const isDateFieldValid = input => { const format = input.getAttribute( 'data-format' ); const value = input.value; - const $ = window.jQuery; - if ( value && format && typeof $ !== 'undefined' ) { - try { - $.datepicker.parseDate( format, value ); + if ( value && format ) { + if ( validateDate( value, format ) ) { input.setCustomValidity( '' ); - } catch { + } else { input.setCustomValidity( L10N.invalidDate ); - return false; } } From 97ef0ef9252e58416569c37c8298913cd4e8bbd1 Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Mon, 10 Feb 2025 12:08:15 -0800 Subject: [PATCH 2/4] changelog --- .../forms/changelog/fix-date-validation-in-all-browsers | 4 ++++ .../jetpack/changelog/fix-date-validation-in-all-browsers | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 projects/packages/forms/changelog/fix-date-validation-in-all-browsers create mode 100644 projects/plugins/jetpack/changelog/fix-date-validation-in-all-browsers diff --git a/projects/packages/forms/changelog/fix-date-validation-in-all-browsers b/projects/packages/forms/changelog/fix-date-validation-in-all-browsers new file mode 100644 index 0000000000000..46e2a13b2a210 --- /dev/null +++ b/projects/packages/forms/changelog/fix-date-validation-in-all-browsers @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Forms: updates the way that we valdate date info by removing jquery diff --git a/projects/plugins/jetpack/changelog/fix-date-validation-in-all-browsers b/projects/plugins/jetpack/changelog/fix-date-validation-in-all-browsers new file mode 100644 index 0000000000000..7cd5addcc1484 --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-date-validation-in-all-browsers @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +not useful since it doesn't completly remove jquery from the front end From 8d4d269036c4c2df79445f033638017358a98853 Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Mon, 10 Feb 2025 12:43:05 -0800 Subject: [PATCH 3/4] Add tests --- .../src/contact-form/js/accessible-form.js | 6 +- .../js/contact-form/accessible-form.test.js | 92 +++++++++++++++++++ .../forms/tests/js/contact-form/wp-mocks.js | 8 ++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 projects/packages/forms/tests/js/contact-form/accessible-form.test.js create mode 100644 projects/packages/forms/tests/js/contact-form/wp-mocks.js diff --git a/projects/packages/forms/src/contact-form/js/accessible-form.js b/projects/packages/forms/src/contact-form/js/accessible-form.js index 94f761a1b14eb..2cbfbe3ce3863 100644 --- a/projects/packages/forms/src/contact-form/js/accessible-form.js +++ b/projects/packages/forms/src/contact-form/js/accessible-form.js @@ -264,9 +264,12 @@ const isMultipleChoiceFieldValid = fieldset => { * * @returns {boolean} */ -const validateDate = ( value, format ) => { +export const validateDate = ( value, format ) => { let year, month, day; + if ( ! value ) { + return false; + } switch ( format ) { case 'mm/dd/yy': [ month, day, year ] = value.split( '/' ).map( Number ); @@ -302,6 +305,7 @@ const isDateFieldValid = input => { const value = input.value; if ( value && format ) { + // only test if we have a value and format. if ( validateDate( value, format ) ) { input.setCustomValidity( '' ); } else { diff --git a/projects/packages/forms/tests/js/contact-form/accessible-form.test.js b/projects/packages/forms/tests/js/contact-form/accessible-form.test.js new file mode 100644 index 0000000000000..b69adf5987f09 --- /dev/null +++ b/projects/packages/forms/tests/js/contact-form/accessible-form.test.js @@ -0,0 +1,92 @@ +// Add these mocks at the top of your test file +import './wp-mocks'; +import { validateDate } from '../../../src/contact-form/js/accessible-form'; + +describe( 'validateDate', () => { + // Test mm/dd/yy format + describe( 'mm/dd/yy format', () => { + const format = 'mm/dd/yy'; + + test( 'validates correct dates', () => { + expect( validateDate( '12/31/2023', format ) ).toBe( true ); + expect( validateDate( '01/01/2024', format ) ).toBe( true ); + expect( validateDate( '02/29/2024', format ) ).toBe( true ); // leap year + } ); + + test( 'invalidates incorrect dates', () => { + expect( validateDate( '13/01/2023', format ) ).toBe( false ); // invalid month + expect( validateDate( '00/01/2023', format ) ).toBe( false ); // invalid month + expect( validateDate( '12/32/2023', format ) ).toBe( false ); // invalid day + expect( validateDate( '12/00/2023', format ) ).toBe( false ); // invalid day + expect( validateDate( '02/29/2023', format ) ).toBe( false ); // not a leap year + } ); + + test( 'invalidates malformed inputs', () => { + expect( validateDate( '12-31-2023', format ) ).toBe( false ); // wrong separator + expect( validateDate( '12/31/', format ) ).toBe( false ); // incomplete + expect( validateDate( 'abc', format ) ).toBe( false ); // nonsense input + } ); + } ); + + // Test dd/mm/yy format + describe( 'dd/mm/yy format', () => { + const format = 'dd/mm/yy'; + + test( 'validates correct dates', () => { + expect( validateDate( '31/12/2023', format ) ).toBe( true ); + expect( validateDate( '01/01/2024', format ) ).toBe( true ); + expect( validateDate( '29/02/2024', format ) ).toBe( true ); // leap year + } ); + + test( 'invalidates incorrect dates', () => { + expect( validateDate( '32/12/2023', format ) ).toBe( false ); // invalid day + expect( validateDate( '00/12/2023', format ) ).toBe( false ); // invalid day + expect( validateDate( '31/13/2023', format ) ).toBe( false ); // invalid month + expect( validateDate( '31/00/2023', format ) ).toBe( false ); // invalid month + expect( validateDate( '29/02/2023', format ) ).toBe( false ); // not a leap year + } ); + + test( 'invalidates malformed inputs', () => { + expect( validateDate( '31-12-2023', format ) ).toBe( false ); // wrong separator + expect( validateDate( '31/12/', format ) ).toBe( false ); // incomplete + expect( validateDate( 'abc', format ) ).toBe( false ); // nonsense input + } ); + } ); + + // Test yy-mm-dd format + describe( 'yy-mm-dd format', () => { + const format = 'yy-mm-dd'; + + test( 'validates correct dates', () => { + expect( validateDate( '2023-12-31', format ) ).toBe( true ); + expect( validateDate( '2024-01-01', format ) ).toBe( true ); + expect( validateDate( '2024-02-29', format ) ).toBe( true ); // leap year + } ); + + test( 'invalidates incorrect dates', () => { + expect( validateDate( '2023-13-01', format ) ).toBe( false ); // invalid month + expect( validateDate( '2023-00-01', format ) ).toBe( false ); // invalid month + expect( validateDate( '2023-12-32', format ) ).toBe( false ); // invalid day + expect( validateDate( '2023-12-00', format ) ).toBe( false ); // invalid day + expect( validateDate( '2023-02-29', format ) ).toBe( false ); // not a leap year + } ); + + test( 'invalidates malformed inputs', () => { + expect( validateDate( '2023/12/31', format ) ).toBe( false ); // wrong separator + expect( validateDate( '2023-12-', format ) ).toBe( false ); // incomplete + expect( validateDate( 'abc', format ) ).toBe( false ); // nonsense input + } ); + } ); + + // Test invalid format + test( 'returns false for invalid format', () => { + expect( validateDate( '12/31/2023', 'invalid-format' ) ).toBe( false ); + } ); + + // Test empty/null inputs + test( 'handles empty/null inputs', () => { + expect( validateDate( '', 'mm/dd/yy' ) ).toBe( false ); + expect( validateDate( null, 'mm/dd/yy' ) ).toBe( false ); + expect( validateDate( undefined, 'mm/dd/yy' ) ).toBe( false ); + } ); +} ); diff --git a/projects/packages/forms/tests/js/contact-form/wp-mocks.js b/projects/packages/forms/tests/js/contact-form/wp-mocks.js new file mode 100644 index 0000000000000..f94303ef016c5 --- /dev/null +++ b/projects/packages/forms/tests/js/contact-form/wp-mocks.js @@ -0,0 +1,8 @@ +global.wp = { + i18n: { + __: jest.fn().mockImplementation( str => str ), + _n: jest + .fn() + .mockImplementation( ( single, plural, number ) => ( number === 1 ? single : plural ) ), + }, +}; From 9e61634123d4f4dfa28315614c37efed98d3e4cc Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Mon, 10 Feb 2025 13:24:59 -0800 Subject: [PATCH 4/4] Update to make the code easier to test --- .../src/contact-form/js/accessible-form.js | 42 +------------------ .../src/contact-form/js/validate-helper.js | 38 +++++++++++++++++ ...e-form.test.js => validate-helper.test.js} | 3 +- 3 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 projects/packages/forms/src/contact-form/js/validate-helper.js rename projects/packages/forms/tests/js/contact-form/{accessible-form.test.js => validate-helper.test.js} (97%) diff --git a/projects/packages/forms/src/contact-form/js/accessible-form.js b/projects/packages/forms/src/contact-form/js/accessible-form.js index 2cbfbe3ce3863..3b83a16df1e0c 100644 --- a/projects/packages/forms/src/contact-form/js/accessible-form.js +++ b/projects/packages/forms/src/contact-form/js/accessible-form.js @@ -7,6 +7,8 @@ * (multiple radio buttons) or Multiple Choice fields (multiple checkboxes). */ +import { validateDate } from './validate-helper'; + document.addEventListener( 'DOMContentLoaded', () => { initAllForms(); } ); @@ -256,45 +258,6 @@ const isMultipleChoiceFieldValid = fieldset => { return false; }; -/** - * Validate the date Value based on the format of the date field. - * - * @param {string} value Date value - * @param {string} format Date format - * - * @returns {boolean} - */ -export const validateDate = ( value, format ) => { - let year, month, day; - - if ( ! value ) { - return false; - } - switch ( format ) { - case 'mm/dd/yy': - [ month, day, year ] = value.split( '/' ).map( Number ); - break; - - case 'dd/mm/yy': - [ day, month, year ] = value.split( '/' ).map( Number ); - break; - - case 'yy-mm-dd': - [ year, month, day ] = value.split( '-' ).map( Number ); - break; - - default: - return false; - } - if ( isNaN( year ) || isNaN( month ) || isNaN( day ) ) { - return false; - } - - const date = new Date( year, month - 1, day ); - - return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; -}; - /** * Check if a Date Picker field is valid. * @param {HTMLInputElement} input Input element @@ -305,7 +268,6 @@ const isDateFieldValid = input => { const value = input.value; if ( value && format ) { - // only test if we have a value and format. if ( validateDate( value, format ) ) { input.setCustomValidity( '' ); } else { diff --git a/projects/packages/forms/src/contact-form/js/validate-helper.js b/projects/packages/forms/src/contact-form/js/validate-helper.js new file mode 100644 index 0000000000000..1e90d32c62096 --- /dev/null +++ b/projects/packages/forms/src/contact-form/js/validate-helper.js @@ -0,0 +1,38 @@ +/** + * Validate the date Value based on the format of the date field. + * + * @param {string} value Date value + * @param {string} format Date format + * + * @returns {boolean} + */ +export const validateDate = ( value, format ) => { + let year, month, day; + + if ( ! value ) { + return false; + } + switch ( format ) { + case 'mm/dd/yy': + [ month, day, year ] = value.split( '/' ).map( Number ); + break; + + case 'dd/mm/yy': + [ day, month, year ] = value.split( '/' ).map( Number ); + break; + + case 'yy-mm-dd': + [ year, month, day ] = value.split( '-' ).map( Number ); + break; + + default: + return false; + } + if ( isNaN( year ) || isNaN( month ) || isNaN( day ) ) { + return false; + } + + const date = new Date( year, month - 1, day ); + + return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; +}; diff --git a/projects/packages/forms/tests/js/contact-form/accessible-form.test.js b/projects/packages/forms/tests/js/contact-form/validate-helper.test.js similarity index 97% rename from projects/packages/forms/tests/js/contact-form/accessible-form.test.js rename to projects/packages/forms/tests/js/contact-form/validate-helper.test.js index b69adf5987f09..f17824a3eb8bc 100644 --- a/projects/packages/forms/tests/js/contact-form/accessible-form.test.js +++ b/projects/packages/forms/tests/js/contact-form/validate-helper.test.js @@ -1,6 +1,5 @@ // Add these mocks at the top of your test file -import './wp-mocks'; -import { validateDate } from '../../../src/contact-form/js/accessible-form'; +import { validateDate } from '../../../src/contact-form/js/validate-helper'; describe( 'validateDate', () => { // Test mm/dd/yy format