diff --git a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx index 71ff65a84bd..78b560e8d92 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx @@ -3,49 +3,10 @@ import { useContext, useEffect, useRef } from 'react'; import { getAssetPath } from '@18f/identity-assets'; import { useI18n } from '@18f/identity-react-i18n'; import AcuantContext from '../context/acuant'; - -/** - * Defines a property on the given object, calling the change callback when that property is set to - * a new value. - * - * @param {any} object Object on which to define property. - * @param {string} property Property name to observe. - * @param {(nextValue: any) => void} onChangeCallback Callback to trigger on change. - */ -export function defineObservableProperty(object, property, onChangeCallback) { - let currentValue; - - Object.defineProperty(object, property, { - get() { - return currentValue; - }, - set(nextValue) { - currentValue = nextValue; - onChangeCallback(nextValue); - }, - configurable: true, - }); -} - -/** - * Resets a property on the given object, applying the originalDescriptor, if provided, - * or deleting the property entirely if not. - * - * @param {any} object Object on which to define property. - * @param {string} property Property name to observe. - * @param {any} originalDescriptor The descriptor to reset the property with. - */ -export function resetObservableProperty(object, property, originalDescriptor) { - if (object === undefined) { - return; - } - - if (originalDescriptor !== undefined) { - Object.defineProperty(object, property, originalDescriptor); - } else { - delete object[property]; - } -} +import { + defineObservableProperty, + stopObservingProperty, +} from '../higher-order/observable-property'; function AcuantCaptureCanvas() { const { isReady, acuantCaptureMode, setAcuantCaptureMode } = useContext(AcuantContext); @@ -53,15 +14,8 @@ function AcuantCaptureCanvas() { const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null)); useEffect(() => { - let canvas; - let originalDescriptor; - function onAcuantCameraCreated() { - canvas = document.getElementById('acuant-ui-canvas'); - if (originalDescriptor === undefined) { - originalDescriptor = Object.getOwnPropertyDescriptor(canvas, 'callback'); - } - + const canvas = document.getElementById('acuant-ui-canvas'); // Acuant SDK assigns a callback property to the canvas when it switches to its "Tap to // Capture" mode (Acuant SDK v11.4.4, L158). Infer capture type by presence of the property. defineObservableProperty(canvas, 'callback', (callback) => { @@ -71,8 +25,12 @@ function AcuantCaptureCanvas() { cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated); return () => { + const canvas = document.getElementById('acuant-ui-canvas'); + if (canvas) { + stopObservingProperty(canvas, 'callback'); + } + cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated); - resetObservableProperty(canvas, 'callback', originalDescriptor); }; }, []); diff --git a/app/javascript/packages/document-capture/higher-order/observable-property.tsx b/app/javascript/packages/document-capture/higher-order/observable-property.tsx new file mode 100644 index 00000000000..c6ed08b349f --- /dev/null +++ b/app/javascript/packages/document-capture/higher-order/observable-property.tsx @@ -0,0 +1,39 @@ +/** + * Defines a property on the given object, calling the change callback when that property is set to + * a new value. + * + * @param object Object on which to define property. + * @param property Property name to observe. + * @param onChangeCallback Callback to trigger on change. + */ +export function defineObservableProperty( + object: any, + property: string, + onChangeCallback: (nextValue: any) => void, +) { + let currentValue: any; + + Object.defineProperty(object, property, { + get() { + return currentValue; + }, + set(nextValue) { + currentValue = nextValue; + onChangeCallback(nextValue); + }, + configurable: true, + }); +} + +/** + * Removes an observable property by removing the defined getter/setter methods + * and replaces the value with the most recent value. + * + * @param object Object on which to remove defined property. + * @param property Property name to remove observer for + */ +export function stopObservingProperty(object: any, property: string) { + const currentValue = object[property]; + + Object.defineProperty(object, property, { value: currentValue, writable: true }); +} diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx index 5267a6af135..3eaa8b4f8e9 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx @@ -1,33 +1,12 @@ import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; -import AcuantCaptureCanvas, { - defineObservableProperty, -} from '@18f/identity-document-capture/components/acuant-capture-canvas'; +import AcuantCaptureCanvas from '@18f/identity-document-capture/components/acuant-capture-canvas'; import { render, useAcuant } from '../../../support/document-capture'; describe('document-capture/components/acuant-capture-canvas', () => { const { initialize } = useAcuant(); - describe('defineObservableProperty', () => { - it('behaves like an object', () => { - const object = {}; - defineObservableProperty(object, 'key', () => {}); - object.key = 'value'; - - expect(object.key).to.equal('value'); - }); - - it('calls the callback on changes, with the changed value', () => { - const callback = sinon.spy(); - const object = {}; - defineObservableProperty(object, 'key', callback); - object.key = 'value'; - - expect(callback).to.have.been.calledOnceWithExactly('value'); - }); - }); - it('renders a "take photo" button', async () => { const { getByRole, container } = render( diff --git a/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx b/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx new file mode 100644 index 00000000000..0e0f66a56e6 --- /dev/null +++ b/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx @@ -0,0 +1,44 @@ +import sinon from 'sinon'; +import { + defineObservableProperty, + stopObservingProperty, +} from '@18f/identity-document-capture/higher-order/observable-property'; + +describe('document-capture/higher-order/observable-property', () => { + describe('defineObservableProperty', () => { + it('behaves like an object', () => { + const object = {} as { key?: string }; + defineObservableProperty(object, 'key', () => {}); + object.key = 'value'; + + expect(object.key).to.equal('value'); + }); + + it('calls the callback on changes, with the changed value', () => { + const callback = sinon.spy(); + const object = {} as { key?: string }; + defineObservableProperty(object, 'key', callback); + object.key = 'value'; + + expect(callback).to.have.been.calledOnceWithExactly('value'); + }); + }); + + describe('stopObservingProperty', () => { + it('removes the defined property and set the last value as a plain value', () => { + const object = {} as { key?: string }; + const callback = sinon.spy(); + defineObservableProperty(object, 'key', callback); + + object.key = 'value'; + + stopObservingProperty(object, 'key'); + expect(object.key).to.equal('value'); + + object.key = 'second_value'; + expect(object.key).to.equal('second_value'); + + expect(callback).to.have.been.calledOnceWithExactly('value'); + }); + }); +});