From 1733422b803fda3de9b40a9fa675ef6bb8b5195e Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Thu, 17 Oct 2024 10:01:12 +0300 Subject: [PATCH] fix(kit): `DateTime` fails to process value without any separators (paste from clipboard) (#1779) --- .../src/support/commands/index.ts | 4 ++ .../src/support/commands/paste.ts | 45 +++++++++++++ .../tests/kit/date-time/date-time-basic.cy.ts | 34 ++++++++++ .../src/lib/masks/date-time/date-time-mask.ts | 12 ++-- .../min-max-date-time-postprocessor.ts | 5 +- .../valid-date-time-preprocessor.ts | 6 +- .../date-time/utils/parse-date-time-string.ts | 29 ++++----- .../tests/parse-date-time-string.spec.ts | 63 +++++++++++++++++++ 8 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 projects/demo-integrations/src/support/commands/paste.ts create mode 100644 projects/kit/src/lib/masks/date-time/utils/tests/parse-date-time-string.spec.ts diff --git a/projects/demo-integrations/src/support/commands/index.ts b/projects/demo-integrations/src/support/commands/index.ts index fac181423..93d101729 100644 --- a/projects/demo-integrations/src/support/commands/index.ts +++ b/projects/demo-integrations/src/support/commands/index.ts @@ -1,5 +1,6 @@ /// +import {paste} from './paste'; import {smartTick} from './smart-tick'; declare global { @@ -9,6 +10,8 @@ declare global { durationMs: number, options?: Parameters[2], ): Chainable; + + paste(value: string): Chainable; } } } @@ -18,3 +21,4 @@ Cypress.Commands.add( {prevSubject: ['optional', 'element', 'window', 'document']}, smartTick, ); +Cypress.Commands.add('paste', {prevSubject: 'element'}, paste); diff --git a/projects/demo-integrations/src/support/commands/paste.ts b/projects/demo-integrations/src/support/commands/paste.ts new file mode 100644 index 000000000..1f2164c47 --- /dev/null +++ b/projects/demo-integrations/src/support/commands/paste.ts @@ -0,0 +1,45 @@ +/** + * Cypress does not have built-in support for pasting from clipboard. + * This utility is VERY approximate alternative for it. + * + * @see https://github.com/cypress-io/cypress/issues/28861 + */ +export function paste( + $subject: T, + data: string, +): ReturnType> { + const inputType = 'insertFromPaste'; + const element = Cypress.dom.unwrap($subject)[0] as + | HTMLInputElement + | HTMLTextAreaElement; + const {value, selectionStart, selectionEnd} = element; + + Cypress.log({ + displayName: 'paste', + message: data, + consoleProps() { + return { + value, + selectionStart, + selectionEnd, + }; + }, + }); + + return cy + .wrap($subject, {log: false}) + .trigger('beforeinput', { + inputType, + data, + log: false, + }) + .invoke( + 'val', + value.slice(0, selectionStart ?? 0) + data + value.slice(selectionEnd ?? 0), + ) + .trigger('input', { + inputType, + data, + log: false, + }); +} diff --git a/projects/demo-integrations/src/tests/kit/date-time/date-time-basic.cy.ts b/projects/demo-integrations/src/tests/kit/date-time/date-time-basic.cy.ts index 42b2982a8..c8a111a69 100644 --- a/projects/demo-integrations/src/tests/kit/date-time/date-time-basic.cy.ts +++ b/projects/demo-integrations/src/tests/kit/date-time/date-time-basic.cy.ts @@ -353,4 +353,38 @@ describe('DateTime | Basic', () => { ); }); }); + + describe('Paste', () => { + it('value without segment separators', () => { + cy.get('@input') + .paste('02112018, 16:20') + .should('have.value', '02.11.2018, 16:20') + .should('have.prop', 'selectionStart', '02.11.2018, 16:20'.length) + .should('have.prop', 'selectionEnd', '02.11.2018, 16:20'.length); + }); + + it('value without separator between date and time', () => { + cy.get('@input') + .paste('02.11.201816:20') + .should('have.value', '02.11.2018, 16:20') + .should('have.prop', 'selectionStart', '02.11.2018, 16:20'.length) + .should('have.prop', 'selectionEnd', '02.11.2018, 16:20'.length); + }); + + it('value with incomplete separator between date and time', () => { + cy.get('@input') + .paste('02.11.2018,16:20') + .should('have.value', '02.11.2018, 16:20') + .should('have.prop', 'selectionStart', '02.11.2018, 16:20'.length) + .should('have.prop', 'selectionEnd', '02.11.2018, 16:20'.length); + }); + + it('value without any separators', () => { + cy.get('@input') + .paste('021120181620') + .should('have.value', '02.11.2018, 16:20') + .should('have.prop', 'selectionStart', '02.11.2018, 16:20'.length) + .should('have.prop', 'selectionEnd', '02.11.2018, 16:20'.length); + }); + }); }); diff --git a/projects/kit/src/lib/masks/date-time/date-time-mask.ts b/projects/kit/src/lib/masks/date-time/date-time-mask.ts index cdf253ad6..6bb36c33e 100644 --- a/projects/kit/src/lib/masks/date-time/date-time-mask.ts +++ b/projects/kit/src/lib/masks/date-time/date-time-mask.ts @@ -87,10 +87,10 @@ export function maskitoDateTimeOptionsGenerator({ timeSegmentMinValues, timeSegmentMaxValues, parseValue: (x) => { - const [dateString, timeString] = parseDateTimeString(x, { + const [dateString, timeString] = parseDateTimeString( + x, dateModeTemplate, - dateTimeSeparator, - }); + ); return {timeString, restValue: dateString + dateTimeSeparator}; }, @@ -109,10 +109,10 @@ export function maskitoDateTimeOptionsGenerator({ dateModeTemplate, dateSegmentSeparator: dateSeparator, splitFn: (value) => { - const [dateString, timeString] = parseDateTimeString(value, { + const [dateString, timeString] = parseDateTimeString( + value, dateModeTemplate, - dateTimeSeparator, - }); + ); return {dateStrings: [dateString], restPart: timeString}; }, diff --git a/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts index cf82026c2..23b0b3e2c 100644 --- a/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts +++ b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts @@ -28,10 +28,7 @@ export function createMinMaxDateTimePostprocessor({ dateTimeSeparator: string; }): MaskitoPostprocessor { return ({value, selection}) => { - const [dateString, timeString] = parseDateTimeString(value, { - dateModeTemplate, - dateTimeSeparator, - }); + const [dateString, timeString] = parseDateTimeString(value, dateModeTemplate); const parsedDate = parseDateString(dateString, dateModeTemplate); const parsedTime = parseTimeString(timeString, timeMode); diff --git a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts index cd15e1d22..5c11c8b80 100644 --- a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts +++ b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts @@ -38,10 +38,10 @@ export function createValidDateTimePreprocessor({ let to = rawTo + data.length; const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); - const [dateString, timeString] = parseDateTimeString(newPossibleValue, { + const [dateString, timeString] = parseDateTimeString( + newPossibleValue, dateModeTemplate, - dateTimeSeparator, - }); + ); let validatedValue = ''; const hasDateTimeSeparator = newPossibleValue.includes(dateTimeSeparator); diff --git a/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts b/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts index 8981e86b6..6b34dc4a6 100644 --- a/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts +++ b/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts @@ -1,21 +1,18 @@ +const NON_DIGIT_PLACEHOLDER_RE = /[^dmy]/g; +const LEADING_NON_DIGIT_RE = /^\D*/; + export function parseDateTimeString( dateTime: string, - { - dateModeTemplate, - dateTimeSeparator, - }: { - dateModeTemplate: string; - dateTimeSeparator: string; - }, + dateModeTemplate: string, ): [date: string, time: string] { - const hasSeparator = dateTime.includes(dateTimeSeparator); + const dateDigitsCount = dateModeTemplate.replaceAll( + NON_DIGIT_PLACEHOLDER_RE, + '', + ).length; + const [date = ''] = + new RegExp(`(\\d[^\\d]*){0,${dateDigitsCount - 1}}\\d?`).exec(dateTime) || []; + const [dateTimeSeparator = ''] = + LEADING_NON_DIGIT_RE.exec(dateTime.slice(date.length)) || []; - return [ - dateTime.slice(0, dateModeTemplate.length), - dateTime.slice( - hasSeparator - ? dateModeTemplate.length + dateTimeSeparator.length - : dateModeTemplate.length, - ), - ]; + return [date, dateTime.slice(date.length + dateTimeSeparator.length)]; } diff --git a/projects/kit/src/lib/masks/date-time/utils/tests/parse-date-time-string.spec.ts b/projects/kit/src/lib/masks/date-time/utils/tests/parse-date-time-string.spec.ts new file mode 100644 index 000000000..683272592 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/utils/tests/parse-date-time-string.spec.ts @@ -0,0 +1,63 @@ +import {parseDateTimeString} from '../parse-date-time-string'; + +describe('parseDateTimeString', () => { + describe('dd.mm.yyyy', () => { + const parse = (value: string): [string, string] => + parseDateTimeString(value, 'dd.mm.yyyy'); + + ( + [ + {input: '', output: ['', '']}, + {input: '02', output: ['02', '']}, + {input: '02.', output: ['02.', '']}, + {input: '0211', output: ['0211', '']}, + {input: '0211.', output: ['0211.', '']}, + {input: '02.112018', output: ['02.112018', '']}, + {input: '02.112018,', output: ['02.112018', '']}, + {input: '02.112018, ', output: ['02.112018', '']}, + {input: '02.11.2018, ', output: ['02.11.2018', '']}, + {input: '021120181620', output: ['02112018', '1620']}, + {input: '02112018,1620', output: ['02112018', '1620']}, + {input: '02112018, 1620', output: ['02112018', '1620']}, + {input: '02112018, 16:20', output: ['02112018', '16:20']}, + {input: '02112018,16:20', output: ['02112018', '16:20']}, + {input: '02.11.2018,1620', output: ['02.11.2018', '1620']}, + {input: '02.11.2018, 1620', output: ['02.11.2018', '1620']}, + {input: '02.11.2018, 16:20', output: ['02.11.2018', '16:20']}, + {input: '02.11.2018,16:20', output: ['02.11.2018', '16:20']}, + ] as const + ).forEach(({input, output}) => { + it(`${input} -> ${JSON.stringify(output)}`, () => { + expect(parse(input)).toEqual(output); + }); + }); + }); + + describe('dd. mm. yyyy (date segment separator consists of space and dot)', () => { + const parse = (value: string): [string, string] => + parseDateTimeString(value, 'dd. mm. yyyy'); + + ( + [ + {input: '', output: ['', '']}, + {input: '02', output: ['02', '']}, + {input: '02.', output: ['02.', '']}, + {input: '02. ', output: ['02. ', '']}, + {input: '0211', output: ['0211', '']}, + {input: '0211. ', output: ['0211. ', '']}, + {input: '02. 112018', output: ['02. 112018', '']}, + {input: '02. 112018,', output: ['02. 112018', '']}, + {input: '02. 112018, ', output: ['02. 112018', '']}, + {input: '02. 11. 2018, ', output: ['02. 11. 2018', '']}, + {input: '02. 11. 2018,1620', output: ['02. 11. 2018', '1620']}, + {input: '02. 11. 2018, 1620', output: ['02. 11. 2018', '1620']}, + {input: '02. 11. 2018, 16:20', output: ['02. 11. 2018', '16:20']}, + {input: '02. 11. 2018,16:20', output: ['02. 11. 2018', '16:20']}, + ] as const + ).forEach(({input, output}) => { + it(`${input} -> ${JSON.stringify(output)}`, () => { + expect(parse(input)).toEqual(output); + }); + }); + }); +});