diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 8e65c7c67f..962f385c90 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -778,7 +778,9 @@ export class ElementHandle extends js.JSHandle { async _setChecked(progress: Progress, state: boolean, options: { position?: types.Point } & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> { const isChecked = async () => { const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {}); - return throwRetargetableDOMError(result); + if (result === 'error:notconnected' || result.received === 'error:notconnected') + throwElementIsNotAttached(); + return result.matches; }; await this._markAsTargetElement(progress.metadata); if (await isChecked() === state) @@ -913,10 +915,14 @@ export class ElementHandle extends js.JSHandle { export function throwRetargetableDOMError(result: T | 'error:notconnected'): T { if (result === 'error:notconnected') - throw new Error('Element is not attached to the DOM'); + throwElementIsNotAttached(); return result; } +export function throwElementIsNotAttached(): never { + throw new Error('Element is not attached to the DOM'); +} + export function assertDone(result: 'done'): void { // This function converts 'done' to void and ensures typescript catches unhandled errors. } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index ad793aded8..1d2098b92c 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1301,7 +1301,9 @@ export class Frame extends SdkObject { const result = await this._callOnElementOnceMatches(metadata, selector, (injected, element, data) => { return injected.elementState(element, data.state); }, { state }, options, scope); - return dom.throwRetargetableDOMError(result); + if (result.received === 'error:notconnected') + dom.throwElementIsNotAttached(); + return result.matches; } async isVisible(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise { @@ -1319,8 +1321,8 @@ export class Frame extends SdkObject { return false; return await resolved.injected.evaluate((injected, { info, root }) => { const element = injected.querySelector(info.parsed, root || document, info.strict); - const state = element ? injected.elementState(element, 'visible') : false; - return state === 'error:notconnected' ? false : state; + const state = element ? injected.elementState(element, 'visible') : { matches: false, received: 'error:notconnected' }; + return state.matches; }, { info: resolved.info, root: resolved.frame === this ? scope : undefined }); } catch (e) { if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e) || isSessionClosedError(e)) @@ -1809,26 +1811,6 @@ function verifyLifecycle(name: string, waitUntil: types.LifecycleEvent): types.L } function renderUnexpectedValue(expression: string, received: any): string { - if (expression === 'to.be.checked') - return received ? 'checked' : 'unchecked'; - if (expression === 'to.be.unchecked') - return received ? 'unchecked' : 'checked'; - if (expression === 'to.be.visible') - return received ? 'visible' : 'hidden'; - if (expression === 'to.be.hidden') - return received ? 'hidden' : 'visible'; - if (expression === 'to.be.enabled') - return received ? 'enabled' : 'disabled'; - if (expression === 'to.be.disabled') - return received ? 'disabled' : 'enabled'; - if (expression === 'to.be.editable') - return received ? 'editable' : 'readonly'; - if (expression === 'to.be.readonly') - return received ? 'readonly' : 'editable'; - if (expression === 'to.be.empty') - return received ? 'empty' : 'not empty'; - if (expression === 'to.be.focused') - return received ? 'focused' : 'not focused'; if (expression === 'to.match.aria') return received ? received.raw : received; return received; diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 4042ab5b49..0acc9bc76c 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -41,8 +41,9 @@ import { parseYamlTemplate } from '@isomorphic/ariaSnapshot'; export type FrameExpectParams = Omit & { expectedValue?: any }; -export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked'; -export type ElementState = ElementStateWithoutStable | 'stable'; +export type ElementState = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked' | 'mixed' | 'stable'; +export type ElementStateWithoutStable = Exclude; +export type ElementStateQueryResult = { matches: boolean, received?: string | 'error:notconnected' }; export type HitTargetInterceptionResult = { stop: () => 'done' | { hitTargetDescription: string }; @@ -545,15 +546,15 @@ export class InjectedScript { if (stableResult === false) return { missingState: 'stable' }; if (stableResult === 'error:notconnected') - return stableResult; + return 'error:notconnected'; } for (const state of states) { if (state !== 'stable') { const result = this.elementState(node, state); - if (result === false) + if (result.received === 'error:notconnected') + return 'error:notconnected'; + if (!result.matches) return { missingState: state }; - if (result === 'error:notconnected') - return result; } } } @@ -608,38 +609,50 @@ export class InjectedScript { return result; } - elementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' { - const element = this.retarget(node, ['stable', 'visible', 'hidden'].includes(state) ? 'none' : 'follow-label'); + elementState(node: Node, state: ElementStateWithoutStable): ElementStateQueryResult { + const element = this.retarget(node, ['visible', 'hidden'].includes(state) ? 'none' : 'follow-label'); if (!element || !element.isConnected) { if (state === 'hidden') - return true; - return 'error:notconnected'; + return { matches: true, received: 'hidden' }; + return { matches: false, received: 'error:notconnected' }; } - if (state === 'visible') - return isElementVisible(element); - if (state === 'hidden') - return !isElementVisible(element); + if (state === 'visible' || state === 'hidden') { + const visible = isElementVisible(element); + return { + matches: state === 'visible' ? visible : !visible, + received: visible ? 'visible' : 'hidden' + }; + } - const disabled = getAriaDisabled(element); - if (state === 'disabled') - return disabled; - if (state === 'enabled') - return !disabled; + if (state === 'disabled' || state === 'enabled') { + const disabled = getAriaDisabled(element); + return { + matches: state === 'disabled' ? disabled : !disabled, + received: disabled ? 'disabled' : 'enabled' + }; + } if (state === 'editable') { + const disabled = getAriaDisabled(element); const readonly = getReadonly(element); if (readonly === 'error') throw this.createStacklessError('Element is not an ,