From 55b4bc99bd0db1e2fa5159e45d251deac26b4462 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Sat, 18 Apr 2020 18:29:31 -0700 Subject: [PATCH] feat(actions): requery the element when it was detached during the action (#1853) --- src/chromium/crPage.ts | 3 + src/dom.ts | 62 ++++++------ src/errors.ts | 6 ++ src/firefox/ffPage.ts | 5 + src/frames.ts | 108 ++++++++++----------- src/helper.ts | 5 + src/injected/injected.ts | 119 +++++++++++++----------- src/injected/selectorEvaluator.ts | 2 +- src/webkit/wkPage.ts | 3 + test/assets/input/animating-button.html | 24 +++++ test/click.spec.js | 51 +++++++--- test/screenshot.spec.js | 2 +- 12 files changed, 241 insertions(+), 149 deletions(-) create mode 100644 test/assets/input/animating-button.html diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 6e992bc444..6b83a09579 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -36,6 +36,7 @@ import { CRPDF } from './crPdf'; import { CRBrowserContext } from './crBrowser'; import * as types from '../types'; import { ConsoleMessage } from '../console'; +import { NotConnectedError } from '../errors'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -765,6 +766,8 @@ class FrameSession { objectId: toRemoteObject(handle).objectId, rect, }).catch(e => { + if (e instanceof Error && e.message.includes('Node is detached from document')) + throw new NotConnectedError(); if (e instanceof Error && e.message.includes('Node does not have a layout object')) e.message = 'Node is either not visible or not an HTMLElement'; throw e; diff --git a/src/dom.ts b/src/dom.ts index 0368e0377c..b6f9a82863 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import * as debug from 'debug'; import * as fs from 'fs'; import * as mime from 'mime'; import * as path from 'path'; import * as util from 'util'; import * as frames from './frames'; -import { assert, debugError, helper } from './helper'; -import Injected from './injected/injected'; +import { assert, debugError, helper, debugInput } from './helper'; +import { Injected, InjectedResult } from './injected/injected'; import * as input from './input'; import * as js from './javascript'; import { Page } from './page'; import { selectors } from './selectors'; import * as types from './types'; +import { NotConnectedError, TimeoutError } from './errors'; export type PointerActionOptions = { modifiers?: input.Modifier[]; @@ -37,8 +37,6 @@ export type ClickOptions = PointerActionOptions & input.MouseClickOptions; export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions; -const debugInput = debug('pw:input'); - export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; private _injectedPromise?: Promise; @@ -220,10 +218,8 @@ export class ElementHandle extends js.JSHandle { const position = options ? options.position : undefined; await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined); const point = position ? await this._offsetPoint(position) : await this._clickablePoint(); - point.x = (point.x * 100 | 0) / 100; point.y = (point.y * 100 | 0) / 100; - if (!force) await this._waitForHitTargetAt(point, deadline); @@ -270,7 +266,8 @@ export class ElementHandle extends js.JSHandle { assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"'); } return await this._page._frameManager.waitForSignalsCreatedBy(async () => { - return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); + const injectedResult = await this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); + return handleInjectedResult(injectedResult, ''); }, deadline, options); } @@ -278,10 +275,9 @@ export class ElementHandle extends js.JSHandle { assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); const deadline = this._page._timeoutSettings.computeDeadline(options); await this._page._frameManager.waitForSignalsCreatedBy(async () => { - const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); - if (typeof errorOrNeedsInput === 'string') - throw new Error(errorOrNeedsInput); - if (errorOrNeedsInput) { + const injectedResult = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); + const needsInput = handleInjectedResult(injectedResult, ''); + if (needsInput) { if (value) await this._page.keyboard.insertText(value); else @@ -291,19 +287,21 @@ export class ElementHandle extends js.JSHandle { } async selectText(): Promise { - const error = await this._evaluateInUtility(({ injected, node }) => injected.selectText(node), {}); - if (typeof error === 'string') - throw new Error(error); + const injectedResult = await this._evaluateInUtility(({ injected, node }) => injected.selectText(node), {}); + handleInjectedResult(injectedResult, ''); } async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions) { const deadline = this._page._timeoutSettings.computeDeadline(options); - const multiple = await this._evaluateInUtility(({ node }) => { + const injectedResult = await this._evaluateInUtility(({ node }): InjectedResult => { if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT') - throw new Error('Node is not an HTMLInputElement'); + return { status: 'error', error: 'Node is not an HTMLInputElement' }; + if (!node.isConnected) + return { status: 'notconnected' }; const input = node as Node as HTMLInputElement; - return input.multiple; + return { status: 'success', value: input.multiple }; }, {}); + const multiple = handleInjectedResult(injectedResult, ''); let ff: string[] | types.FilePayload[]; if (!Array.isArray(files)) ff = [ files ] as string[] | types.FilePayload[]; @@ -329,14 +327,8 @@ export class ElementHandle extends js.JSHandle { } async focus() { - const errorMessage = await this._evaluateInUtility(({ node }) => { - if (!(node as any)['focus']) - return 'Node is not an HTML or SVG element.'; - (node as Node as HTMLElement | SVGElement).focus(); - return false; - }, {}); - if (errorMessage) - throw new Error(errorMessage); + const injectedResult = await this._evaluateInUtility(({ injected, node }) => injected.focusNode(node), {}); + handleInjectedResult(injectedResult, ''); } async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { @@ -416,7 +408,9 @@ export class ElementHandle extends js.JSHandle { const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => { return injected.waitForDisplayedAtStablePosition(node, timeout); }, helper.timeUntilDeadline(deadline)); - await helper.waitWithDeadline(stablePromise, 'element to be displayed and not moving', deadline); + const timeoutMessage = 'element to be displayed and not moving'; + const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline); + handleInjectedResult(injectedResult, timeoutMessage); debugInput('...done'); } @@ -434,7 +428,9 @@ export class ElementHandle extends js.JSHandle { const hitTargetPromise = this._evaluateInUtility(({ injected, node }, { timeout, point }) => { return injected.waitForHitTargetAt(node, timeout, point); }, { timeout: helper.timeUntilDeadline(deadline), point }); - await helper.waitWithDeadline(hitTargetPromise, 'element to receive pointer events', deadline); + const timeoutMessage = 'element to receive pointer events'; + const injectedResult = await helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline); + handleInjectedResult(injectedResult, timeoutMessage); debugInput('...done'); } } @@ -446,3 +442,13 @@ export function toFileTransferPayload(files: types.FilePayload[]): types.FileTra data: file.buffer.toString('base64') })); } + +function handleInjectedResult(injectedResult: InjectedResult, timeoutMessage: string): T { + if (injectedResult.status === 'notconnected') + throw new NotConnectedError(); + if (injectedResult.status === 'timeout') + throw new TimeoutError(`waiting for ${timeoutMessage} failed: timeout exceeded`); + if (injectedResult.status === 'error') + throw new Error(injectedResult.error); + return injectedResult.value as T; +} diff --git a/src/errors.ts b/src/errors.ts index 3044091584..fbe67c8a3e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -23,4 +23,10 @@ class CustomError extends Error { } } +export class NotConnectedError extends CustomError { + constructor() { + super('Element is not attached to the DOM'); + } +} + export class TimeoutError extends CustomError {} diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 0af4ca27ea..8b1e58b785 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -31,6 +31,7 @@ import { RawKeyboardImpl, RawMouseImpl } from './ffInput'; import { FFNetworkManager, headersArray } from './ffNetworkManager'; import { Protocol } from './protocol'; import { selectors } from '../selectors'; +import { NotConnectedError } from '../errors'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -422,6 +423,10 @@ export class FFPage implements PageDelegate { frameId: handle._context.frame._id, objectId: toRemoteObject(handle).objectId!, rect, + }).catch(e => { + if (e instanceof Error && e.message.includes('Node is detached from document')) + throw new NotConnectedError(); + throw e; }); } diff --git a/src/frames.ts b/src/frames.ts index 0faf59d94d..803f449ee8 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -19,9 +19,9 @@ import * as fs from 'fs'; import * as util from 'util'; import { ConsoleMessage } from './console'; import * as dom from './dom'; -import { TimeoutError } from './errors'; +import { TimeoutError, NotConnectedError } from './errors'; import { Events } from './events'; -import { assert, helper, RegisteredListener } from './helper'; +import { assert, helper, RegisteredListener, debugInput } from './helper'; import * as js from './javascript'; import * as network from './network'; import { Page } from './page'; @@ -693,72 +693,82 @@ export class Frame { return result!; } + private async _retryWithSelectorIfNotConnected( + selector: string, options: types.TimeoutOptions, + action: (handle: dom.ElementHandle, deadline: number) => Promise): Promise { + const deadline = this._page._timeoutSettings.computeDeadline(options); + while (!helper.isPastDeadline(deadline)) { + try { + const { world, task } = selectors._waitForSelectorTask(selector, 'attached', deadline); + const handle = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selector}"`); + const element = handle.asElement() as dom.ElementHandle; + try { + return await action(element, deadline); + } finally { + element.dispose(); + } + } catch (e) { + if (!(e instanceof NotConnectedError)) + throw e; + debugInput('Element was detached from the DOM, retrying'); + } + } + throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`); + } + async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.click(helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.click(helper.optionsWithUpdatedTimeout(options, deadline))); } async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.dblclick(helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.dblclick(helper.optionsWithUpdatedTimeout(options, deadline))); } async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.fill(value, helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.fill(value, helper.optionsWithUpdatedTimeout(options, deadline))); } - async focus(selector: string, options?: types.TimeoutOptions) { - const { handle } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.focus(); - handle.dispose(); + async focus(selector: string, options: types.TimeoutOptions = {}) { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.focus()); } - async hover(selector: string, options?: dom.PointerActionOptions & types.PointerActionWaitOptions) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.hover(helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.hover(helper.optionsWithUpdatedTimeout(options, deadline))); } - async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options?: types.NavigatingActionWaitOptions): Promise { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - const result = await handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); - return result; + async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise { + return await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, deadline))); } - async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - const result = await handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); - return result; + async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, deadline))); } - async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.type(text, helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.type(text, helper.optionsWithUpdatedTimeout(options, deadline))); } - async press(selector: string, key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.press(key, helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.press(key, helper.optionsWithUpdatedTimeout(options, deadline))); } - async check(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.check(helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.check(helper.optionsWithUpdatedTimeout(options, deadline))); } - async uncheck(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); - await handle.uncheck(helper.optionsWithUpdatedTimeout(options, deadline)); - handle.dispose(); + async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + await this._retryWithSelectorIfNotConnected(selector, options, + (handle, deadline) => handle.uncheck(helper.optionsWithUpdatedTimeout(options, deadline))); } async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: types.WaitForFunctionOptions & types.WaitForElementOptions = {}, arg?: any): Promise { @@ -773,14 +783,6 @@ export class Frame { return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); } - private async _waitForSelectorInUtilityContext(selector: string, options?: types.WaitForElementOptions): Promise<{ handle: dom.ElementHandle, deadline: number }> { - const { waitFor = 'attached' } = options || {}; - const deadline = this._page._timeoutSettings.computeDeadline(options); - const { world, task } = selectors._waitForSelectorTask(selector, waitFor, deadline); - const result = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selectorToString(selector, waitFor)}"`); - return { handle: result.asElement() as dom.ElementHandle, deadline }; - } - async waitForFunction(pageFunction: types.Func1, arg: Arg, options?: types.WaitForFunctionOptions): Promise>; async waitForFunction(pageFunction: types.Func1, arg?: any, options?: types.WaitForFunctionOptions): Promise>; async waitForFunction(pageFunction: types.Func1, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise> { diff --git a/src/helper.ts b/src/helper.ts index d5c2e8b84a..1782d4d7be 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -24,6 +24,7 @@ import { TimeoutError } from './errors'; import * as types from './types'; export const debugError = debug(`pw:error`); +export const debugInput = debug('pw:input'); export type RegisteredListener = { emitter: EventEmitter; @@ -346,6 +347,10 @@ class Helper { return seconds * 1000 + (nanoseconds / 1000000 | 0); } + static isPastDeadline(deadline: number) { + return deadline !== Number.MAX_SAFE_INTEGER && this.monotonicTime() >= deadline; + } + static timeUntilDeadline(deadline: number): number { return Math.min(deadline - this.monotonicTime(), 2147483647); // 2^31-1 safe setTimeout in Node. } diff --git a/src/injected/injected.ts b/src/injected/injected.ts index ff7a443793..37a1041c3a 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -16,9 +16,14 @@ import * as types from '../types'; -type Predicate = () => any; +type Predicate = () => T; +export type InjectedResult = + (T extends undefined ? { status: 'success', value?: T} : { status: 'success', value: T }) | + { status: 'notconnected' } | + { status: 'timeout' } | + { status: 'error', error: string }; -class Injected { +export class Injected { isVisible(element: Element): boolean { if (!element.ownerDocument || !element.ownerDocument.defaultView) return true; @@ -29,7 +34,7 @@ class Injected { return !!(rect.top || rect.bottom || rect.width || rect.height); } - private _pollMutation(predicate: Predicate, timeout: number): Promise { + private _pollMutation(predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); @@ -39,7 +44,7 @@ class Injected { return Promise.resolve(success); let fulfill: (result?: any) => void; - const result = new Promise(x => fulfill = x); + const result = new Promise(x => fulfill = x); const observer = new MutationObserver(() => { if (timedOut) { observer.disconnect(); @@ -60,13 +65,13 @@ class Injected { return result; } - private _pollRaf(predicate: Predicate, timeout: number): Promise { + private _pollRaf(predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); let fulfill: (result?: any) => void; - const result = new Promise(x => fulfill = x); + const result = new Promise(x => fulfill = x); const onRaf = () => { if (timedOut) { @@ -84,13 +89,13 @@ class Injected { return result; } - private _pollInterval(pollInterval: number, predicate: Predicate, timeout: number): Promise { + private _pollInterval(pollInterval: number, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); let fulfill: (result?: any) => void; - const result = new Promise(x => fulfill = x); + const result = new Promise(x => fulfill = x); const onTimeout = () => { if (timedOut) { fulfill(); @@ -107,7 +112,7 @@ class Injected { return result; } - poll(polling: 'raf' | 'mutation' | number, timeout: number, predicate: Predicate): Promise { + poll(polling: 'raf' | 'mutation' | number, timeout: number, predicate: Predicate): Promise { if (polling === 'raf') return this._pollRaf(predicate, timeout); if (polling === 'mutation') @@ -122,9 +127,11 @@ class Injected { return { left: parseInt(style.borderLeftWidth || '', 10), top: parseInt(style.borderTopWidth || '', 10) }; } - selectOptions(node: Node, optionsToSelect: (Node | types.SelectOption)[]) { + selectOptions(node: Node, optionsToSelect: (Node | types.SelectOption)[]): InjectedResult { if (node.nodeName.toLowerCase() !== 'select') - throw new Error('Element is not a element.' }; + if (!node.isConnected) + return { status: 'notconnected' }; const element = node as HTMLSelectElement; const options = Array.from(element.options); @@ -148,81 +155,97 @@ class Injected { } element.dispatchEvent(new Event('input', { 'bubbles': true })); element.dispatchEvent(new Event('change', { 'bubbles': true })); - return options.filter(option => option.selected).map(option => option.value); + return { status: 'success', value: options.filter(option => option.selected).map(option => option.value) }; } - fill(node: Node, value: string) { + fill(node: Node, value: string): InjectedResult { if (node.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; + return { status: 'error', error: 'Node is not of type HTMLElement' }; const element = node as HTMLElement; + if (!element.isConnected) + return { status: 'notconnected' }; if (!this.isVisible(element)) - return 'Element is not visible'; + return { status: 'error', error: 'Element is not visible' }; if (element.nodeName.toLowerCase() === 'input') { const input = element as HTMLInputElement; const type = (input.getAttribute('type') || '').toLowerCase(); const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']); const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']); if (!kTextInputTypes.has(type) && !kDateTypes.has(type)) - return 'Cannot fill input of type "' + type + '".'; + return { status: 'error', error: 'Cannot fill input of type "' + type + '".' }; if (type === 'number') { value = value.trim(); if (!value || isNaN(Number(value))) - return 'Cannot type text into input[type=number].'; + return { status: 'error', error: 'Cannot type text into input[type=number].' }; } if (input.disabled) - return 'Cannot fill a disabled input.'; + return { status: 'error', error: 'Cannot fill a disabled input.' }; if (input.readOnly) - return 'Cannot fill a readonly input.'; + return { status: 'error', error: 'Cannot fill a readonly input.' }; if (kDateTypes.has(type)) { value = value.trim(); input.focus(); input.value = value; if (input.value !== value) - return `Malformed ${type} "${value}"`; + return { status: 'error', error: `Malformed ${type} "${value}"` }; element.dispatchEvent(new Event('input', { 'bubbles': true })); element.dispatchEvent(new Event('change', { 'bubbles': true })); - return false; // We have already changed the value, no need to input it. + return { status: 'success', value: false }; // We have already changed the value, no need to input it. } } else if (element.nodeName.toLowerCase() === 'textarea') { const textarea = element as HTMLTextAreaElement; if (textarea.disabled) - return 'Cannot fill a disabled textarea.'; + return { status: 'error', error: 'Cannot fill a disabled textarea.' }; if (textarea.readOnly) - return 'Cannot fill a readonly textarea.'; + return { status: 'error', error: 'Cannot fill a readonly textarea.' }; } else if (!element.isContentEditable) { - return 'Element is not an ,