From e7a63ea20e01293d9780cc9a318adb175e30dce4 Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Mon, 14 Oct 2024 16:27:29 +0300 Subject: [PATCH 1/2] fix(kit): `DateTime` fails to process value without any separators (paste from clipboard) --- .../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 | 33 ++++++++++++++ 8 files changed, 139 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..f278c2547 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..c2407a3fc --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/utils/tests/parse-date-time-string.spec.ts @@ -0,0 +1,33 @@ +import {parseDateTimeString} from '../parse-date-time-string'; + +describe('parseDateTimeString', () => { + 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); + }); + }); +}); From 786fb5dd8c8f4088ff3508be6e9295a7ae521825 Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Wed, 16 Oct 2024 16:11:16 +0300 Subject: [PATCH 2/2] chore: fix case for multi-character date segment separator --- .../date-time/utils/parse-date-time-string.ts | 2 +- .../tests/parse-date-time-string.spec.ts | 82 +++++++++++++------ 2 files changed, 57 insertions(+), 27 deletions(-) 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 f278c2547..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 @@ -10,7 +10,7 @@ export function parseDateTimeString( '', ).length; const [date = ''] = - new RegExp(`(\\d\\D?){0,${dateDigitsCount - 1}}\\d?`).exec(dateTime) || []; + new RegExp(`(\\d[^\\d]*){0,${dateDigitsCount - 1}}\\d?`).exec(dateTime) || []; const [dateTimeSeparator = ''] = LEADING_NON_DIGIT_RE.exec(dateTime.slice(date.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 index c2407a3fc..683272592 100644 --- 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 @@ -1,33 +1,63 @@ import {parseDateTimeString} from '../parse-date-time-string'; describe('parseDateTimeString', () => { - const parse = (value: string): [string, string] => - parseDateTimeString(value, 'dd.mm.yyyy'); + 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); + ( + [ + {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); + }); }); }); });