diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 7887eb6c3..e65c14784 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -78,7 +78,7 @@ steps: - echo "--- Install dependencies" - yarn install --immutable - echo "+++ Run Browser integration tests :pray:" - - yarn turbo run --filter=@internal/browser-integration-tests test + - yarn turbo run --filter=@internal/browser-integration-tests test:int retry: automatic: - exit_status: '*' @@ -156,6 +156,8 @@ steps: - yarn turbo run --filter='./packages/signals/*' lint - echo "+++ Run Tests" - yarn turbo run --filter='./packages/signals/*' test + - echo "+++ Run Integration Tests" + - yarn turbo run --filter='./packages/signals/*' test:int plugins: - ssh://git@github.com/segmentio/cache-buildkite-plugin#v2.0.0: key: "v1.1-cache-dev-{{ checksum 'yarn.lock' }}" diff --git a/.changeset/slimy-rabbits-agree.md b/.changeset/slimy-rabbits-agree.md new file mode 100644 index 000000000..a63ebc327 --- /dev/null +++ b/.changeset/slimy-rabbits-agree.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-signals': patch +--- + +Fix circular submit error for react-hook-form diff --git a/package.json b/package.json index 4e2358c08..e3c4b30bf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "turbo run build --filter='./packages/**'", "watch": "turbo run watch --filter='./packages/**'", "dev": "yarn workspace @playground/next-playground run dev", - "prepush": "yarn lint && CI=true yarn test --colors --silent", + "prepush": "turbo run lint --filter='...[master...HEAD]' && CI=true yarn turbo run test --filter='...[master...HEAD]' -- --colors --silent", "postinstall": "husky install", "changeset": "changeset", "update-versions-and-changelogs": "changeset version && yarn version-run-all && bash scripts/update-lockfile.sh", diff --git a/packages/browser-integration-tests/package.json b/packages/browser-integration-tests/package.json index 4e9485a1e..f947af059 100644 --- a/packages/browser-integration-tests/package.json +++ b/packages/browser-integration-tests/package.json @@ -6,7 +6,7 @@ "hoistingLimits": "workspaces" }, "scripts": { - "test": "playwright test", + "test:int": "playwright test", "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'", "concurrently": "yarn run -T concurrently", "watch:test": "yarn test --watch", diff --git a/packages/signals/signals-example/package.json b/packages/signals/signals-example/package.json index 94d256bf3..387f96585 100644 --- a/packages/signals/signals-example/package.json +++ b/packages/signals/signals-example/package.json @@ -11,6 +11,7 @@ "@segment/analytics-signals": "workspace:^", "react": "^18.0.0", "react-dom": "^18.0.0", + "react-hook-form": "^7.54.2", "react-router-dom": "^6.23.1" }, "devDependencies": { diff --git a/packages/signals/signals-example/src/components/App.tsx b/packages/signals/signals-example/src/components/App.tsx index e64f7335e..df7fcb8e5 100644 --- a/packages/signals/signals-example/src/components/App.tsx +++ b/packages/signals/signals-example/src/components/App.tsx @@ -4,6 +4,7 @@ import { loadAnalytics } from '../lib/analytics' import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom' import { HomePage } from '../pages/Home' import { OtherPage } from '../pages/Other' +import { ReactHookFormPage } from '../pages/ReactHookForm' const App: React.FC = () => { useEffect(() => { @@ -14,11 +15,13 @@ const App: React.FC = () => { } /> } /> + } /> ) diff --git a/packages/signals/signals-example/src/components/ComplexReactHookForm.tsx b/packages/signals/signals-example/src/components/ComplexReactHookForm.tsx new file mode 100644 index 000000000..2e26d5f22 --- /dev/null +++ b/packages/signals/signals-example/src/components/ComplexReactHookForm.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { useForm, SubmitHandler } from 'react-hook-form' + +interface IFormInput { + name: string + selectField: string + expectFormError: boolean +} + +const ComplexFormWithHookForm = () => { + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm() + const onSubmit: SubmitHandler = (data) => { + const statusCode = data.expectFormError ? parseInt(data.name, 10) : 200 + const formData = { + status: statusCode, + name: data.name, + selectField: data.selectField, + } + console.log('Submitting form:', JSON.stringify(formData)) + fetch('/parrot', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) + .then((response) => response.json()) + .then((data) => { + console.log('Form submitted successfully:', data) + }) + .catch((error) => { + console.error('Error submitting form:', error) + }) + } + + const name = watch('name') + const statusCode = React.useMemo(() => { + try { + const val = parseInt(name, 10) + return val >= 100 && val <= 511 ? val : 400 + } catch (err) { + return 400 + } + }, [name]) + + return ( +
+ +
+
+ + + {errors.name && This field is required} +
+
+ + + {errors.selectField && This field is required} +
+
+ + +
+ +
+ +
+ ) +} + +export default ComplexFormWithHookForm diff --git a/packages/signals/signals-example/src/pages/ReactHookForm.tsx b/packages/signals/signals-example/src/pages/ReactHookForm.tsx new file mode 100644 index 000000000..6be232e11 --- /dev/null +++ b/packages/signals/signals-example/src/pages/ReactHookForm.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import ComplexForm from '../components/ComplexReactHookForm' +import { analytics } from '../lib/analytics' + +export const ReactHookFormPage: React.FC = () => { + return ( +
+

Hello, React Hook Form with TypeScript!

+ + +
+ ) +} diff --git a/packages/signals/signals-integration-tests/package.json b/packages/signals/signals-integration-tests/package.json index 0a94ae712..207d87741 100644 --- a/packages/signals/signals-integration-tests/package.json +++ b/packages/signals/signals-integration-tests/package.json @@ -8,7 +8,7 @@ "scripts": { ".": "yarn run -T turbo run --filter=@internal/signals-integration-tests...", "build": "webpack", - "test": "playwright test", + "test:int": "playwright test", "test:vanilla": "playwright test src/tests/signals-vanilla", "test:perf": "playwright test src/tests/performance", "test:custom": "playwright test src/tests/custom", diff --git a/packages/signals/signals/src/core/processor/processor.ts b/packages/signals/signals/src/core/processor/processor.ts index f4f9ba8d5..5a9aee4a7 100644 --- a/packages/signals/signals/src/core/processor/processor.ts +++ b/packages/signals/signals/src/core/processor/processor.ts @@ -1,7 +1,7 @@ import { logger } from '../../lib/logger' import { Signal } from '@segment/analytics-signals-runtime' import { AnyAnalytics } from '../../types' -import { MethodName, Sandbox } from './sandbox' +import { AnalyticsMethodCalls, MethodName, Sandbox } from './sandbox' export class SignalEventProcessor { private sandbox: Sandbox @@ -12,7 +12,14 @@ export class SignalEventProcessor { } async process(signal: Signal, signals: Signal[]) { - const analyticsMethodCalls = await this.sandbox.process(signal, signals) + let analyticsMethodCalls: AnalyticsMethodCalls + try { + analyticsMethodCalls = await this.sandbox.process(signal, signals) + } catch (err) { + // in practice, we should never hit this error, but if we do, we should log it. + console.error('Error processing signal', { signal, signals }, err) + return + } for (const methodName in analyticsMethodCalls) { const name = methodName as MethodName diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts new file mode 100644 index 000000000..59583549c --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts @@ -0,0 +1,279 @@ +import { parseElement } from '../element-parser' + +describe(parseElement, () => { + test('parses a generic HTML element', () => { + const element = document.createElement('div') + element.id = 'test-id' + element.classList.add('test-class') + element.setAttribute('title', 'Test Title') + element.textContent = 'Test Content' + + const parsed = parseElement(element) + + expect(parsed).toMatchInlineSnapshot(` + { + "attributes": { + "class": "test-class", + "id": "test-id", + "title": "Test Title", + }, + "classList": [ + "test-class", + ], + "describedBy": undefined, + "id": "test-id", + "innerText": undefined, + "label": undefined, + "labels": [], + "name": undefined, + "nodeName": "DIV", + "tagName": "DIV", + "textContent": "Test Content", + "title": "Test Title", + "type": undefined, + "value": undefined, + } + `) + }) + + test('parses an HTMLSelectElement', () => { + const select = document.createElement('select') + select.id = 'select-id' + select.classList.add('select-class') + select.setAttribute('title', 'Select Title') + select.selectedIndex = 1 + + const option1 = document.createElement('option') + option1.value = 'value1' + option1.label = 'label1' + select.appendChild(option1) + + const option2 = document.createElement('option') + option2.value = 'value2' + option2.label = 'label2' + select.appendChild(option2) + + select.selectedIndex = 1 + + const parsed = parseElement(select) + + expect(parsed).toMatchInlineSnapshot(` + { + "attributes": { + "class": "select-class", + "id": "select-id", + "title": "Select Title", + }, + "classList": [ + "select-class", + ], + "describedBy": undefined, + "id": "select-id", + "innerText": undefined, + "label": undefined, + "labels": [], + "name": "", + "nodeName": "SELECT", + "selectedIndex": 1, + "selectedOptions": [ + { + "label": "label2", + "value": "value2", + }, + ], + "tagName": "SELECT", + "textContent": "", + "title": "Select Title", + "type": "select-one", + "value": "value2", + } + `) + }) + + test('parses an HTMLInputElement', () => { + const input = document.createElement('input') + input.id = 'input-id' + input.classList.add('input-class') + input.setAttribute('title', 'Input Title') + input.type = 'checkbox' + input.checked = true + + const parsed = parseElement(input) + + expect(parsed).toMatchInlineSnapshot(` + { + "attributes": { + "class": "input-class", + "id": "input-id", + "title": "Input Title", + "type": "checkbox", + }, + "checked": true, + "classList": [ + "input-class", + ], + "describedBy": undefined, + "id": "input-id", + "innerText": undefined, + "label": undefined, + "labels": [], + "name": "", + "nodeName": "INPUT", + "tagName": "INPUT", + "textContent": "", + "title": "Input Title", + "type": "checkbox", + "value": "on", + } + `) + }) + + test('parses an HTMLMediaElement', () => { + const video = document.createElement('video') + video.id = 'video-id' + video.classList.add('video-class') + video.setAttribute('title', 'Video Title') + video.src = 'video.mp4' + video.currentTime = 10 + + // Mock the duration property + Object.defineProperty(video, 'duration', { + value: 120, + writable: true, + }) + + Object.defineProperty(video, 'paused', { + value: false, + writable: true, + }) + + Object.defineProperty(video, 'readyState', { + value: 4, + writable: true, + }) + + video.muted = true + video.playbackRate = 1.5 + video.volume = 0.8 + + const parsed = parseElement(video) + + expect(parsed).toMatchInlineSnapshot(` + { + "attributes": { + "class": "video-class", + "id": "video-id", + "src": "video.mp4", + "title": "Video Title", + }, + "classList": [ + "video-class", + ], + "currentSrc": "", + "currentTime": 10, + "describedBy": undefined, + "duration": 120, + "ended": false, + "id": "video-id", + "innerText": undefined, + "label": undefined, + "labels": [], + "muted": true, + "name": undefined, + "nodeName": "VIDEO", + "paused": false, + "playbackRate": 1.5, + "readyState": 4, + "src": "http://localhost/video.mp4", + "tagName": "VIDEO", + "textContent": "", + "title": "Video Title", + "type": undefined, + "value": undefined, + "volume": 0.8, + } + `) + }) + + test('parses an HTMLFormElement', () => { + const form = document.createElement('input') + form.id = 'form-id' + form.classList.add('form-class') + form.setAttribute('title', 'Form Title') + + const input = document.createElement('input') + input.name = 'input-name' + input.value = 'input-value' + form.appendChild(input) + + const parsed = parseElement(form) + + expect(parsed).toMatchInlineSnapshot(` + { + "attributes": { + "class": "form-class", + "id": "form-id", + "title": "Form Title", + }, + "checked": false, + "classList": [ + "form-class", + ], + "describedBy": undefined, + "id": "form-id", + "innerText": undefined, + "label": undefined, + "labels": [], + "name": "", + "nodeName": "INPUT", + "tagName": "INPUT", + "textContent": "", + "title": "Form Title", + "type": "text", + "value": "", + } + `) + }) + test('handles scenarios where name is an object', () => { + const form = document.createElement('form') + form.id = 'form-id' + form.classList.add('form-class') + form.setAttribute('title', 'Form Title') + + Object.defineProperty(form, 'name', { + // this can happen in some weird cases?. just in case + value: { + hello: 'world', + }, + writable: true, + }) + + const parsed = parseElement(form) + + expect(parsed).toMatchInlineSnapshot(` + { + "attributes": { + "class": "form-class", + "id": "form-id", + "title": "Form Title", + }, + "classList": [ + "form-class", + ], + "describedBy": undefined, + "formData": {}, + "id": "form-id", + "innerText": undefined, + "label": undefined, + "labels": [], + "name": undefined, + "nodeName": "FORM", + "tagName": "FORM", + "textContent": undefined, + "title": "Form Title", + "type": undefined, + "value": undefined, + } + `) + }) +}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts index 73e372cfc..b26e5bfb1 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts @@ -3,7 +3,8 @@ import { createInteractionSignal } from '../../../types/factories' import { SignalEmitter } from '../../emitter' import { SignalGlobalSettings } from '../../signals' import { SignalGenerator } from '../types' -import { shouldIgnoreElement, parseElement } from './dom-gen' +import { shouldIgnoreElement } from './dom-gen' +import { parseElement } from './element-parser' import { MutationObservable, AttributeChangedEvent, diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts index 115e02594..b45ca1ec3 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts @@ -5,195 +5,7 @@ import { } from '../../../types/factories' import { SignalEmitter } from '../../emitter' import { SignalGenerator } from '../types' -import { cleanText } from './helpers' -import type { ParsedAttributes } from '@segment/analytics-signals-runtime' - -interface Label { - textContent: string - id: string - attributes: ParsedAttributes -} - -const parseFormData = (data: FormData): Record => { - return [...data].reduce((acc, [key, value]) => { - if (typeof value === 'string') { - acc[key] = value - } - return acc - }, {} as Record) -} - -const parseLabels = ( - labels: NodeListOf | null | undefined -): Label[] => { - if (!labels) return [] - return [...labels].map(parseToLabel).filter((el): el is Label => Boolean(el)) -} - -const parseToLabel = (label: HTMLElement): Label => { - const textContent = label.textContent ? cleanText(label.textContent) : '' - return { - id: label.id, - attributes: parseNodeMap(label.attributes), - textContent, - } -} - -const parseNodeMap = (nodeMap: NamedNodeMap): ParsedAttributes => { - return Array.from(nodeMap).reduce((acc, attr) => { - acc[attr.name] = attr.value - return acc - }, {}) -} - -interface ParsedElementBase { - /** - * The attributes of the element -- this is a key-value object of the attributes of the element - */ - attributes: ParsedAttributes - classList: string[] - id: string - /** - * The labels associated with this element -- either from the `labels` property or from the `aria-labelledby` attribute - */ - labels?: Label[] - /** - * The first label associated with this element -- either from the `labels` property or from the `aria-labelledby` attribute - */ - label?: Label - name?: string - nodeName: string - tagName: string - title: string - type?: string - - /** - * The value of the element -- for inputs, this is the value of the input, for selects, this is the value of the selected option - */ - value?: string - /** - * The value content of the element -- this is the value content of the element, stripped of newlines, tabs, and multiple spaces - */ - textContent?: string - /** - * The inner value of the element -- this is the value content of the element, stripped of newlines, tabs, and multiple spaces - */ - innerText?: string - /** - * The element referenced by the `aria-describedby` attribute - */ - describedBy?: Label -} - -interface ParsedSelectElement extends ParsedElementBase { - selectedOptions: { label: string; value: string }[] - selectedIndex: number -} -interface ParsedInputElement extends ParsedElementBase { - checked: boolean -} -interface ParsedMediaElement extends ParsedElementBase { - currentSrc?: string - currentTime?: number - duration: number - ended: boolean - muted: boolean - paused: boolean - playbackRate: number - readyState?: number - src?: string - volume?: number -} - -interface ParsedHTMLFormElement extends ParsedElementBase { - formData: Record - innerText: never - textContent: never -} - -type AnyParsedElement = - | ParsedHTMLFormElement - | ParsedSelectElement - | ParsedInputElement - | ParsedMediaElement - | ParsedElementBase - -/** - * Get the element referenced from an type - */ -const getReferencedElement = ( - el: HTMLElement, - attr: string -): HTMLElement | undefined => { - const value = el.getAttribute(attr) - if (!value) return undefined - return document.getElementById(value) ?? undefined -} - -export const parseElement = (el: HTMLElement): AnyParsedElement => { - const labels = parseLabels((el as HTMLInputElement).labels) - const labeledBy = getReferencedElement(el, 'aria-labelledby') - const describedBy = getReferencedElement(el, 'aria-describedby') - if (labeledBy) { - const label = parseToLabel(labeledBy) - labels.unshift(label) - } - const base: ParsedElementBase = { - // adding a bunch of fields that are not on _all_ elements, but are on enough that it's useful to have them here. - attributes: parseNodeMap(el.attributes), - classList: [...el.classList], - id: el.id, - labels, - label: labels[0], - name: (el as HTMLInputElement).name, - nodeName: el.nodeName, - tagName: el.tagName, - title: el.title, - type: (el as HTMLInputElement).type, - value: (el as HTMLInputElement).value, - textContent: (el.textContent && cleanText(el.textContent)) ?? undefined, - innerText: (el.innerText && cleanText(el.innerText)) ?? undefined, - describedBy: (describedBy && parseToLabel(describedBy)) ?? undefined, - } - - if (el instanceof HTMLSelectElement) { - return { - ...base, - selectedOptions: [...el.selectedOptions].map((option) => ({ - value: option.value, - label: option.label, - })), - selectedIndex: el.selectedIndex, - } - } else if (el instanceof HTMLInputElement) { - return { - ...base, - checked: el.checked, - } - } else if (el instanceof HTMLMediaElement) { - return { - ...base, - currentSrc: el.currentSrc, - currentTime: el.currentTime, - duration: el.duration, - ended: el.ended, - muted: el.muted, - paused: el.paused, - playbackRate: el.playbackRate, - readyState: el.readyState, - src: el.src, - volume: el.volume, - } - } else if (el instanceof HTMLFormElement) { - return { - ...base, - innerText: undefined, - textContent: undefined, - formData: parseFormData(new FormData(el)), - } - } - return base -} +import { parseElement } from './element-parser' export class ClickSignalsGenerator implements SignalGenerator { id = 'click' diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts new file mode 100644 index 000000000..9fb036f06 --- /dev/null +++ b/packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts @@ -0,0 +1,180 @@ +import { cleanText } from './helpers' +import type { ParsedAttributes } from '@segment/analytics-signals-runtime' + +interface Label { + textContent: string + id: string + attributes: ParsedAttributes +} + +const parseFormData = (data: FormData): Record => { + return [...data].reduce((acc, [key, value]) => { + if (typeof value === 'string') { + acc[key] = value + } + return acc + }, {} as Record) +} + +const parseLabels = ( + labels: NodeListOf | null | undefined +): Label[] => { + if (!labels) return [] + return [...labels].map(parseToLabel).filter((el): el is Label => Boolean(el)) +} + +const parseToLabel = (label: HTMLElement): Label => { + const textContent = label.textContent ? cleanText(label.textContent) : '' + return { + id: label.id, + attributes: parseNodeMap(label.attributes), + textContent, + } +} + +const parseNodeMap = (nodeMap: NamedNodeMap): ParsedAttributes => { + return Array.from(nodeMap).reduce((acc, attr) => { + if (typeof attr.value === 'string' || attr.value === null) { + acc[attr.name] = attr.value + } + return acc + }, {}) +} + +interface ParsedElementBase { + attributes: ParsedAttributes + classList: string[] + id: string + labels?: Label[] + label?: Label + name?: string + nodeName: string + tagName: string + title: string + type?: string + value?: string + textContent?: string + innerText?: string + describedBy?: Label +} + +interface ParsedSelectElement extends ParsedElementBase { + selectedOptions: { label: string; value: string }[] + selectedIndex: number +} +interface ParsedInputElement extends ParsedElementBase { + checked: boolean +} +interface ParsedMediaElement extends ParsedElementBase { + currentSrc?: string + currentTime?: number + duration: number + ended: boolean + muted: boolean + paused: boolean + playbackRate: number + readyState?: number + src?: string + volume?: number +} + +interface ParsedHTMLFormElement extends ParsedElementBase { + formData: Record + innerText: never + textContent: never +} + +type AnyParsedElement = + | ParsedHTMLFormElement + | ParsedSelectElement + | ParsedInputElement + | ParsedMediaElement + | ParsedElementBase + +const getReferencedElement = ( + el: HTMLElement, + attr: string +): HTMLElement | undefined => { + const value = el.getAttribute(attr) + if (!value) return undefined + return document.getElementById(value) ?? undefined +} + +export const parseElement = (el: HTMLElement): AnyParsedElement => { + const labels = parseLabels((el as HTMLInputElement).labels) + const labeledBy = getReferencedElement(el, 'aria-labelledby') + const describedBy = getReferencedElement(el, 'aria-describedby') + if (labeledBy) { + const label = parseToLabel(labeledBy) + labels.unshift(label) + } + + const parsedAttributes = parseNodeMap(el.attributes) + + // This exists because of a bug in react-hook-form, where 'name', if used as the field registration name overrides the native element name value to reference itself. + // This is a very weird scenario where a property was on the element, but not in the attributes map. + // This probably only needs to be run on name, but running this on some other fields out of caution. + const getSanitizedProp = (prop: string): string | undefined => { + if (!(prop in el)) { + return undefined + } + // @ts-ignore + const val = el[prop] + return typeof val === 'string' ? val : undefined + } + + const base: ParsedElementBase = { + attributes: parsedAttributes, + classList: [...el.classList], + id: getSanitizedProp('id') || '', + labels, + label: labels[0], + name: getSanitizedProp('name'), + nodeName: el.nodeName, + tagName: el.tagName, + title: getSanitizedProp('title') || '', + type: getSanitizedProp('type'), + value: getSanitizedProp('value'), + textContent: (el.textContent && cleanText(el.textContent)) ?? undefined, + innerText: (el.innerText && cleanText(el.innerText)) ?? undefined, + describedBy: (describedBy && parseToLabel(describedBy)) ?? undefined, + } + + if (el instanceof HTMLSelectElement) { + return { + ...base, + selectedOptions: [...el.selectedOptions].map((option) => ({ + value: option.value, + label: option.label, + })), + selectedIndex: el.selectedIndex, + } + } else if (el instanceof HTMLInputElement) { + return { + ...base, + checked: el.checked, + } + } else if (el instanceof HTMLMediaElement) { + return { + ...base, + currentSrc: el.currentSrc, + currentTime: el.currentTime, + duration: el.duration, + ended: el.ended, + muted: el.muted, + paused: el.paused, + playbackRate: el.playbackRate, + readyState: el.readyState, + src: el.src, + volume: el.volume, + } + } else if (el instanceof HTMLFormElement) { + return { + ...base, + innerText: undefined, + textContent: undefined, + formData: parseFormData(new FormData(el)), + } + } + return base +} diff --git a/yarn.lock b/yarn.lock index cf6452877..45ef7999d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3058,6 +3058,7 @@ __metadata: html-webpack-plugin: ^5.6.0 react: ^18.0.0 react-dom: ^18.0.0 + react-hook-form: ^7.54.2 react-router-dom: ^6.23.1 style-loader: ^4.0.0 typescript: ^4.7.0 @@ -21145,6 +21146,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.54.2": + version: 7.54.2 + resolution: "react-hook-form@npm:7.54.2" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + checksum: 49a867ece9894dca82f6552e5eefd012b7db962c56a7543f9275ae0b6ec202d549973c3694e7f97436afc2396549cb8fc8777241dd660b71793547aa9c8e5686 + languageName: node + linkType: hard + "react-is@npm:^16.13.1": version: 16.13.1 resolution: "react-is@npm:16.13.1"