From 9e070299734696bf254856b2eb08615628202a17 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 28 Jul 2021 22:30:37 -0700 Subject: [PATCH] chore: relayout matchers (#7901) --- src/test/expect.ts | 15 +- src/test/matchers/matchers.ts | 242 +++++++++++++++++++++++++++++++ src/test/matchers/toBeTruthy.ts | 100 +------------ src/test/matchers/toEqual.ts | 28 +--- src/test/matchers/toMatchText.ts | 98 +------------ types/testExpect.d.ts | 104 ++++++------- 6 files changed, 304 insertions(+), 283 deletions(-) create mode 100644 src/test/matchers/matchers.ts diff --git a/src/test/expect.ts b/src/test/expect.ts index fbf061fa45..815d8c2c4b 100644 --- a/src/test/expect.ts +++ b/src/test/expect.ts @@ -24,20 +24,19 @@ import { toBeFocused, toBeHidden, toBeSelected, - toBeVisible -} from './matchers/toBeTruthy'; -import { toHaveLength, toHaveProp } from './matchers/toEqual'; -import { toMatchSnapshot } from './matchers/toMatchSnapshot'; -import { + toBeVisible, toContainText, toHaveAttr, - toHaveCSS, toHaveClass, + toHaveCSS, toHaveData, toHaveId, + toHaveLength, + toHaveProp, toHaveText, toHaveValue -} from './matchers/toMatchText'; +} from './matchers/matchers'; +import { toMatchSnapshot } from './matchers/toMatchSnapshot'; import type { Expect } from './types'; export const expect: Expect = expectLibrary as any; @@ -54,8 +53,8 @@ expectLibrary.extend({ toBeVisible, toContainText, toHaveAttr, - toHaveCSS, toHaveClass, + toHaveCSS, toHaveData, toHaveId, toHaveLength, diff --git a/src/test/matchers/matchers.ts b/src/test/matchers/matchers.ts new file mode 100644 index 0000000000..73ce4bf877 --- /dev/null +++ b/src/test/matchers/matchers.ts @@ -0,0 +1,242 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * 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 matchers from 'expect/build/matchers'; +import { Locator } from '../../..'; +import type { Expect } from '../types'; +import { toBeTruthy } from './toBeTruthy'; +import { toEqual } from './toEqual'; +import { toMatchText } from './toMatchText'; + +export async function toBeChecked( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeChecked', locator, async timeout => { + return await locator.isChecked({ timeout }); + }, options); +} + +export async function toBeDisabled( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeDisabled', locator, async timeout => { + return await locator.isDisabled({ timeout }); + }, options); +} + +export async function toBeEditable( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeEditable', locator, async timeout => { + return await locator.isEditable({ timeout }); + }, options); +} + +export async function toBeEmpty( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeEmpty', locator, async timeout => { + return await locator.evaluate(element => { + if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') + return !(element as HTMLInputElement).value; + return !element.textContent?.trim(); + }, { timeout }); + }, options); +} + +export async function toBeEnabled( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeEnabled', locator, async timeout => { + return await locator.isEnabled({ timeout }); + }, options); +} + +export async function toBeFocused( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeFocused', locator, async timeout => { + return await locator.evaluate(element => { + return document.activeElement === element; + }, { timeout }); + }, options); +} + +export async function toBeHidden( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeHidden', locator, async timeout => { + return await locator.isHidden({ timeout }); + }, options); +} + +export async function toBeSelected( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeSelected', locator, async timeout => { + return await locator.evaluate(element => { + return (element as HTMLOptionElement).selected; + }, { timeout }); + }, options); +} + +export async function toBeVisible( + this: ReturnType, + locator: Locator, + options?: { timeout?: number }, +) { + return toBeTruthy.call(this, 'toBeVisible', locator, async timeout => { + return await locator.isVisible({ timeout }); + }, options); +} + +export async function toContainText( + this: ReturnType, + locator: Locator, + expected: string, + options?: { timeout?: number, useInnerText?: boolean }, +) { + return toMatchText.call(this, 'toContainText', locator, async timeout => { + if (options?.useInnerText) + return await locator.innerText({ timeout }); + return await locator.textContent() || ''; + }, expected, { ...options, matchSubstring: true }); +} + +export async function toHaveAttr( + this: ReturnType, + locator: Locator, + name: string, + expected: string | RegExp, + options?: { timeout?: number }, +) { + return toMatchText.call(this, 'toHaveAttr', locator, async timeout => { + return await locator.getAttribute(name, { timeout }) || ''; + }, expected, options); +} + +export async function toHaveClass( + this: ReturnType, + locator: Locator, + expected: string, + options?: { timeout?: number }, +) { + return toMatchText.call(this, 'toHaveClass', locator, async timeout => { + return await locator.evaluate(element => element.className, { timeout }); + }, expected, { ...options, matchSubstring: true }); +} + +export async function toHaveCSS( + this: ReturnType, + locator: Locator, + name: string, + expected: string | RegExp, + options?: { timeout?: number }, +) { + return toMatchText.call(this, 'toHaveCSS', locator, async timeout => { + return await locator.evaluate(async (element, name) => { + return (window.getComputedStyle(element) as any)[name]; + }, name, { timeout }); + }, expected, options); +} + +export async function toHaveData( + this: ReturnType, + locator: Locator, + name: string, + expected: string | RegExp, + options?: { timeout?: number }, +) { + return toMatchText.call(this, 'toHaveData', locator, async timeout => { + return await locator.getAttribute('data-' + name, { timeout }) || ''; + }, expected, options); +} + +export async function toHaveId( + this: ReturnType, + locator: Locator, + expected: string | RegExp, + options?: { timeout?: number }, +) { + return toMatchText.call(this, 'toHaveId', locator, async timeout => { + return await locator.getAttribute('id', { timeout }) || ''; + }, expected, options); +} + +export async function toHaveLength( + this: ReturnType, + locator: Locator, + expected: number, + options?: { timeout?: number }, +) { + if (typeof locator !== 'object' || locator.constructor.name !== 'Locator') + return matchers.toHaveLength.call(this, locator, expected); + return toEqual.call(this, 'toHaveLength', locator, async timeout => { + return await locator.count(); + }, expected, { expectedType: 'number', ...options }); +} + +export async function toHaveProp( + this: ReturnType, + locator: Locator, + name: string, + expected: number, + options?: { timeout?: number }, +) { + return toEqual.call(this, 'toHaveProp', locator, async timeout => { + return await locator.evaluate((element, name) => (element as any)[name], name, { timeout }); + }, expected, { expectedType: 'number', ...options }); +} + +export async function toHaveText( + this: ReturnType, + locator: Locator, + expected: string | RegExp, + options?: { timeout?: number, useInnerText?: boolean }, +) { + return toMatchText.call(this, 'toHaveText', locator, async timeout => { + if (options?.useInnerText) + return await locator.innerText({ timeout }); + return await locator.textContent() || ''; + }, expected, options); +} + +export async function toHaveValue( + this: ReturnType, + locator: Locator, + expected: string | RegExp, + options?: { timeout?: number }, +) { + return toMatchText.call(this, 'toHaveValue', locator, async timeout => { + return await locator.inputValue({ timeout }); + }, expected, options); +} diff --git a/src/test/matchers/toBeTruthy.ts b/src/test/matchers/toBeTruthy.ts index 0b7b708c6c..8c1b3404bd 100644 --- a/src/test/matchers/toBeTruthy.ts +++ b/src/test/matchers/toBeTruthy.ts @@ -23,7 +23,7 @@ import { currentTestInfo } from '../globals'; import type { Expect } from '../types'; import { expectLocator, monotonicTime, pollUntilDeadline } from '../util'; -async function toBeTruthyImpl( +export async function toBeTruthy( this: ReturnType, matcherName: string, locator: Locator, @@ -59,101 +59,3 @@ async function toBeTruthyImpl( return { message, pass }; } - -export async function toBeChecked( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeChecked', locator, async timeout => { - return await locator.isChecked({ timeout }); - }, options); -} - -export async function toBeEditable( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeEditable', locator, async timeout => { - return await locator.isEditable({ timeout }); - }, options); -} - -export async function toBeEnabled( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeEnabled', locator, async timeout => { - return await locator.isEnabled({ timeout }); - }, options); -} - -export async function toBeDisabled( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeDisabled', locator, async timeout => { - return await locator.isDisabled({ timeout }); - }, options); -} - -export async function toBeEmpty( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeEmpty', locator, async timeout => { - return await locator.evaluate(element => { - if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') - return !(element as HTMLInputElement).value; - return !element.textContent?.trim(); - }, { timeout }); - }, options); -} - -export async function toBeHidden( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeHidden', locator, async timeout => { - return await locator.isHidden({ timeout }); - }, options); -} - -export async function toBeVisible( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeVisible', locator, async timeout => { - return await locator.isVisible({ timeout }); - }, options); -} - -export async function toBeFocused( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeFocused', locator, async timeout => { - return await locator.evaluate(element => { - return document.activeElement === element; - }, { timeout }); - }, options); -} - -export async function toBeSelected( - this: ReturnType, - locator: Locator, - options?: { timeout?: number }, -) { - return toBeTruthyImpl.call(this, 'toBeSelected', locator, async timeout => { - return await locator.evaluate(element => { - return (element as HTMLOptionElement).selected; - }, { timeout }); - }, options); -} diff --git a/src/test/matchers/toEqual.ts b/src/test/matchers/toEqual.ts index d20a1fc3db..bb72df8630 100644 --- a/src/test/matchers/toEqual.ts +++ b/src/test/matchers/toEqual.ts @@ -15,7 +15,6 @@ */ import { equals } from 'expect/build/jasmineUtils'; -import matchers from 'expect/build/matchers'; import { iterableEquality } from 'expect/build/utils'; @@ -38,7 +37,7 @@ const RECEIVED_LABEL = 'Received'; // The optional property of matcher context is true if undefined. const isExpand = (expand?: boolean): boolean => expand !== false; -async function toEqualImpl( +export async function toEqual( this: ReturnType, matcherName: string, locator: Locator, @@ -94,28 +93,3 @@ async function toEqualImpl( // or create a different error message return { actual: received, expected, message, name: matcherName, pass }; } - -export async function toHaveLength( - this: ReturnType, - locator: Locator, - expected: number, - options?: { timeout?: number }, -) { - if (typeof locator !== 'object' || locator.constructor.name !== 'Locator') - return matchers.toHaveLength.call(this, locator, expected); - return toEqualImpl.call(this, 'toHaveLength', locator, async timeout => { - return await locator.count(); - }, expected, { expectedType: 'number', ...options }); -} - -export async function toHaveProp( - this: ReturnType, - locator: Locator, - name: string, - expected: number, - options?: { timeout?: number }, -) { - return toEqualImpl.call(this, 'toHaveProp', locator, async timeout => { - return await locator.evaluate((element, name) => (element as any)[name], name, { timeout }); - }, expected, { expectedType: 'number', ...options }); -} diff --git a/src/test/matchers/toMatchText.ts b/src/test/matchers/toMatchText.ts index 4254625dfe..b1e8afdd1d 100644 --- a/src/test/matchers/toMatchText.ts +++ b/src/test/matchers/toMatchText.ts @@ -33,7 +33,7 @@ import { currentTestInfo } from '../globals'; import type { Expect } from '../types'; import { expectLocator, monotonicTime, pollUntilDeadline } from '../util'; -async function toMatchTextImpl( +export async function toMatchText( this: ReturnType, matcherName: string, locator: Locator, @@ -122,99 +122,3 @@ async function toMatchTextImpl( return { message, pass }; } - -export async function toHaveText( - this: ReturnType, - locator: Locator, - expected: string | RegExp, - options?: { timeout?: number, useInnerText?: boolean }, -) { - return toMatchTextImpl.call(this, 'toHaveText', locator, async timeout => { - if (options?.useInnerText) - return await locator.innerText({ timeout }); - return await locator.textContent() || ''; - }, expected, options); -} - -export async function toContainText( - this: ReturnType, - locator: Locator, - expected: string, - options?: { timeout?: number, useInnerText?: boolean }, -) { - return toMatchTextImpl.call(this, 'toContainText', locator, async timeout => { - if (options?.useInnerText) - return await locator.innerText({ timeout }); - return await locator.textContent() || ''; - }, expected, { ...options, matchSubstring: true }); -} - -export async function toHaveAttr( - this: ReturnType, - locator: Locator, - name: string, - expected: string | RegExp, - options?: { timeout?: number }, -) { - return toMatchTextImpl.call(this, 'toHaveAttr', locator, async timeout => { - return await locator.getAttribute(name, { timeout }) || ''; - }, expected, options); -} - -export async function toHaveData( - this: ReturnType, - locator: Locator, - name: string, - expected: string | RegExp, - options?: { timeout?: number }, -) { - return toMatchTextImpl.call(this, 'toHaveData', locator, async timeout => { - return await locator.getAttribute('data-' + name, { timeout }) || ''; - }, expected, options); -} - -export async function toHaveCSS( - this: ReturnType, - locator: Locator, - name: string, - expected: string | RegExp, - options?: { timeout?: number }, -) { - return toMatchTextImpl.call(this, 'toHaveCSS', locator, async timeout => { - return await locator.evaluate(async (element, name) => { - return (window.getComputedStyle(element) as any)[name]; - }, name, { timeout }); - }, expected, options); -} - -export async function toHaveId( - this: ReturnType, - locator: Locator, - expected: string | RegExp, - options?: { timeout?: number }, -) { - return toMatchTextImpl.call(this, 'toHaveId', locator, async timeout => { - return await locator.getAttribute('id', { timeout }) || ''; - }, expected, options); -} - -export async function toHaveValue( - this: ReturnType, - locator: Locator, - expected: string | RegExp, - options?: { timeout?: number }, -) { - return toMatchTextImpl.call(this, 'toHaveValue', locator, async timeout => { - return await locator.inputValue({ timeout }); - }, expected, options); -} -export async function toHaveClass( - this: ReturnType, - locator: Locator, - expected: string, - options?: { timeout?: number }, -) { - return toMatchTextImpl.call(this, 'toHaveClass', locator, async timeout => { - return await locator.evaluate(element => element.className, { timeout }); - }, expected, { ...options, matchSubstring: true }); -} diff --git a/types/testExpect.d.ts b/types/testExpect.d.ts index c04fbd8e67..80ef86cd13 100644 --- a/types/testExpect.d.ts +++ b/types/testExpect.d.ts @@ -69,66 +69,36 @@ declare global { threshold?: number }): R; - /** - * Asserts element's exact text content. - */ - toHaveText(expected: string | RegExp, options?: { timeout?: number, useInnerText?: boolean }): Promise; - - /** - * Asserts element's text content matches given pattern or contains given substring. - */ - toContainText(expected: string, options?: { timeout?: number, useInnerText?: boolean }): Promise; - - /** - * Asserts element's attributes `name` matches expected value. - */ - toHaveAttr(expected: string | RegExp, name: string, options?: { timeout?: number }): Promise; - - /** - * Asserts element's data attribute data-`name` matches expected value. - */ - toHaveData(expected: string | RegExp, name: string, options?: { timeout?: number }): Promise; - - /** - * Asserts element's computed CSS property `name` matches expected value. - */ - toHaveCSS(expected: string | RegExp, name: string, options?: { timeout?: number }): Promise; - - /** - * Asserts element's `id` attribute matches expected value. - */ - toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise; - - /** - * Asserts input element's value. - */ - toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise; - /** * Asserts input is checked. */ toBeChecked(options?: { timeout?: number }): Promise; - /** - * Asserts input is editable. - */ - toBeEditable(options?: { timeout?: number }): Promise; - - /** - * Asserts input is enabled. - */ - toBeEnabled(options?: { timeout?: number }): Promise; - /** * Asserts input is disabled. */ toBeDisabled(options?: { timeout?: number }): Promise; + /** + * Asserts input is editable. + */ + toBeEditable(options?: { timeout?: number }): Promise; + /** * Asserts given DOM node or input has no text content or no input value. */ toBeEmpty(options?: { timeout?: number }): Promise; + /** + * Asserts input is enabled. + */ + toBeEnabled(options?: { timeout?: number }): Promise; + + /** + * Asserts given DOM is a focused (active) in document. + */ + toBeFocused(options?: { timeout?: number }): Promise; + /** * Asserts given DOM node is hidden or detached from DOM. */ @@ -139,25 +109,55 @@ declare global { */ toBeVisible(options?: { timeout?: number }): Promise; - /** - * Asserts given DOM is a focused (active) in document. - */ - toBeFocused(options?: { timeout?: number }): Promise; - /** * Asserts given select option is selected */ toBeSelected(options?: { timeout?: number }): Promise; + /** + * Asserts element's text content matches given pattern or contains given substring. + */ + toContainText(expected: string, options?: { timeout?: number, useInnerText?: boolean }): Promise; + + /** + * Asserts element's attributes `name` matches expected value. + */ + toHaveAttr(expected: string | RegExp, name: string, options?: { timeout?: number }): Promise; + + /** + * Asserts that DOM node has a given CSS class. + */ + toHaveClass(className: string, options?: { timeout?: number }): Promise; + + /** + * Asserts element's computed CSS property `name` matches expected value. + */ + toHaveCSS(expected: string | RegExp, name: string, options?: { timeout?: number }): Promise; + + /** + * Asserts element's data attribute data-`name` matches expected value. + */ + toHaveData(expected: string | RegExp, name: string, options?: { timeout?: number }): Promise; + + /** + * Asserts element's `id` attribute matches expected value. + */ + toHaveId(expected: string | RegExp, options?: { timeout?: number }): Promise; + /** * Asserts JavaScript object that corresponds to the Node has a property with given value. */ toHaveProp(name: string, value: any, options?: { timeout?: number }): Promise; /** - * Asserts that DOM node has a given CSS class. + * Asserts element's exact text content. */ - toHaveClass(className: string, options?: { timeout?: number }): Promise; + toHaveText(expected: string | RegExp, options?: { timeout?: number, useInnerText?: boolean }): Promise; + + /** + * Asserts input element's value. + */ + toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise; } } }