diff --git a/addon-test-support/@ember/test-helpers/dom/fire-event.ts b/addon-test-support/@ember/test-helpers/dom/fire-event.ts index 5728c9c8d..6c591ea36 100644 --- a/addon-test-support/@ember/test-helpers/dom/fire-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/fire-event.ts @@ -1,5 +1,6 @@ import { isDocument, isElement } from './-target'; import tuple from '../-tuple'; +import isFormControl, { FormControl } from './-is-form-control'; // eslint-disable-next-line require-jsdoc const MOUSE_EVENT_CONSTRUCTOR = (() => { @@ -128,6 +129,11 @@ function fireEvent( event = buildBasicEvent(eventType, options); } + // HACK: Prepare React `input` elements for `change` events + if (eventType === 'change' && isFormControl(element)) { + _prepareReactOnChangeEvent(element as FormControl); + } + element.dispatchEvent(event); return event; } @@ -314,3 +320,21 @@ function buildFileEvent( return event; } + +/** + * Prepare a potential React element for simulated trigger of input `change` event. + * + * HACK: This is necessary for React elements in `react-dom` v15.6.0+. + * + * @param {FormControl} element Form control element, in particular an `input` element + * + * @see https://github.com/facebook/react/issues/11488 + * @see https://github.com/facebook/react/blob/main/packages/react-dom/src/client/inputValueTracking.js + */ +function _prepareReactOnChangeEvent(element: FormControl) { + // @ts-expect-error: `_valueTracker` is a React-specific property (not native) + const tracker = element._valueTracker; + if (tracker) { + tracker.setValue(''); + } +} diff --git a/tests/unit/dom/fill-in-test.js b/tests/unit/dom/fill-in-test.js index fba2b887c..8fe3b74d0 100644 --- a/tests/unit/dom/fill-in-test.js +++ b/tests/unit/dom/fill-in-test.js @@ -347,4 +347,32 @@ module('DOM Helper: fillIn', function (hooks) { ) ); }); + + ['input', 'textarea'].forEach((elementType) => { + test(`filling in a psuedo React ${elementType} changes its value tracker`, async function (assert) { + element = buildInstrumentedElement(elementType); + element._valueTracker = { + currentValue: 'foo', + getValue() { + return this.currentValue; + }, + setValue(value) { + this.currentValue = '' + value; + }, + }; + element.value = 'foo'; + + await setupContext(context); + + await fillIn(element, 'bar'); + + assert.verifySteps(clickSteps); + assert.equal(element.value, 'bar', 'value updated'); + assert.equal( + element._valueTracker.getValue(), + '', + 'value tracker updated' + ); + }); + }); }); diff --git a/tests/unit/dom/type-in-test.js b/tests/unit/dom/type-in-test.js index 14d6556be..9a2ea57db 100644 --- a/tests/unit/dom/type-in-test.js +++ b/tests/unit/dom/type-in-test.js @@ -361,4 +361,32 @@ module('DOM Helper: typeIn', function (hooks) { new Error("Can not `typeIn` with text: 'fo' that exceeds maxlength: '1'.") ); }); + + ['input', 'textarea'].forEach((elementType) => { + test(`filling in a psuedo React ${elementType} changes its value tracker`, async function (assert) { + element = buildInstrumentedElement(elementType); + element._valueTracker = { + currentValue: 'foo', + getValue() { + return this.currentValue; + }, + setValue(value) { + this.currentValue = '' + value; + }, + }; + element.value = 'foo'; + + await setupContext(context); + + await typeIn(element, 'bar'); + + assert.verifySteps(expectedEvents); + assert.equal(element.value, 'foobar', 'value updated'); + assert.equal( + element._valueTracker.getValue(), + '', + 'value tracker updated' + ); + }); + }); });