diff --git a/semcore/input-number/__tests__/index.test.tsx b/semcore/input-number/__tests__/index.test.tsx index be3896e7c7..88604c9aee 100644 --- a/semcore/input-number/__tests__/index.test.tsx +++ b/semcore/input-number/__tests__/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import InputNumber from '../src'; import type { NSInputNumber } from '../src'; +import { localizedMessages } from '../src/translations/__intergalactic-dynamic-locales'; describe('input-number Dependency imports', () => { runDependencyCheckTests('input-number'); @@ -178,78 +179,161 @@ describe('InputNumber', () => { expect(input.value).toBe('1.00'); }); - test.sequential('Verify int numbers', async () => { + test.sequential('Verify English locale accepts comma as an alternative decimal separator', async () => { const spy = vi.fn(); const { getByTestId } = render( - + , ); - const input = getByTestId('input1'); - await focusInput(input); - await userEvent.keyboard('123'); - expect(spy).lastCalledWith('123', expect.anything()); - }); - test.sequential('Verify float numbers', async () => { - const spy = vi.fn(); - const { getByTestId } = render( - - - , - ); - const input = getByTestId('input2'); - await focusInput(input); - await userEvent.keyboard('123.4'); - expect(spy).lastCalledWith('123.4', expect.anything()); + const input = getByTestId('input3333') as HTMLInputElement; + await userEvent.keyboard('[Tab]'); + await userEvent.keyboard('12345,99'); + + expect(spy).lastCalledWith('12345.99', expect.anything()); + expect(input.value).toBe('12,345.99'); }); - test.concurrent('Verify format in int numbers', async () => { - const spy = vi.fn(); - const { getByTestId } = render( - - - , - ); + const localeGroups = [ + { + locales: ['en', 'ja', 'ko', 'zh'], + thousandsSeparator: ',', + decimalSeparator: '.', + groupFourDigitNumbers: true, + }, + { + locales: ['de', 'nl', 'pt', 'tr', 'vi'], + thousandsSeparator: '.', + decimalSeparator: ',', + groupFourDigitNumbers: true, + }, + { + locales: ['es', 'it'], + thousandsSeparator: '.', + decimalSeparator: ',', + groupFourDigitNumbers: false, + }, + { + locales: ['fr'], + thousandsSeparator: '\u202F', + decimalSeparator: ',', + groupFourDigitNumbers: true, + }, + { + locales: ['sv'], + thousandsSeparator: '\u00A0', + decimalSeparator: ',', + groupFourDigitNumbers: true, + }, + { + locales: ['pl'], + thousandsSeparator: '\u00A0', + decimalSeparator: ',', + groupFourDigitNumbers: false, + }, + ]; + + test('Verify locale separator cases cover every supported locale', () => { + const testedLocales = localeGroups.flatMap(({ locales }) => locales).sort(); + const supportedLocales = Object.keys(localizedMessages).sort(); + + expect(testedLocales).toEqual(supportedLocales); + }); + + for (const { + locales, + thousandsSeparator, + decimalSeparator, + groupFourDigitNumbers, + } of localeGroups) { + for (const locale of locales) { + test.sequential(`Verify locale=${locale} thousands and decimal separators`, async () => { + const spy = vi.fn(); + const { getByTestId } = render( + + + , + ); + const input = getByTestId('localized-input') as HTMLInputElement; - const input = getByTestId('input3') as HTMLInputElement; - await userEvent.keyboard('[Tab]'); - await userEvent.keyboard('12345'); + await focusInput(input); + await userEvent.keyboard('1000'); - expect(spy).lastCalledWith('12345', expect.anything()); - expect(input.value).toBe('12,345'); - }); + const expectedFourDigitValue = groupFourDigitNumbers + ? `1${thousandsSeparator}000` + : '1000'; + expect(input.value, `Unexpected four-digit grouping for locale "${locale}"`) + .toBe(expectedFourDigitValue); - test.sequential('Verify format in float numbers', async () => { - const spy = vi.fn(); - const { getByTestId } = render( - - - , - ); + await userEvent.keyboard('0'); - const input = getByTestId('input4') as HTMLInputElement; - await focusInput(input); - await userEvent.keyboard('12345.4'); - expect(spy).lastCalledWith('12345.4', expect.anything()); - expect(input.value).toBe('12,345.4'); - }); + expect(input.value, `Unexpected five-digit grouping for locale "${locale}"`) + .toBe(`10${thousandsSeparator}000`); - test.sequential('Verify not locale decimal separator', async () => { - const spy = vi.fn(); - const { getByTestId } = render( - - - , + await userEvent.keyboard('0'); + + expect(spy).lastCalledWith('100000', expect.anything()); + expect(input.value, `Unexpected thousands separator for locale "${locale}"`) + .toBe(`100${thousandsSeparator}000`); + + await userEvent.keyboard(`${decimalSeparator}99`); + + expect(spy.mock.lastCall?.[0], `Unexpected parsed value for locale "${locale}"`) + .toBe('100000.99'); + expect(input.value, `Unexpected decimal separator for locale "${locale}"`) + .toBe(`100${thousandsSeparator}000${decimalSeparator}99`); + }); + } + } + + const ControlledLocalized = ({ + spy, + locale, + }: { + spy: (value: string, event?: React.SyntheticEvent) => void; + locale: string; + }) => { + const [value, setValue] = React.useState(''); + + return ( + + { + spy(nextValue, event); + setValue(nextValue); + }} + /> + ); + }; - const input = getByTestId('input3333') as HTMLInputElement; - await userEvent.keyboard('[Tab]'); - await userEvent.keyboard('12345,99'); + for (const { locales, thousandsSeparator } of localeGroups) { + for (const locale of locales) { + test.sequential(`Verify locale=${locale} keeps the typed value after blur`, async () => { + const spy = vi.fn(); + const { getByTestId } = render(); + const input = getByTestId('localized-blur-input') as HTMLInputElement; - expect(spy).lastCalledWith('12345.99', expect.anything()); - expect(input.value).toBe('12,345.99'); - }); + await focusInput(input); + await userEvent.keyboard('100000'); + + expect(spy.mock.lastCall?.[0], `Unexpected parsed value before blur for locale "${locale}"`) + .toBe('100000'); + expect(input.value, `Unexpected display value before blur for locale "${locale}"`) + .toBe(`100${thousandsSeparator}000`); + + await userEvent.tab(); + + expect(spy.mock.lastCall?.[0], `Parsed value corrupted on blur for locale "${locale}"`) + .toBe('100000'); + expect(input.value, `Display value corrupted on blur for locale "${locale}"`) + .toBe(`100${thousandsSeparator}000`); + }); + } + } test.sequential('Verify format in hundredths fractions numbers', async () => { const spy = vi.fn(); diff --git a/semcore/input-number/src/InputNumber.tsx b/semcore/input-number/src/InputNumber.tsx index 327f00fb33..2c1d51ee29 100644 --- a/semcore/input-number/src/InputNumber.tsx +++ b/semcore/input-number/src/InputNumber.tsx @@ -149,7 +149,7 @@ class Value extends Component< get separatorThousands() { const { numberFormatter } = this.props; - return numberFormatter.format(1111).replace(/\d/g, ''); + return numberFormatter.format(11111).replace(/\d/g, ''); } onPropsChange(changedProps: { value?: string | number | null }) { @@ -167,7 +167,8 @@ class Value extends Component< getFormattedValue = (value: string) => { return value .replace(new RegExp(`[${this.separatorThousands}]`, 'g'), '') - .replace(this.separatorDecimal, '.'); + .replace(this.separatorDecimal, '.') + .replace(new RegExp('\\.\\.', 'g'), '.'); }; valueParser = ( @@ -176,8 +177,7 @@ class Value extends Component< prevDisplayValue: NSInputNumber.Value.State['displayValue'], ) => { const { numberFormatter } = this.props; - - const stringNumber = this.getFormattedValue(String(value)); + const stringNumber = String(value); if (Number.isNaN(Number(stringNumber))) { return { @@ -189,7 +189,7 @@ class Value extends Component< let displayValue = ''; if (/\.[0-9]*0$/.test(stringNumber)) { - const [int, decimal] = stringNumber.split(this.separatorDecimal); + const [int, decimal] = stringNumber.split('.'); displayValue = numberFormatter.format(+int) + this.separatorDecimal + decimal; } else if (stringNumber !== '') { displayValue = numberFormatter.format(+stringNumber); diff --git a/stories/components/input-number/tests/input-number.stories.tsx b/stories/components/input-number/tests/input-number.stories.tsx index aae12cfce6..7a89389e46 100644 --- a/stories/components/input-number/tests/input-number.stories.tsx +++ b/stories/components/input-number/tests/input-number.stories.tsx @@ -21,7 +21,7 @@ export const Basic: StoryObj = { }, locale: { control: { type: 'select' }, - options: ['de', 'ko'], + options: ['en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'sv', 'tr', 'vi', 'zh'], }, disabledValue: { control: { type: 'boolean' },