From 31e0a63fcd99c6c5a36cbf881eac76833d31b30f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Dec 2021 10:31:26 -0800 Subject: [PATCH] feat(toBeChecked): allow passing checked: false (#10665) --- docs/src/api/class-locatorassertions.md | 5 ++- .../playwright-core/src/client/locator.ts | 2 +- .../src/server/injected/injectedScript.ts | 15 +++++--- .../playwright-test/src/matchers/matchers.ts | 7 ++-- .../playwright-test/types/testExpect.d.ts | 20 +++++++---- .../playwright.expect.true.spec.ts | 34 ++++++++++++++++--- 6 files changed, 62 insertions(+), 21 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index b89f9c127f..6a07c061a3 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -280,8 +280,11 @@ locator = page.locator(".subscribe") expect(locator).to_be_checked() ``` -### option: LocatorAssertions.toBeChecked.timeout = %%-assertions-timeout-%% +### option: LocatorAssertions.toBeChecked.checked +* langs: js +- `checked` <[boolean]> +### option: LocatorAssertions.toBeChecked.timeout = %%-assertions-timeout-%% ## method: LocatorAssertions.toBeDisabled diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 22cf6f7ffa..7456549931 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -234,7 +234,7 @@ export class Locator implements api.Locator { await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options }); } - async _expect(expression: string, options: FrameExpectOptions): Promise<{ matches: boolean, received?: any, log?: string[] }> { + async _expect(expression: string, options: Omit & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }> { const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot }; if (options.expectedValue) params.expectedValue = serializeArgument(options.expectedValue); diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 2c4ab0855a..1ed0414931 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -51,7 +51,7 @@ export type InjectedScriptPoll = { cancel: () => void, }; -export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked'; +export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked'; export type ElementState = ElementStateWithoutStable | 'stable'; export interface SelectorEngineV2 { @@ -502,14 +502,17 @@ export class InjectedScript { if (state === 'editable') return !disabled && editable; - if (state === 'checked') { - if (['checkbox', 'radio'].includes(element.getAttribute('role') || '')) - return element.getAttribute('aria-checked') === 'true'; + if (state === 'checked' || state === 'unchecked') { + if (['checkbox', 'radio'].includes(element.getAttribute('role') || '')) { + const result = element.getAttribute('aria-checked') === 'true'; + return state === 'checked' ? result : !result; + } if (element.nodeName !== 'INPUT') throw this.createStacklessError('Not a checkbox or radio button'); if (!['radio', 'checkbox'].includes((element as HTMLInputElement).type.toLowerCase())) throw this.createStacklessError('Not a checkbox or radio button'); - return (element as HTMLInputElement).checked; + const result = (element as HTMLInputElement).checked; + return state === 'checked' ? result : !result; } throw this.createStacklessError(`Unexpected element state "${state}"`); } @@ -899,6 +902,8 @@ export class InjectedScript { let elementState: boolean | 'error:notconnected' | 'error:notcheckbox' | undefined; if (expression === 'to.be.checked') { elementState = progress.injectedScript.elementState(element, 'checked'); + } else if (expression === 'to.be.unchecked') { + elementState = progress.injectedScript.elementState(element, 'unchecked'); } else if (expression === 'to.be.disabled') { elementState = progress.injectedScript.elementState(element, 'disabled'); } else if (expression === 'to.be.editable') { diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index 03ebcba26a..5392a2773d 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -24,7 +24,7 @@ import { toEqual } from './toEqual'; import { callLogText, toExpectedTextValues, toMatchText } from './toMatchText'; interface LocatorEx extends Locator { - _expect(expression: string, options: FrameExpectOptions): Promise<{ matches: boolean, received?: any, log?: string[] }>; + _expect(expression: string, options: Omit & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>; } interface APIResponseEx extends APIResponse { @@ -34,10 +34,11 @@ interface APIResponseEx extends APIResponse { export function toBeChecked( this: ReturnType, locator: LocatorEx, - options?: { timeout?: number }, + options?: { checked?: boolean, timeout?: number }, ) { return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout) => { - return await locator._expect('to.be.checked', { isNot, timeout }); + const checked = !options || options.checked === undefined || options.checked === true; + return await locator._expect(checked ? 'to.be.checked' : 'to.be.unchecked', { isNot, timeout }); }, options); } diff --git a/packages/playwright-test/types/testExpect.d.ts b/packages/playwright-test/types/testExpect.d.ts index f8457fb63a..07d5bb31f2 100644 --- a/packages/playwright-test/types/testExpect.d.ts +++ b/packages/playwright-test/types/testExpect.d.ts @@ -1,9 +1,17 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * Modifications copyright (c) Microsoft Corporation. + * Copyright (c) Microsoft Corporation. * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ import type * as expect from 'expect'; @@ -69,9 +77,9 @@ declare global { }): R; /** - * Asserts input is checked. + * Asserts input is checked (or unchecked if { checked: false } is passed). */ - toBeChecked(options?: { timeout?: number }): Promise; + toBeChecked(options?: { checked?: boolean, timeout?: number }): Promise; /** * Asserts input is disabled. diff --git a/tests/playwright-test/playwright.expect.true.spec.ts b/tests/playwright-test/playwright.expect.true.spec.ts index 3d3da42b5f..e80fb6d01e 100644 --- a/tests/playwright-test/playwright.expect.true.spec.ts +++ b/tests/playwright-test/playwright.expect.true.spec.ts @@ -27,6 +27,18 @@ test('should support toBeChecked', async ({ runInlineTest }) => { await expect(locator).toBeChecked(); }); + test('pass 2', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).toBeChecked({ checked: true }); + }); + + test('pass 3', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).not.toBeChecked({ checked: false }); + }); + test('fail', async ({ page }) => { await page.setContent(''); const locator = page.locator('input'); @@ -37,7 +49,7 @@ test('should support toBeChecked', async ({ runInlineTest }) => { const output = stripAscii(result.output); expect(output).toContain('Error: expect(received).toBeChecked()'); expect(output).toContain('expect(locator).toBeChecked'); - expect(result.passed).toBe(1); + expect(result.passed).toBe(3); expect(result.failed).toBe(1); expect(result.exitCode).toBe(1); }); @@ -53,22 +65,34 @@ test('should support toBeChecked w/ not', async ({ runInlineTest }) => { await expect(locator).not.toBeChecked(); }); + test('pass 2', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).toBeChecked({ checked: false }); + }); + test('fail not', async ({ page }) => { await page.setContent(''); const locator = page.locator('input'); - await expect(locator).not.toBeChecked({ timeout: 1000 }); + await expect(locator).not.toBeChecked({ timeout: 500 }); + }); + + test('fail 2', async ({ page }) => { + await page.setContent(''); + const locator = page.locator('input'); + await expect(locator).toBeChecked({ checked: false, timeout: 500 }); }); test('fail missing', async ({ page }) => { await page.setContent('
no inputs here
'); const locator2 = page.locator('input2'); - await expect(locator2).not.toBeChecked({ timeout: 1000 }); + await expect(locator2).not.toBeChecked({ timeout: 500 }); }); `, }, { workers: 1 }); const output = stripAscii(result.output); - expect(result.passed).toBe(1); - expect(result.failed).toBe(2); + expect(result.passed).toBe(2); + expect(result.failed).toBe(3); expect(result.exitCode).toBe(1); // fail not expect(output).toContain('Error: expect(received).not.toBeChecked()');