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
, // Track previous props to detect changes
};
type SelectionSnapshot = {
@@ -124,22 +125,23 @@ class CurrencyInput extends React.Component): 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 : '';
@@ -166,7 +168,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 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 allowNegative changed (affects whether negative values are allowed)
+ const allowNegativeChanged = nextProps.allowNegative !== previousProps.allowNegative;
+
+ // Check if separators or display formatting changed (these require reformatting the current value)
+ const formatChanged =
+ nextProps.decimalSeparator !== previousProps.decimalSeparator ||
+ nextProps.thousandSeparator !== previousProps.thousandSeparator ||
+ nextProps.precision !== previousProps.precision ||
+ nextProps.prefix !== previousProps.prefix ||
+ nextProps.suffix !== previousProps.suffix ||
+ allowNegativeChanged;
+
+ if (valueChanged) {
+ // Parent changed the value prop - use the new value
+ const newState = CurrencyInput.prepareProps(nextProps);
+ return { ...newState, previousProps: nextProps };
}
- return CurrencyInput.prepareProps(props);
+
+ if (formatChanged) {
+ // Display formatting or allowNegative changed - reformat the current value
+ // First, determine the value to use. Start with the current state value,
+ // which may be adjusted below if allowNegative was disabled and value is negative.
+ let valueToFormat = prevState.value;
+
+ if (allowNegativeChanged && !nextProps.allowNegative) {
+ // allowNegative was disabled - if current value is negative, make it positive
+ const parsedValue = typeof prevState.value === 'number'
+ ? prevState.value
+ : prevState.value === null
+ ? 0
+ : CurrencyInput.stringValueToFloat(String(prevState.value), nextProps.thousandSeparator, nextProps.decimalSeparator);
+
+ if (parsedValue < 0) {
+ valueToFormat = Math.abs(parsedValue);
+ }
+ }
+
+ // Reformat with new formatting and potentially adjusted value
+ const propsWithCurrentValue = { ...nextProps, value: valueToFormat };
+ const newState = CurrencyInput.prepareProps(propsWithCurrentValue);
+ return { ...newState, previousProps: nextProps };
+ }
+
+ // Other props changed but value and display formatting didn't
+ // Just update the previousProps reference and preserve current state
+ return { ...prevState, previousProps: nextProps };
}
/**
diff --git a/tests/base.spec.ts b/tests/base.spec.ts
index 875c8d2..3e4cad7 100644
--- a/tests/base.spec.ts
+++ b/tests/base.spec.ts
@@ -11,7 +11,7 @@ test.describe('base tests', () => {
});
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');
});
@@ -21,4 +21,346 @@ 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();
+
+ const currencyInput = page.locator('#currency-input');
+ await expect(currencyInput).toHaveValue('0,00');
+ 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();
+
+ const currencyInput = page.locator('#currency-input');
+ await expect(currencyInput).toHaveValue('0,00');
+ 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();
+
+ const currencyInput = page.locator('#currency-input');
+ await expect(currencyInput).toHaveValue('0');
+ 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();
+
+ const currencyInput = page.locator('#currency-input');
+ await expect(currencyInput).toHaveValue('0.000');
+ 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 }) => {
+ // allowNegative is false by default, so minus signs should be rejected
+ const currencyInput = page.locator('#currency-input');
+ await currencyInput.focus();
+ 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('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]');
+ 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();
+
+ // 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('removes negative sign when allowNegative is disabled', async ({ page }) => {
+ // 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 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();
+
+ // Value should now be positive
+ 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', () => {
+ 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();
+
+ const currencyInput = page.locator('#currency-input');
+ await currencyInput.focus();
+
+ 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);
+ });
+ });
+
+ 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/controlled-value.spec.ts b/tests/controlled-value.spec.ts
new file mode 100644
index 0000000..0d771f2
--- /dev/null
+++ b/tests/controlled-value.spec.ts
@@ -0,0 +1,41 @@
+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 }) => {
+ // Simulate a parent-driven prop change by updating the form-controlled value field.
+
+ 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 valueInput = page.locator('[name=value]');
+ const applyBtn = page.locator('[name=apply]');
+
+ // Simulate a parent setting the controlled value to 22.22
+ await valueInput.fill('22.22');
+ await applyBtn.click();
+
+ // Input should reflect the externally provided value (22.22)
+ await expect(currencyInput).toHaveValue('$22.22 USD');
+ });
+});
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');
+ });
+});