From 18a6faf02de76985a5bddb3f930e38d723ca6035 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Sat, 13 Dec 2025 02:47:19 -0500 Subject: [PATCH 1/7] adding more tests --- tests/base.spec.ts | 302 ++++++++++++++++++++++++++++++++++++++- tests/input-type.spec.ts | 64 +++++++++ 2 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 tests/input-type.spec.ts diff --git a/tests/base.spec.ts b/tests/base.spec.ts index 875c8d2..4ce2121 100644 --- a/tests/base.spec.ts +++ b/tests/base.spec.ts @@ -21,4 +21,304 @@ test.describe('base tests', () => { }); }); -// TODO: add tests for each of the possible parameters +test.describe('component parameters', () => { + test.beforeEach(async ({ page }) => { + await page.goto(fileUrl); + }); + + test.describe('prefix and suffix', () => { + test('renders with default prefix $ and suffix USD', async ({ page }) => { + const currencyInput = await page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('$0.00 USD'); + }); + + test('renders with custom prefix', async ({ page }) => { + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill('£'); + await suffixInput.fill(''); + await applyBtn.click(); + + const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('£0.00'); + }); + + test('renders with custom suffix', async ({ page }) => { + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill(''); + await suffixInput.fill(' EUR'); + await applyBtn.click(); + + const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('0.00 EUR'); + }); + + test('renders with both custom prefix and suffix', async ({ page }) => { + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill('¥'); + await suffixInput.fill(' JPY'); + await applyBtn.click(); + + const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('¥0.00 JPY'); + }); + }); + + test.describe('separators and precision', () => { + test('default separators are comma thousand and period decimal', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + const decimalInput = page.locator('[name=decimalSeparator]'); + const thousandInput = page.locator('[name=thousandSeparator]'); + + // Verify defaults + const decimalValue = await decimalInput.inputValue(); + const thousandValue = await thousandInput.inputValue(); + expect(decimalValue).toBe('.'); + expect(thousandValue).toBe(','); + }); + + test('custom decimal separator', async ({ page }) => { + const decimalInput = page.locator('[name=decimalSeparator]'); + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill(''); + await suffixInput.fill(''); + await decimalInput.fill(','); + await applyBtn.click(); + + await page.waitForTimeout(100); + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('12345'); + + await expect(currencyInput).toHaveValue('123,45'); + }); + + test('custom thousand separator', async ({ page }) => { + const thousandInput = page.locator('[name=thousandSeparator]'); + const precisionInput = page.locator('[name=precision]'); + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const decimalInput = page.locator('[name=decimalSeparator]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill(''); + await suffixInput.fill(''); + await decimalInput.fill(','); + await thousandInput.fill('.'); + await precisionInput.fill('2'); + await applyBtn.click(); + + await page.waitForTimeout(100); + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('1234567'); + + // With custom separators, should format as 1.234.567,00 + const value = await currencyInput.inputValue(); + expect(value).toContain('.'); + expect(value).toContain(','); + }); + + test('precision 0', async ({ page }) => { + const precisionInput = page.locator('[name=precision]'); + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const decimalInput = page.locator('[name=decimalSeparator]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill(''); + await suffixInput.fill(''); + await decimalInput.fill('.'); + await precisionInput.fill('0'); + await applyBtn.click(); + + await page.waitForTimeout(100); + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('12345'); + + await expect(currencyInput).toHaveValue('12,345'); + }); + + test('precision 3', async ({ page }) => { + const precisionInput = page.locator('[name=precision]'); + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const decimalInput = page.locator('[name=decimalSeparator]'); + const applyBtn = page.locator('[name=apply]'); + + await prefixInput.fill(''); + await suffixInput.fill(''); + await decimalInput.fill('.'); + await precisionInput.fill('3'); + await applyBtn.click(); + + await page.waitForTimeout(100); + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('12345'); + + await expect(currencyInput).toHaveValue('12.345'); + }); + }); + + test.describe('allowNegative', () => { + test('rejects negative input when allowNegative is false', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('100'); + + // Should not contain minus sign since allowNegative is false by default + const value = await currencyInput.inputValue(); + expect(value).not.toContain('-'); + }); + + test('allows typing when allowNegative is false', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('100'); + + // Should have numeric content + const value = await currencyInput.inputValue(); + expect(value).toMatch(/\d/); + }); + }); + + test.describe('allowEmpty', () => { + test('does not allow empty value when allowEmpty is false', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + + // Should default to 0.00 when allowEmpty is false + await expect(currencyInput).toHaveValue('$0.00 USD'); + }); + + test('maintains default value after clearing', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('100'); + + // Type something first + const valueBefore = await currencyInput.inputValue(); + expect(valueBefore).toContain('1'); + + // Clear and it should go back to 0 + await currencyInput.fill(''); + await expect(currencyInput).toHaveValue('$0.00 USD'); + }); + }); + + test.describe('selectAllOnFocus', () => { + test('caret position is managed at end of input content', async ({ page }) => { + const selectAllCheckbox = page.locator('[name=selectAllOnFocus]'); + const applyBtn = page.locator('[name=apply]'); + + await selectAllCheckbox.check(); + await applyBtn.click(); + + await page.waitForTimeout(100); + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + + // With selectAllOnFocus, all text should be selected + // The selection should encompass the content + const inputValue = await currencyInput.inputValue(); + const selectionStart = await currencyInput.evaluate((el: HTMLInputElement) => el.selectionStart); + const selectionEnd = await currencyInput.evaluate((el: HTMLInputElement) => el.selectionEnd); + + // Should have selected content + expect(selectionEnd - selectionStart).toBeGreaterThan(0); + }); + }); + + test.describe('input type', () => { + test('can set input type to email', async ({ page }) => { + const inputTypeField = page.locator('[name=inputType]'); + const applyBtn = page.locator('[name=apply]'); + + await inputTypeField.fill('email'); + await applyBtn.click(); + + const currencyInput = page.locator('#currency-input'); + const type = await currencyInput.getAttribute('type'); + + expect(type).toBe('email'); + }); + + test('can set input type back to text', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + const type = await currencyInput.getAttribute('type'); + + expect(type).toBe('text'); + }); + }); + + test.describe('basic input and formatting', () => { + test('typing numbers formats correctly', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('100'); + + // With precision 2, "100" becomes $1.00 + await expect(currencyInput).toHaveValue('$1.00 USD'); + }); + + test('backspace removes digits correctly', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('100'); + + await expect(currencyInput).toHaveValue('$1.00 USD'); + + await currencyInput.press('Backspace'); + await expect(currencyInput).toHaveValue('$0.10 USD'); + }); + + test('decimal point insertion respects precision', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('123'); + + // With precision 2, 123 should be $1.23 + await expect(currencyInput).toHaveValue('$1.23 USD'); + }); + }); + + test.describe('element attributes', () => { + test('has correct id attribute', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + const id = await currencyInput.getAttribute('id'); + + expect(id).toBe('currency-input'); + }); + + test('null-input-test element has correct id', async ({ page }) => { + const nullInputTest = page.locator('#null-input-test'); + const id = await nullInputTest.getAttribute('id'); + + expect(id).toBe('null-input-test'); + }); + }); +}); diff --git a/tests/input-type.spec.ts b/tests/input-type.spec.ts new file mode 100644 index 0000000..f8e9e7d --- /dev/null +++ b/tests/input-type.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; +import * as path from 'path'; + +const projectDir = path.resolve(__dirname, '../'); +const filePath = path.join(projectDir, 'examples/index.html'); +const fileUrl = `file://${filePath}`; + +test.describe('input type variations', () => { + test.beforeEach(async ({ page }) => { + await page.goto(fileUrl); + }); + + test('default input type is text', async ({ page }) => { + const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveAttribute('type', 'text'); + }); + + test('set input type to tel for mobile keypad', async ({ page }) => { + const inputTypeField = page.locator('[name=inputType]'); + const applyBtn = page.locator('[name=apply]'); + + await inputTypeField.fill('tel'); + await applyBtn.click(); + + const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveAttribute('type', 'tel'); + + // Ensure formatting still works + await currencyInput.focus(); + await currencyInput.fill(''); + await currencyInput.type('123'); + await expect(currencyInput).toHaveValue('$1.23 USD'); + }); + + test('set input type to number (native numeric)', async ({ page }) => { + const inputTypeField = page.locator('[name=inputType]'); + const applyBtn = page.locator('[name=apply]'); + + await inputTypeField.fill('number'); + await applyBtn.click(); + + const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveAttribute('type', 'number'); + + // Many browsers disallow arbitrary characters and formatting in native number inputs. + // Just verify that switching to number does not throw, then switch back to text. + await inputTypeField.fill('text'); + await applyBtn.click(); + await expect(currencyInput).toHaveAttribute('type', 'text'); + }); + + test('set input type to email (stress case) then back to text', async ({ page }) => { + const inputTypeField = page.locator('[name=inputType]'); + const applyBtn = page.locator('[name=apply]'); + + await inputTypeField.fill('email'); + await applyBtn.click(); + await expect(page.locator('#currency-input')).toHaveAttribute('type', 'email'); + + await inputTypeField.fill('text'); + await applyBtn.click(); + await expect(page.locator('#currency-input')).toHaveAttribute('type', 'text'); + }); +}); From ecb5a9abe57da71e384fa434d9068616635ea8c4 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Sat, 13 Dec 2025 04:21:55 -0500 Subject: [PATCH 2/7] fix #44, setting value should work --- playwright.config.ts | 4 +++ src/index.tsx | 27 +++++++++++++++++--- tests/controlled-value.spec.ts | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 tests/controlled-value.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 6a45cf2..b529d89 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -43,6 +43,10 @@ export default defineConfig({ testMatch: '**/mask.spec.ts', dependencies: ['base tests'], }, + { + name: 'controlled-value tests', + testMatch: '**/controlled-value.spec.ts', + }, { name: 'chromium', use: { ...devices['Desktop Chrome'] }, diff --git a/src/index.tsx b/src/index.tsx index 798f46b..6702413 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -36,6 +36,7 @@ type CurrencyInputState = { disableSelectionHandling: boolean, maskedValue: string, value: number | string, // TODO: should be string? should also have a separate float field for 'pennies' + previousProps?: Readonly, // Track previous props to detect changes }; type SelectionSnapshot = { @@ -166,6 +167,7 @@ class CurrencyInput extends React.Component, prevState: Readonly) { - const props = { ...nextProps }; - if (nextProps.value !== prevState.value) { - props.value = prevState.value; + const previousProps = prevState.previousProps || nextProps; // First call has no previous props + + // Check if any props that affect value/formatting have changed + const valueChanged = nextProps.value !== previousProps.value; + const separatorsChanged = + nextProps.decimalSeparator !== previousProps.decimalSeparator || + nextProps.thousandSeparator !== previousProps.thousandSeparator; + const formattingChanged = + nextProps.precision !== previousProps.precision || + nextProps.allowNegative !== previousProps.allowNegative || + nextProps.prefix !== previousProps.prefix || + nextProps.suffix !== previousProps.suffix; + + if (valueChanged || separatorsChanged || formattingChanged) { + // Something changed - prepare new state and track these props + const newState = CurrencyInput.prepareProps(nextProps); + return { ...newState, previousProps: nextProps }; } - return CurrencyInput.prepareProps(props); + + // Nothing significant changed - preserve current state but update previousProps reference + // This allows onChange to update state without interference + return { ...prevState, previousProps: nextProps }; } /** diff --git a/tests/controlled-value.spec.ts b/tests/controlled-value.spec.ts new file mode 100644 index 0000000..b8e1b8c --- /dev/null +++ b/tests/controlled-value.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import * as path from 'path'; + +const projectDir = path.resolve(__dirname, '../'); +const filePath = path.join(projectDir, 'examples/index.html'); +const fileUrl = `file://${filePath}`; + +test.describe('controlled component (value prop updates)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(fileUrl); + }); + + test('should update input value when value prop changes externally', async ({ page }) => { + // This test simulates the reported bug: + // A parent component changes the value prop (e.g., via a button click that calls setAmount(22.22)) + // and the CurrencyInput should reflect that change. + + const currencyInput = page.locator('#currency-input'); + + // Initial state: should show $0.00 USD + await expect(currencyInput).toHaveValue('$0.00 USD'); + + // Simulate typing into the input to set it to a different value + // Note: Using pressSequentially instead of fill+type to properly trigger React events + await currencyInput.focus(); + await currencyInput.selectText(); + await currencyInput.pressSequentially('5000'); + + // Now it should show $50.00 USD (with precision 2) + await expect(currencyInput).toHaveValue('$50.00 USD'); + + // Now change the value via the form control (simulating external prop change) + const prefixInput = page.locator('[name=prefix]'); + const suffixInput = page.locator('[name=suffix]'); + const applyBtn = page.locator('[name=apply]'); + + // Set a different prefix/suffix to force a refresh, but keep the same value + // The component should preserve the currently formatted value + await prefixInput.fill('$'); + await suffixInput.fill(' USD'); + await applyBtn.click(); + + // Input should still show the value it had + await expect(currencyInput).toHaveValue('$50.00 USD'); + }); +}); From 18d4b4b15e849b44a5546aebb3dff8e0e8c01452 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Sat, 13 Dec 2025 16:38:41 -0500 Subject: [PATCH 3/7] Fix allowNegative to not erase input when toggled - Modified getDerivedStateFromProps to distinguish between: - Value prop changes (parent controlling input) - always reformat - Display formatting changes (separators, prefix, suffix, precision) - reformat current value - Behavior changes (allowNegative, etc.) - preserve current input - This allows allowNegative to toggle without losing user's current input - Updated allowNegative tests to properly test toggling and negative number input - All 535 tests passing --- package.json | 4 +-- src/index.tsx | 30 ++++++++++++++-------- tests/base.spec.ts | 46 +++++++++++++++++++++------------- tests/controlled-value.spec.ts | 14 +++++------ 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index e168fc2..e284a93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ericblade/react-currency-input", - "version": "1.4.4", + "version": "1.4.5", "description": "React component for inputting currency amounts", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -71,4 +71,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/src/index.tsx b/src/index.tsx index 6702413..d3cf1a6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -185,26 +185,34 @@ class CurrencyInput extends React.Component, prevState: Readonly) { const previousProps = prevState.previousProps || nextProps; // First call has no previous props - - // Check if any props that affect value/formatting have changed + + // Check if the VALUE prop itself changed (parent is controlling the input) const valueChanged = nextProps.value !== previousProps.value; - const separatorsChanged = + + // Check if separators or display formatting changed (these require reformatting the current value) + const formatChanged = nextProps.decimalSeparator !== previousProps.decimalSeparator || - nextProps.thousandSeparator !== previousProps.thousandSeparator; - const formattingChanged = + nextProps.thousandSeparator !== previousProps.thousandSeparator || nextProps.precision !== previousProps.precision || - nextProps.allowNegative !== previousProps.allowNegative || nextProps.prefix !== previousProps.prefix || nextProps.suffix !== previousProps.suffix; - - if (valueChanged || separatorsChanged || formattingChanged) { - // Something changed - prepare new state and track these props + + if (valueChanged) { + // Parent changed the value prop - use the new value const newState = CurrencyInput.prepareProps(nextProps); return { ...newState, previousProps: nextProps }; } - // Nothing significant changed - preserve current state but update previousProps reference - // This allows onChange to update state without interference + if (formatChanged) { + // Display formatting changed - reformat the current value with new formatting + const propsWithCurrentValue = { ...nextProps, value: prevState.value }; + const newState = CurrencyInput.prepareProps(propsWithCurrentValue); + return { ...newState, previousProps: nextProps }; + } + + // Other props changed (allowNegative) but value and display formatting didn't + // Don't reformat - just update the previousProps reference and preserve current state + // This allows allowNegative to toggle without erasing the user's input return { ...prevState, previousProps: nextProps }; } diff --git a/tests/base.spec.ts b/tests/base.spec.ts index 4ce2121..964f309 100644 --- a/tests/base.spec.ts +++ b/tests/base.spec.ts @@ -179,38 +179,48 @@ test.describe('component parameters', () => { test.describe('allowNegative', () => { test('rejects negative input when allowNegative is false', async ({ page }) => { + // allowNegative is false by default, so minus signs should be rejected const currencyInput = page.locator('#currency-input'); await currencyInput.focus(); - await currencyInput.fill(''); - await currencyInput.type('100'); + await currencyInput.selectText(); + await currencyInput.pressSequentially('-100'); // Should not contain minus sign since allowNegative is false by default const value = await currencyInput.inputValue(); expect(value).not.toContain('-'); }); - test('allows typing when allowNegative is false', async ({ page }) => { - const currencyInput = page.locator('#currency-input'); - await currencyInput.focus(); - await currencyInput.fill(''); - await currencyInput.type('100'); + test('accepts negative numbers when allowNegative is true', async ({ page }) => { + // Enable allowNegative via form control + const allowNegativeCheckbox = page.locator('[name=allowNegative]'); + const applyBtn = page.locator('[name=apply]'); - // Should have numeric content - const value = await currencyInput.inputValue(); - expect(value).toMatch(/\d/); - }); - }); + await allowNegativeCheckbox.check(); + await applyBtn.click(); - test.describe('allowEmpty', () => { - test('does not allow empty value when allowEmpty is false', async ({ page }) => { + // Wait for the state to update and component to re-render + await page.waitForTimeout(200); + const currencyInput = page.locator('#currency-input'); await currencyInput.focus(); - await currencyInput.fill(''); - - // Should default to 0.00 when allowEmpty is false - await expect(currencyInput).toHaveValue('$0.00 USD'); + await currencyInput.selectText(); + + // First input a number (can't have negative zero) + await currencyInput.pressSequentially('50'); + + let value = await currencyInput.inputValue(); + expect(value).toContain('0.50'); + + // Now add the minus sign - should toggle the number to negative + await currencyInput.press('Minus'); + + // Should now contain minus sign + value = await currencyInput.inputValue(); + expect(value).toContain('-'); }); + }); + test.describe('allowEmpty', () => { test('maintains default value after clearing', async ({ page }) => { const currencyInput = page.locator('#currency-input'); await currencyInput.focus(); diff --git a/tests/controlled-value.spec.ts b/tests/controlled-value.spec.ts index b8e1b8c..e64336b 100644 --- a/tests/controlled-value.spec.ts +++ b/tests/controlled-value.spec.ts @@ -14,32 +14,32 @@ test.describe('controlled component (value prop updates)', () => { // This test simulates the reported bug: // A parent component changes the value prop (e.g., via a button click that calls setAmount(22.22)) // and the CurrencyInput should reflect that change. - + const currencyInput = page.locator('#currency-input'); - + // Initial state: should show $0.00 USD await expect(currencyInput).toHaveValue('$0.00 USD'); - + // Simulate typing into the input to set it to a different value // Note: Using pressSequentially instead of fill+type to properly trigger React events await currencyInput.focus(); await currencyInput.selectText(); await currencyInput.pressSequentially('5000'); - + // Now it should show $50.00 USD (with precision 2) await expect(currencyInput).toHaveValue('$50.00 USD'); - + // Now change the value via the form control (simulating external prop change) const prefixInput = page.locator('[name=prefix]'); const suffixInput = page.locator('[name=suffix]'); const applyBtn = page.locator('[name=apply]'); - + // Set a different prefix/suffix to force a refresh, but keep the same value // The component should preserve the currently formatted value await prefixInput.fill('$'); await suffixInput.fill(' USD'); await applyBtn.click(); - + // Input should still show the value it had await expect(currencyInput).toHaveValue('$50.00 USD'); }); From 2699b8f8af993fdbfa95e08a285091d3de94b1fb Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Sat, 13 Dec 2025 16:46:03 -0500 Subject: [PATCH 4/7] Make negative numbers positive when allowNegative is disabled - When allowNegative changes from true to false, any negative value is converted to positive - Added test to verify negative sign is removed when allowNegative is disabled - All 543 tests passing --- src/index.tsx | 19 ++++++++++++++++--- tests/base.spec.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index d3cf1a6..18d9a09 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -210,9 +210,22 @@ class CurrencyInput extends React.Component { value = await currencyInput.inputValue(); expect(value).toContain('-'); }); + + test('removes negative sign when allowNegative is disabled', async ({ page }) => { + // First enable allowNegative + const allowNegativeCheckbox = page.locator('[name=allowNegative]'); + const applyBtn = page.locator('[name=apply]'); + + await allowNegativeCheckbox.check(); + await applyBtn.click(); + await page.waitForTimeout(200); + + const currencyInput = page.locator('#currency-input'); + await currencyInput.focus(); + await currencyInput.selectText(); + + // Input a negative number + await currencyInput.pressSequentially('50'); + await currencyInput.press('Minus'); + + let value = await currencyInput.inputValue(); + expect(value).toContain('-'); + + // Now disable allowNegative + await allowNegativeCheckbox.uncheck(); + await applyBtn.click(); + await page.waitForTimeout(200); + + // Value should now be positive + value = await currencyInput.inputValue(); + expect(value).not.toContain('-'); + }); }); test.describe('allowEmpty', () => { From 16c2a2cbbef601b8d3e1858f55c073d02daa747a Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Sun, 14 Dec 2025 00:45:24 -0500 Subject: [PATCH 5/7] some more adjustment of allowNegativeChanged --- examples/index.js | 4 +- playwright.config.ts | 6 +++ src/index.tsx | 69 +++++++++++++++++++--------------- tests/base.spec.ts | 52 +++++++++++-------------- tests/controlled-value.spec.ts | 17 +++------ 5 files changed, 75 insertions(+), 73 deletions(-) diff --git a/examples/index.js b/examples/index.js index ad0f656..c408b1f 100644 --- a/examples/index.js +++ b/examples/index.js @@ -69,7 +69,7 @@ const ExampleForm = ({
Disable selection handling
- {/* Value
*/} + Value
): CurrencyInputState { + static prepareProps(props: Readonly): CurrencyInputState { + const { + onChangeEvent, + value: propValue, + decimalSeparator, + thousandSeparator, + precision, + inputType, + allowNegative, + allowEmpty, + prefix, + suffix, + selectAllOnFocus, + autoFocus, + disableSelectionHandling: propDisableSelectionHandling, + ...customProps + } = props; let initialValue = propValue; if (initialValue === null) { initialValue = allowEmpty ? null : ''; @@ -167,8 +168,7 @@ class CurrencyInput extends React.Component, prevState: Readonly) { - const previousProps = prevState.previousProps || nextProps; // First call has no previous props + const previousProps = prevState.previousProps || nextProps; // First call uses the initial props snapshot // Check if the VALUE prop itself changed (parent is controlling the input) const valueChanged = nextProps.value !== previousProps.value; - + // Check if separators or display formatting changed (these require reformatting the current value) const formatChanged = nextProps.decimalSeparator !== previousProps.decimalSeparator || @@ -202,7 +202,7 @@ class CurrencyInput extends React.Component { }); test('sanity startup', async ({ page }) => { - const currencyInput = await page.locator('#currency-input'); + const currencyInput = page.locator('#currency-input'); await expect(currencyInput).toHaveValue('$0.00 USD'); }); @@ -96,8 +96,8 @@ test.describe('component parameters', () => { await decimalInput.fill(','); await applyBtn.click(); - await page.waitForTimeout(100); const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('0,00'); await currencyInput.focus(); await currencyInput.fill(''); await currencyInput.type('12345'); @@ -120,8 +120,8 @@ test.describe('component parameters', () => { await precisionInput.fill('2'); await applyBtn.click(); - await page.waitForTimeout(100); const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('0,00'); await currencyInput.focus(); await currencyInput.fill(''); await currencyInput.type('1234567'); @@ -145,8 +145,8 @@ test.describe('component parameters', () => { await precisionInput.fill('0'); await applyBtn.click(); - await page.waitForTimeout(100); const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('0'); await currencyInput.focus(); await currencyInput.fill(''); await currencyInput.type('12345'); @@ -167,8 +167,8 @@ test.describe('component parameters', () => { await precisionInput.fill('3'); await applyBtn.click(); - await page.waitForTimeout(100); const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('0.000'); await currencyInput.focus(); await currencyInput.fill(''); await currencyInput.type('12345'); @@ -194,26 +194,24 @@ test.describe('component parameters', () => { // Enable allowNegative via form control const allowNegativeCheckbox = page.locator('[name=allowNegative]'); const applyBtn = page.locator('[name=apply]'); + const currencyInput = page.locator('#currency-input'); await allowNegativeCheckbox.check(); await applyBtn.click(); - // Wait for the state to update and component to re-render - await page.waitForTimeout(200); - - const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('$0.00 USD'); await currencyInput.focus(); await currencyInput.selectText(); - + // First input a number (can't have negative zero) await currencyInput.pressSequentially('50'); - + let value = await currencyInput.inputValue(); expect(value).toContain('0.50'); - + // Now add the minus sign - should toggle the number to negative await currencyInput.press('Minus'); - + // Should now contain minus sign value = await currencyInput.inputValue(); expect(value).toContain('-'); @@ -223,27 +221,25 @@ test.describe('component parameters', () => { // First enable allowNegative const allowNegativeCheckbox = page.locator('[name=allowNegative]'); const applyBtn = page.locator('[name=apply]'); + const currencyInput = page.locator('#currency-input'); await allowNegativeCheckbox.check(); await applyBtn.click(); - await page.waitForTimeout(200); - - const currencyInput = page.locator('#currency-input'); + await expect(currencyInput).toHaveValue('$0.00 USD'); await currencyInput.focus(); await currencyInput.selectText(); - + // Input a negative number await currencyInput.pressSequentially('50'); await currencyInput.press('Minus'); - + let value = await currencyInput.inputValue(); expect(value).toContain('-'); - + // Now disable allowNegative await allowNegativeCheckbox.uncheck(); await applyBtn.click(); - await page.waitForTimeout(200); - + // Value should now be positive value = await currencyInput.inputValue(); expect(value).not.toContain('-'); @@ -275,18 +271,14 @@ test.describe('component parameters', () => { await selectAllCheckbox.check(); await applyBtn.click(); - await page.waitForTimeout(100); const currencyInput = page.locator('#currency-input'); await currencyInput.focus(); - // With selectAllOnFocus, all text should be selected - // The selection should encompass the content - const inputValue = await currencyInput.inputValue(); - const selectionStart = await currencyInput.evaluate((el: HTMLInputElement) => el.selectionStart); - const selectionEnd = await currencyInput.evaluate((el: HTMLInputElement) => el.selectionEnd); - - // Should have selected content - expect(selectionEnd - selectionStart).toBeGreaterThan(0); + await expect.poll(async () => { + const selectionStart = await currencyInput.evaluate((el: HTMLInputElement) => el.selectionStart ?? 0); + const selectionEnd = await currencyInput.evaluate((el: HTMLInputElement) => el.selectionEnd ?? 0); + return selectionEnd - selectionStart; + }).toBeGreaterThan(0); }); }); diff --git a/tests/controlled-value.spec.ts b/tests/controlled-value.spec.ts index e64336b..0d771f2 100644 --- a/tests/controlled-value.spec.ts +++ b/tests/controlled-value.spec.ts @@ -11,9 +11,7 @@ test.describe('controlled component (value prop updates)', () => { }); test('should update input value when value prop changes externally', async ({ page }) => { - // This test simulates the reported bug: - // A parent component changes the value prop (e.g., via a button click that calls setAmount(22.22)) - // and the CurrencyInput should reflect that change. + // Simulate a parent-driven prop change by updating the form-controlled value field. const currencyInput = page.locator('#currency-input'); @@ -30,17 +28,14 @@ test.describe('controlled component (value prop updates)', () => { await expect(currencyInput).toHaveValue('$50.00 USD'); // Now change the value via the form control (simulating external prop change) - const prefixInput = page.locator('[name=prefix]'); - const suffixInput = page.locator('[name=suffix]'); + const valueInput = page.locator('[name=value]'); const applyBtn = page.locator('[name=apply]'); - // Set a different prefix/suffix to force a refresh, but keep the same value - // The component should preserve the currently formatted value - await prefixInput.fill('$'); - await suffixInput.fill(' USD'); + // Simulate a parent setting the controlled value to 22.22 + await valueInput.fill('22.22'); await applyBtn.click(); - // Input should still show the value it had - await expect(currencyInput).toHaveValue('$50.00 USD'); + // Input should reflect the externally provided value (22.22) + await expect(currencyInput).toHaveValue('$22.22 USD'); }); }); From 701ebcfe7b6fa35495f33c4e656cd42a62e04be2 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Sun, 14 Dec 2025 01:16:25 -0500 Subject: [PATCH 6/7] Update tests/base.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/base.spec.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/base.spec.ts b/tests/base.spec.ts index 51cf2f0..e594649 100644 --- a/tests/base.spec.ts +++ b/tests/base.spec.ts @@ -282,28 +282,6 @@ test.describe('component parameters', () => { }); }); - test.describe('input type', () => { - test('can set input type to email', async ({ page }) => { - const inputTypeField = page.locator('[name=inputType]'); - const applyBtn = page.locator('[name=apply]'); - - await inputTypeField.fill('email'); - await applyBtn.click(); - - const currencyInput = page.locator('#currency-input'); - const type = await currencyInput.getAttribute('type'); - - expect(type).toBe('email'); - }); - - test('can set input type back to text', async ({ page }) => { - const currencyInput = page.locator('#currency-input'); - const type = await currencyInput.getAttribute('type'); - - expect(type).toBe('text'); - }); - }); - test.describe('basic input and formatting', () => { test('typing numbers formats correctly', async ({ page }) => { const currencyInput = page.locator('#currency-input'); From c77fded0af7ca1cc7d62a240471ce5db4171aeaf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Dec 2025 03:47:27 -0500 Subject: [PATCH 7/7] Fix getDerivedStateFromProps to handle simultaneous allowNegative and format prop changes (#46) * Initial plan * Fix getDerivedStateFromProps to handle simultaneous allowNegative and format prop changes Co-authored-by: ericblade <1451847+ericblade@users.noreply.github.com> * Add clarifying comment for valueToFormat initialization Co-authored-by: ericblade <1451847+ericblade@users.noreply.github.com> * Fix whitespace for consistency with codebase style Co-authored-by: ericblade <1451847+ericblade@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ericblade <1451847+ericblade@users.noreply.github.com> --- src/index.tsx | 35 +++++++++++++++++------------------ tests/base.spec.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 202b507..1cd9df6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -189,13 +189,17 @@ class CurrencyInput extends React.Component { value = await currencyInput.inputValue(); expect(value).not.toContain('-'); }); + + test('removes negative sign when allowNegative is disabled and format changes simultaneously', async ({ page }) => { + // First enable allowNegative + const allowNegativeCheckbox = page.locator('[name=allowNegative]'); + const prefixInput = page.locator('[name=prefix]'); + const applyBtn = page.locator('[name=apply]'); + const currencyInput = page.locator('#currency-input'); + + await allowNegativeCheckbox.check(); + await applyBtn.click(); + await expect(currencyInput).toHaveValue('$0.00 USD'); + await currencyInput.focus(); + await currencyInput.selectText(); + + // Input a negative number + await currencyInput.pressSequentially('50'); + await currencyInput.press('Minus'); + + let value = await currencyInput.inputValue(); + expect(value).toContain('-'); + + // Now disable allowNegative AND change prefix at the same time + await allowNegativeCheckbox.uncheck(); + await prefixInput.fill('€'); + await applyBtn.click(); + + // Value should now be positive AND have the new prefix + value = await currencyInput.inputValue(); + expect(value).not.toContain('-'); + expect(value).toContain('€'); + expect(value).toContain('50'); + }); }); test.describe('allowEmpty', () => {