diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82902dd68..b5d24fd51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,5 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install Dependencies run: npm install --force - - name: Build - run: npm run build - - name: Run Tests - run: npm test + - name: Run All Checks + run: npm run checks:all diff --git a/docs/API.md b/docs/API.md index 7f9ca71ba..24ddae8c4 100644 --- a/docs/API.md +++ b/docs/API.md @@ -2,6 +2,8 @@ When you're writing tests, you often need to check that values meet certain conditions. `expect` gives you access to a number of "matchers" that let you validate different things on the `browser`, an `element` or `mock` object. +**Note:** Multi-remote is not yet supported; any working case is coincidental and could break or change until fully supported. + ## Soft Assertions Soft assertions allow you to continue test execution even when an assertion fails. This is useful when you want to check multiple conditions in a test and collect all failures rather than stopping at the first failure. Failures are collected and reported at the end of the test. @@ -252,6 +254,41 @@ await expect(browser).toHaveClipboardText(expect.stringContaining('clipboard tex ## Element Matchers +### Multiples Elements Support + +All element matchers support arrays (e.g., `$$()` results). + +- Each element must pass the matcher for the assertion to succeed; if any fail, the assertion fails. + - `toHaveText` differ and keep it's legacy behavior. +- See [MultipleElements.md](MultipleElements.md) for details. + +#### Usage + +```ts +await expect($$('#someElem')).toBeDisplayed() +await expect(await $$('#someElem')).toBeDisplayed() +``` + +```ts +const elements = await $$('#someElem') + +// Single value: checked against every element +await expect(elements).toHaveAttribute('class', 'form-control') + +// Array: each value checked at corresponding element index (must match length) +await expect(elements).toHaveAttribute('class', ['control1', 'control2']) + +// Use asymmetric matchers for flexible matching +await expect(elements).toHaveAttribute('class', [expect.stringContaining('control1'), 'control2']) + +// Use RegEx `i` for case insensitive +await expect(elements).toHaveAttribute('class', [/'Control1'/i, 'control2']) + + +// Works with filtered arrays too +await expect($$('#someElem').filter(el => el.isDisplayed())).toHaveAttribute('class', ['control1', 'control2']) +``` + ### toBeDisplayed Calls [`isDisplayed`](https://webdriver.io/docs/api/element/isDisplayed/) on given element. @@ -259,8 +296,7 @@ Calls [`isDisplayed`](https://webdriver.io/docs/api/element/isDisplayed/) on giv ##### Usage ```js -const elem = await $('#someElem') -await expect(elem).toBeDisplayed() +await expect($('#someElem')).toBeDisplayed() ``` ### toExist @@ -270,8 +306,7 @@ Calls [`isExisting`](https://webdriver.io/docs/api/element/isExisting) on given ##### Usage ```js -const elem = await $('#someElem') -await expect(elem).toExist() +await expect($('#someElem')).toExist() ``` ### toBePresent @@ -281,8 +316,7 @@ Same as `toExist`. ##### Usage ```js -const elem = await $('#someElem') -await expect(elem).toBePresent() +await expect($('#someElem')).toBePresent() ``` ### toBeExisting @@ -375,8 +409,7 @@ Checks if an element can be clicked by calling [`isClickable`](https://webdriver ##### Usage ```js -const elem = await $('#elem') -await expect(elem).toBeClickable() +await expect($('#elem')).toBeClickable() ``` ### toBeDisabled @@ -412,8 +445,7 @@ Checks if an element is enabled by calling [`isSelected`](https://webdriver.io/d ##### Usage ```js -const elem = await $('#elem') -await expect(elem).toBeSelected() +await expect($('#elem')).toBeSelected() ``` ### toBeChecked @@ -423,8 +455,7 @@ Same as `toBeSelected`. ##### Usage ```js -const elem = await $('#elem') -await expect(elem).toBeChecked() +await expect($('#elem')).toBeChecked() ``` ### toHaveComputedLabel @@ -502,8 +533,7 @@ Checks if element has a specific `id` attribute. ##### Usage ```js -const elem = await $('#elem') -await expect(elem).toHaveId('elem') +await expect($('#elem')).toHaveId('elem') ``` ### toHaveStyle @@ -513,8 +543,7 @@ Checks if an element has specific `CSS` properties. By default, values must matc ##### Usage ```js -const elem = await $('#elem') -await expect(elem).toHaveStyle({ +await expect($('#elem')).toHaveStyle({ 'font-family': 'Faktum', 'font-weight': '500', 'font-size': '12px', @@ -549,10 +578,11 @@ In case there is a list of elements in the div below: You can assert them using an array: ```js -const elem = await $$('ul > li') -await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk']) +await expect($$('ul > li')).toHaveText(['Coffee', 'Tea', 'Milk']) ``` +**Note:** Assertion with multiple elements will pass if the element text's matches any of the text in the arrays. Strict array matching is not yet supported. + ### toHaveHTML Checks if element has a specific text. Can also be called with an array as parameter in the case where the element can have different texts. @@ -583,8 +613,7 @@ Checks if an element is within the viewport by calling [`isDisplayedInViewport`] ##### Usage ```js -const elem = await $('#elem') -await expect(elem).toBeDisplayedInViewport() +await expect($('#elem')).toBeDisplayedInViewport() ``` ### toHaveChildren diff --git a/docs/MultipleElements.md b/docs/MultipleElements.md new file mode 100644 index 000000000..bd2afa1ad --- /dev/null +++ b/docs/MultipleElements.md @@ -0,0 +1,42 @@ +# Multiple Elements Support + +Matchers element array support (e.g., `$$()`): + +- **Strict Index-based Matching**: If an array of expected values is provided, it must match the elements' count; each value is checked at its index. +- If a single value is provided, every element is compared to it. +- Asymmetric matchers (e.g., `expect.stringContaining`) work within expected value arrays. +- An error is thrown if no elements are found (except with `toBeElementsArrayOfSize`). +- Options like `StringOptions` or `HTMLOptions` apply to the whole array; `NumberOptions` behaves like any expected provided value. +- The assertion passes only if **all** elements match. +- Using `.not` means all elements must **not** match. + +**Note:** Strict Index-based matching does not apply to `toHaveText`, since an existing behavior was already in placed. + +## Limitations +- Instead of `StringOptions` for a single expected value, use RegExp or asymmetric matchers. + - For `ignoreCase` use RegEx (`/MyExample/i`) + - For `containing` use Asymmetric Matchers (`expect.stringContaining('Example')`) +- Passing an array of "containing" values is deprecated and not supported outside `toHaveText`. + +## Supported types +You can pass any of these element types to `expect`: +- `ChainablePromiseArray` (the non-awaited case) +- `ElementArray` (the awaited case) +- `Element[]` (the filtered case) + +## Alternative + +For more granular or explicit per-element validation, use a parameterized test of your framework. +Example in Mocha: +```ts + describe('Element at index of `$$`', function () { + [ { expectedText: 'one', index: 0 }, + { expectedText: 'two', index: 2 }, + { expectedText: 'four', index: 4 }, + ].forEach(function ( { expectedText, index } ) { + it("Element at $index of `$$('label')` is $expectedText", function () { + expect($$('label')[index]).toHaveText(expectedText); + }); + }); + }); +``` diff --git a/package-lock.json b/package-lock.json index ddb8c6569..ba7808956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@types/node": "^24.10.1", "@vitest/coverage-v8": "^4.0.16", "@wdio/eslint": "^0.1.2", - "@wdio/types": "^9.20.0", + "@wdio/types": "^9.23.3", "eslint": "^9.39.2", "husky": "^9.1.7", "npm-run-all2": "^8.0.4", @@ -32,7 +32,7 @@ "shelljs": "^0.10.0", "typescript": "^5.9.3", "vitest": "^4.0.16", - "webdriverio": "^9.21.0" + "webdriverio": "^9.23.3" }, "engines": { "node": ">=20" @@ -2433,9 +2433,9 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.1.tgz", - "integrity": "sha512-YmhAxs7XPuxN0j7LJloHpfD1ylhDuFmmwMvfy/+6nBSrETT2ycL53LrhgPtR+f+GcPSybQVuQ5inWWu5MrWCpA==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.2.tgz", + "integrity": "sha512-GBY0+2lI9fDrjgb5dFL9+enKXqyOPok9PXg/69NVkjW3bikbK9RQrNrI3qccQXmDNN7ln4j/yL89Qgvj/tfqrw==", "license": "Apache-2.0", "dependencies": { "debug": "^4.4.3", @@ -3407,17 +3407,18 @@ } }, "node_modules/@wdio/config": { - "version": "9.23.2", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.23.2.tgz", - "integrity": "sha512-19Z+AIQ1NUpr6ncTumjSthm6A7c3DbaGTp+VCdcyN+vHYOK4WsWIomSk+uSbFosYFQVGRjCaHaeGSnC8GNPGYQ==", + "version": "9.23.3", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.23.3.tgz", + "integrity": "sha512-tQCT1R6R3hdib7Qb+82Dxgn/sB+CiR8+GS4zyJh5vU0dzLGeYsCo2B5W89VLItvRjveTmsmh8NOQGV2KH0FHTQ==", "license": "MIT", "dependencies": { "@wdio/logger": "9.18.0", - "@wdio/types": "9.23.2", - "@wdio/utils": "9.23.2", + "@wdio/types": "9.23.3", + "@wdio/utils": "9.23.3", "deepmerge-ts": "^7.0.3", "glob": "^10.2.2", - "import-meta-resolve": "^4.0.0" + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" }, "engines": { "node": ">=18.20.0" @@ -3521,9 +3522,9 @@ } }, "node_modules/@wdio/protocols": { - "version": "9.23.2", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.23.2.tgz", - "integrity": "sha512-pmCYOYI2N89QCC8IaiHwaWyP0mR8T1iKkEGpoTq2XVihp7VK/lfPvieyeZT5/e28MadYLJsDQ603pbu5J1NRDg==", + "version": "9.23.3", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.23.3.tgz", + "integrity": "sha512-QfA3Gfl9/3QRX1FnH7x2+uZrgpkwYcksgk1bxGLzl/E0Qefp3BkhgHAfSB1+iKsiYIw9iFOLVx+x+zh0F4BSeg==", "license": "MIT" }, "node_modules/@wdio/repl": { @@ -3554,9 +3555,9 @@ "license": "MIT" }, "node_modules/@wdio/types": { - "version": "9.23.2", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.23.2.tgz", - "integrity": "sha512-ryfrERGsNp+aCcrTE1rFU6cbmDj8GHZ04R9k52KNt2u1a6bv3Eh5A/cUA0hXuMdEUfsc8ePLYdwQyOLFydZ0ig==", + "version": "9.23.3", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.23.3.tgz", + "integrity": "sha512-Ufjh06DAD7cGTMORUkq5MTZLw1nAgBSr2y8OyiNNuAfPGCwHEU3EwEfhG/y0V7S7xT5pBxliqWi7AjRrCgGcIA==", "license": "MIT", "dependencies": { "@types/node": "^20.1.0" @@ -3581,14 +3582,14 @@ "license": "MIT" }, "node_modules/@wdio/utils": { - "version": "9.23.2", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.23.2.tgz", - "integrity": "sha512-+QfgXUWeA940AXT5l5UlrBKoHBk9GLSQE3BA+7ra1zWuFvv6SHG6M2mwplcPlOlymJMqXy8e7ZgLEoLkXuvC1Q==", + "version": "9.23.3", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.23.3.tgz", + "integrity": "sha512-LO/cTpOcb3r49psjmWTxjFduHUMHDOhVfSzL1gfBCS5cGv6h3hAWOYw/94OrxLn1SIOgZu/hyLwf3SWeZB529g==", "license": "MIT", "dependencies": { "@puppeteer/browsers": "^2.2.0", "@wdio/logger": "9.18.0", - "@wdio/types": "9.23.2", + "@wdio/types": "9.23.3", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", "edgedriver": "^6.1.2", @@ -3606,9 +3607,9 @@ } }, "node_modules/@zip.js/zip.js": { - "version": "2.8.15", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.15.tgz", - "integrity": "sha512-HZKJLFe4eGVgCe9J87PnijY7T1Zn638bEHS+Fm/ygHZozRpefzWcOYfPaP52S8pqk9g4xN3+LzMDl3Lv9dLglA==", + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.16.tgz", + "integrity": "sha512-kCjaXh50GCf9afcof6ekjXPKR//rBVIxNHJLSUaM3VAET2F0+hymgrK1GpInRIIFUpt+wsnUfgx2+bbrmc+7Tw==", "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", @@ -5701,9 +5702,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.3.tgz", - "integrity": "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", + "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", "funding": [ { "type": "github", @@ -6959,7 +6960,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -9727,18 +9727,18 @@ } }, "node_modules/webdriver": { - "version": "9.23.2", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.23.2.tgz", - "integrity": "sha512-HZy3eydZbmex0pbyLwHaDsAyZ+S+V4XQTdGK/nAOi4uPa74U6yT9vXqtb+3B+5/LDM7L8kTD6Z3b1y4gB4pmTw==", + "version": "9.23.3", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.23.3.tgz", + "integrity": "sha512-8FdXOhzkxqDI6F1dyIsQONhKLDZ9HPSEwNBnH3bD1cHnj/6nVvyYrUtDPo/+J324BuwOa1IVTH3m8mb3B2hTlA==", "license": "MIT", "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "9.23.2", + "@wdio/config": "9.23.3", "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.23.2", - "@wdio/types": "9.23.2", - "@wdio/utils": "9.23.2", + "@wdio/protocols": "9.23.3", + "@wdio/types": "9.23.3", + "@wdio/utils": "9.23.3", "deepmerge-ts": "^7.0.3", "https-proxy-agent": "^7.0.6", "undici": "^6.21.3", @@ -9764,19 +9764,19 @@ "license": "MIT" }, "node_modules/webdriverio": { - "version": "9.23.2", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.23.2.tgz", - "integrity": "sha512-VjfTw1bRJdBrzjoCu7BGThxn1JK2V7mAGvxibaBrCNIayPPQjLhVDNJPOVEiR7txM6zmOUWxhkCDxHjhMYirfQ==", + "version": "9.23.3", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.23.3.tgz", + "integrity": "sha512-1dhMsBx/GLHJsDLhg/xuEQ48JZPrbldz7qdFT+MXQZADj9CJ4bJywWtVBME648MmVMfgDvLc5g2ThGIOupSLvQ==", "license": "MIT", "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.23.2", + "@wdio/config": "9.23.3", "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.23.2", + "@wdio/protocols": "9.23.3", "@wdio/repl": "9.16.2", - "@wdio/types": "9.23.2", - "@wdio/utils": "9.23.2", + "@wdio/types": "9.23.3", + "@wdio/utils": "9.23.3", "archiver": "^7.0.1", "aria-query": "^5.3.0", "cheerio": "^1.0.0-rc.12", @@ -9793,7 +9793,7 @@ "rgb2hex": "0.2.5", "serialize-error": "^12.0.0", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.23.2" + "webdriver": "9.23.3" }, "engines": { "node": ">=18.20.0" diff --git a/package.json b/package.json index 2c14c4626..86a9c5a5c 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@jest/globals": "^30.2.0", "@vitest/coverage-v8": "^4.0.16", "@wdio/eslint": "^0.1.2", - "@wdio/types": "^9.20.0", + "@wdio/types": "^9.23.3", "eslint": "^9.39.2", "husky": "^9.1.7", "npm-run-all2": "^8.0.4", @@ -91,7 +91,7 @@ "shelljs": "^0.10.0", "typescript": "^5.9.3", "vitest": "^4.0.16", - "webdriverio": "^9.21.0" + "webdriverio": "^9.23.3" }, "peerDependencies": { "@wdio/globals": "^9.0.0", diff --git a/src/matchers.ts b/src/matchers.ts index 323fafc05..c6e795b7b 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -1,6 +1,9 @@ +// Browser matchers export * from './matchers/browser/toHaveClipboardText.js' export * from './matchers/browser/toHaveTitle.js' export * from './matchers/browser/toHaveUrl.js' + +// Element matchers export * from './matchers/element/toBeClickable.js' export * from './matchers/element/toBeDisabled.js' export * from './matchers/element/toBeDisplayed.js' @@ -11,9 +14,9 @@ export * from './matchers/element/toBeFocused.js' export * from './matchers/element/toBeSelected.js' export * from './matchers/element/toHaveAttribute.js' export * from './matchers/element/toHaveChildren.js' -export * from './matchers/element/toHaveClass.js' export * from './matchers/element/toHaveComputedLabel.js' export * from './matchers/element/toHaveComputedRole.js' +export * from './matchers/element/toHaveElementClass.js' export * from './matchers/element/toHaveElementProperty.js' export * from './matchers/element/toHaveHeight.js' export * from './matchers/element/toHaveHref.js' @@ -25,7 +28,11 @@ export * from './matchers/element/toHaveText.js' export * from './matchers/element/toHaveValue.js' export * from './matchers/element/toHaveWidth.js' export * from './matchers/elements/toBeElementsArrayOfSize.js' + +// Mock matchers export * from './matchers/mock/toBeRequested.js' export * from './matchers/mock/toBeRequestedTimes.js' export * from './matchers/mock/toBeRequestedWith.js' + +// Snapshot matchers export * from './matchers/snapshot.js' diff --git a/src/matchers/browser/toHaveClipboardText.ts b/src/matchers/browser/toHaveClipboardText.ts index 00b023408..9fe9f20ec 100644 --- a/src/matchers/browser/toHaveClipboardText.ts +++ b/src/matchers/browser/toHaveClipboardText.ts @@ -10,25 +10,27 @@ export async function toHaveClipboardText( expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'clipboard text', verb = 'have' } = this + const { expectation = 'clipboard text', verb = 'have', matcherName = 'toHaveClipboardText', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toHaveClipboardText', + matcherName, expectedValue, options, }) let actual - const pass = await waitUntil(async () => { - await browser.setPermissions({ name: 'clipboard-read' }, 'granted') + const pass = await waitUntil( + async () => { + await browser.setPermissions({ name: 'clipboard-read' }, 'granted') /** * changes are that some browser don't support the clipboard API yet */ - .catch((err) => log.warn(`Couldn't set clipboard permissions: ${err}`)) - actual = await browser.execute(() => window.navigator.clipboard.readText()) - return compareText(actual, expectedValue, options).result - }, isNot, options) + .catch((err) => log.warn(`Couldn't set clipboard permissions: ${err}`)) + actual = await browser.execute(() => window.navigator.clipboard.readText()) + return compareText(actual, expectedValue, options).result + }, + isNot, + options) const message = enhanceError('browser', expectedValue, actual, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { @@ -37,7 +39,7 @@ export async function toHaveClipboardText( } await options.afterAssertion?.({ - matcherName: 'toHaveClipboardText', + matcherName, expectedValue, options, result diff --git a/src/matchers/browser/toHaveTitle.ts b/src/matchers/browser/toHaveTitle.ts index 4c18dd7f8..669e3a4d2 100644 --- a/src/matchers/browser/toHaveTitle.ts +++ b/src/matchers/browser/toHaveTitle.ts @@ -6,21 +6,24 @@ export async function toHaveTitle( expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'title', verb = 'have' } = this + const { expectation = 'title', verb = 'have', matcherName = 'toHaveTitle', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toHaveTitle', + matcherName, expectedValue, options, }) let actual - const pass = await waitUntil(async () => { - actual = await browser.getTitle() + const pass = await waitUntil( + async () => { + actual = await browser.getTitle() - return compareText(actual, expectedValue, options).result - }, isNot, options) + return compareText(actual, expectedValue, options).result + }, + isNot, + options + ) const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { @@ -29,7 +32,7 @@ export async function toHaveTitle( } await options.afterAssertion?.({ - matcherName: 'toHaveTitle', + matcherName, expectedValue, options, result diff --git a/src/matchers/browser/toHaveUrl.ts b/src/matchers/browser/toHaveUrl.ts index 06719ac5d..08032b333 100644 --- a/src/matchers/browser/toHaveUrl.ts +++ b/src/matchers/browser/toHaveUrl.ts @@ -6,21 +6,24 @@ export async function toHaveUrl( expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'url', verb = 'have' } = this + const { expectation = 'url', verb = 'have', matcherName = 'toHaveUrl', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toHaveUrl', + matcherName, expectedValue, options, }) let actual - const pass = await waitUntil(async () => { - actual = await browser.getUrl() + const pass = await waitUntil( + async () => { + actual = await browser.getUrl() - return compareText(actual, expectedValue, options).result - }, isNot, options) + return compareText(actual, expectedValue, options).result + }, + isNot, + options + ) const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { @@ -29,7 +32,7 @@ export async function toHaveUrl( } await options.afterAssertion?.({ - matcherName: 'toHaveUrl', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toBeClickable.ts b/src/matchers/element/toBeClickable.ts index 5e9f95bad..607409165 100644 --- a/src/matchers/element/toBeClickable.ts +++ b/src/matchers/element/toBeClickable.ts @@ -1,9 +1,9 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toBeClickable( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { this.expectation = this.expectation || 'clickable' diff --git a/src/matchers/element/toBeDisabled.ts b/src/matchers/element/toBeDisabled.ts index 471304675..429fafdd1 100644 --- a/src/matchers/element/toBeDisabled.ts +++ b/src/matchers/element/toBeDisabled.ts @@ -1,9 +1,9 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toBeDisabled( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { this.expectation = this.expectation || 'disabled' diff --git a/src/matchers/element/toBeDisplayed.ts b/src/matchers/element/toBeDisplayed.ts index 16b0352e3..2d7d6eeb4 100644 --- a/src/matchers/element/toBeDisplayed.ts +++ b/src/matchers/element/toBeDisplayed.ts @@ -1,6 +1,6 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' const DEFAULT_OPTIONS_DISPLAYED: ExpectWebdriverIO.ToBeDisplayedOptions = { ...DEFAULT_OPTIONS, @@ -11,7 +11,7 @@ const DEFAULT_OPTIONS_DISPLAYED: ExpectWebdriverIO.ToBeDisplayedOptions = { } export async function toBeDisplayed( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.ToBeDisplayedOptions = DEFAULT_OPTIONS_DISPLAYED, ) { this.expectation = this.expectation || 'displayed' diff --git a/src/matchers/element/toBeDisplayedInViewport.ts b/src/matchers/element/toBeDisplayedInViewport.ts index 32c7ae8df..a94607f15 100644 --- a/src/matchers/element/toBeDisplayedInViewport.ts +++ b/src/matchers/element/toBeDisplayedInViewport.ts @@ -1,9 +1,9 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toBeDisplayedInViewport( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { this.expectation = this.expectation || 'displayed in viewport' diff --git a/src/matchers/element/toBeEnabled.ts b/src/matchers/element/toBeEnabled.ts index 0a3a612a4..cccdf8bc4 100644 --- a/src/matchers/element/toBeEnabled.ts +++ b/src/matchers/element/toBeEnabled.ts @@ -1,9 +1,9 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toBeEnabled( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { this.expectation = this.expectation || 'enabled' diff --git a/src/matchers/element/toBeExisting.ts b/src/matchers/element/toBeExisting.ts index 578e4da68..992de45c0 100644 --- a/src/matchers/element/toBeExisting.ts +++ b/src/matchers/element/toBeExisting.ts @@ -1,23 +1,24 @@ -import { executeCommandBe, aliasFn } from '../../utils.js' +import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementMaybePromise, WdioElementOrArrayMaybePromise } from '../../types.js' export async function toExist( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { this.expectation = this.expectation || 'exist' this.verb = this.verb || '' + this.matcherName = this.matcherName || 'toExist' await options.beforeAssertion?.({ - matcherName: 'toExist', + matcherName: this.matcherName, options, }) const result = await executeCommandBe.call(this, received, el => el?.isExisting(), options) await options.afterAssertion?.({ - matcherName: 'toExist', + matcherName: this.matcherName, options, result }) @@ -26,8 +27,14 @@ export async function toExist( } export function toBeExisting(el: WdioElementMaybePromise, options?: ExpectWebdriverIO.CommandOptions) { - return aliasFn.call(this, toExist, { verb: 'be', expectation: 'existing' }, el, options) + this.verb = 'be' + this.expectation = 'existing' + this.matcherName = 'toBeExisting' + return toExist.call(this, el, options) } export function toBePresent(el: WdioElementMaybePromise, options?: ExpectWebdriverIO.CommandOptions) { - return aliasFn.call(this, toExist, { verb: 'be', expectation: 'present' }, el, options) + this.verb = 'be' + this.expectation = 'present' + this.matcherName = 'toBePresent' + return toExist.call(this, el, options) } diff --git a/src/matchers/element/toBeFocused.ts b/src/matchers/element/toBeFocused.ts index 7fe8d19bb..c81c06fa0 100644 --- a/src/matchers/element/toBeFocused.ts +++ b/src/matchers/element/toBeFocused.ts @@ -1,9 +1,9 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toBeFocused( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { this.expectation = this.expectation || 'focused' diff --git a/src/matchers/element/toBeSelected.ts b/src/matchers/element/toBeSelected.ts index 21376ecec..a671f01f2 100644 --- a/src/matchers/element/toBeSelected.ts +++ b/src/matchers/element/toBeSelected.ts @@ -1,21 +1,24 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toBeSelected( - received: ChainablePromiseElement | WebdriverIO.Element, + received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { + this.verb = this.verb || 'be' this.expectation = this.expectation || 'selected' + this.matcherName = this.matcherName || 'toBeSelected' await options.beforeAssertion?.({ - matcherName: 'toBeSelected', + matcherName: this.matcherName, options, }) const result = await executeCommandBe.call(this, received, el => el?.isSelected(), options) await options.afterAssertion?.({ - matcherName: 'toBeSelected', + matcherName: this.matcherName, options, result }) @@ -23,21 +26,12 @@ export async function toBeSelected( return result } -export async function toBeChecked (el: WebdriverIO.Element, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS) { +export async function toBeChecked (received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS) { + this.verb = 'be' this.expectation = 'checked' + this.matcherName = 'toBeChecked' - await options.beforeAssertion?.({ - matcherName: 'toBeChecked', - options, - }) - - const result = await toBeSelected.call(this, el, options) - - await options.afterAssertion?.({ - matcherName: 'toBeChecked', - options, - result - }) + const result = await toBeSelected.call(this, received, options) return result } diff --git a/src/matchers/element/toHaveAttribute.ts b/src/matchers/element/toHaveAttribute.ts index 9d42293e7..3010f6c7f 100644 --- a/src/matchers/element/toHaveAttribute.ts +++ b/src/matchers/element/toHaveAttribute.ts @@ -1,68 +1,83 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' import { compareText, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' -async function conditionAttr(el: WebdriverIO.Element, attribute: string) { - const attr = await el.getAttribute(attribute) - if (typeof attr !== 'string') { - return { result: false, value: attr } +async function conditionAttributeIsPresent(el: WebdriverIO.Element, attribute: string) { + const attributeValue = await el.getAttribute(attribute) + if (typeof attributeValue !== 'string') { + return { result: false, value: attributeValue } } - return { result: true, value: attr } + return { result: true, value: attributeValue } } -async function conditionAttrAndValue(el: WebdriverIO.Element, attribute: string, value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { - const attr = await el.getAttribute(attribute) - if (typeof attr !== 'string') { - return { result: false, value: attr } +async function conditionAttributeValueMatchWithExpected(el: WebdriverIO.Element, attribute: string, expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { + const attributeValue = await el.getAttribute(attribute) + if (typeof attributeValue !== 'string') { + return { result: false, value: attributeValue } } - return compareText(attr, value, options) + return compareText(attributeValue, expectedValue, options) } -export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attribute: string, value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { - const isNot = this.isNot - const { expectation = 'attribute', verb = 'have' } = this +export async function toHaveAttributeAndValue(received: WdioElementOrArrayMaybePromise, attribute: string, expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { + const { expectation = 'attribute', verb = 'have', isNot } = this - let el = await received?.getElement() + let el let attr - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, conditionAttrAndValue, options, [attribute, value, options]) - el = result.el as WebdriverIO.Element - attr = result.values - - return result.success - }, isNot, options) - - const expected = wrapExpectedWithArray(el, attr, value) + const pass = await waitUntil( + async () => { + const result = await executeCommand(received, + undefined, + (elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, (element, expected) => conditionAttributeValueMatchWithExpected(element, attribute, expected, options)) + ) + + el = result.elementOrArray + attr = result.valueOrArray + + return result + }, + isNot, + { wait: options.wait, interval: options.interval } + ) + + const expected = wrapExpectedWithArray(el, attr, expectedValue) const message = enhanceError(el, expected, attr, this, verb, expectation, attribute, options) return { pass, message: (): string => message - } as ExpectWebdriverIO.AssertionResult + } } -async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: string) { - const isNot = this.isNot - const { expectation = 'attribute', verb = 'have' } = this +async function toHaveAttributeFn(received: WdioElementOrArrayMaybePromise, attribute: string, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS) { + const { expectation = 'attribute', verb = 'have', isNot } = this - let el = await received?.getElement() + let el - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, conditionAttr, {}, [attribute]) - el = result.el as WebdriverIO.Element + const pass = await waitUntil( + async () => { + const result = await executeCommand( + received, + undefined, + (elements) => defaultMultipleElementsIterationStrategy(elements, attribute, (el) => conditionAttributeIsPresent(el, attribute)) + ) - return result.success - }, isNot, {}) + el = result.elementOrArray - const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, {}) + return result + }, + isNot, + { wait: options.wait, interval: options.interval } + ) + + const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, options) return { pass, @@ -70,14 +85,17 @@ async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: s } } +// TODO: one day it would be better to have overload signature one with value and ExpectWebdriverIO.StringOptions, the other with no value and commnad options export async function toHaveAttribute( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, attribute: string, - value?: string | RegExp | WdioAsymmetricMatcher, + value?: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const { matcherName = 'toHaveAttribute' } = this + await options.beforeAssertion?.({ - matcherName: 'toHaveAttribute', + matcherName, expectedValue: [attribute, value], options, }) @@ -89,7 +107,7 @@ export async function toHaveAttribute( : await toHaveAttributeFn.call(this, received, attribute) await options.afterAssertion?.({ - matcherName: 'toHaveAttribute', + matcherName, expectedValue: [attribute, value], options, result diff --git a/src/matchers/element/toHaveChildren.ts b/src/matchers/element/toHaveChildren.ts index f9b035965..79495023e 100644 --- a/src/matchers/element/toHaveChildren.ts +++ b/src/matchers/element/toHaveChildren.ts @@ -1,66 +1,58 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' +import type { NumberMatcher } from '../../util/numberOptionsUtil.js' +import { validateNumberOptionsArray } from '../../util/numberOptionsUtil.js' import { - compareNumbers, enhanceError, - executeCommand, - numberError, waitUntil, wrapExpectedWithArray } from '../../utils.js' -async function condition(el: WebdriverIO.Element, options: ExpectWebdriverIO.NumberOptions) { +async function condition(el: WebdriverIO.Element, value: NumberMatcher) { const children = await el.$$('./*').getElements() - // If no options passed in + children exists - if ( - typeof options.lte !== 'number' && - typeof options.gte !== 'number' && - typeof options.eq !== 'number' - ) { - return { - result: children.length > 0, - value: children?.length - } - } - return { - result: compareNumbers(children?.length, options), + result: value.equals(children?.length), value: children?.length } } export async function toHaveChildren( - received: WdioElementMaybePromise, - expectedValue?: number | ExpectWebdriverIO.NumberOptions, - options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS + received: WdioElementOrArrayMaybePromise, + expectedValue?: MaybeArray, + options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'children', verb = 'have' } = this + const { expectation = 'children', verb = 'have', matcherName = 'toHaveChildren', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toHaveChildren', + matcherName, expectedValue, options, }) - const numberOptions: ExpectWebdriverIO.NumberOptions = typeof expectedValue === 'number' - ? { eq: expectedValue } as ExpectWebdriverIO.NumberOptions - : expectedValue || {} + const { numberMatcher, numberCommandOptions } = validateNumberOptionsArray(expectedValue ?? { gte: 1 }) - let el = await received?.getElement() + let el let children - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, condition, numberOptions, [numberOptions]) - el = result.el as WebdriverIO.Element - children = result.values + const pass = await waitUntil( + async () => { + const result = await executeCommand(received, + undefined, + async (elements) => defaultMultipleElementsIterationStrategy(elements, numberMatcher, condition) + ) + + el = result.elementOrArray + children = result.valueOrArray - return result.success - }, isNot, { ...numberOptions, ...options }) + return result + }, + isNot, + { wait: numberCommandOptions?.wait ?? options.wait, interval: numberCommandOptions?.interval ?? options.interval } + ) - const error = numberError(numberOptions) - const expectedArray = wrapExpectedWithArray(el, children, error) - const message = enhanceError(el, expectedArray, children, this, verb, expectation, '', numberOptions) + const expectedArray = wrapExpectedWithArray(el, children, numberMatcher) + const message = enhanceError(el, expectedArray, children, this, verb, expectation, '', { ...numberCommandOptions, ...options }) const result: ExpectWebdriverIO.AssertionResult = { pass, message: (): string => message diff --git a/src/matchers/element/toHaveClass.ts b/src/matchers/element/toHaveClass.ts deleted file mode 100644 index 864d4ad04..000000000 --- a/src/matchers/element/toHaveClass.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' -import { compareText, compareTextWithArray, enhanceError, executeCommand, isAsymmetricMatcher, waitUntil, wrapExpectedWithArray } from '../../utils.js' -import { toHaveAttributeAndValue } from './toHaveAttribute.js' - -async function condition(el: WebdriverIO.Element, attribute: string, value: string | RegExp | Array | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { - const actualClass = await el.getAttribute(attribute) - if (typeof actualClass !== 'string') { - return { result: false } - } - - /** - * if value is an asymmetric matcher, no need to split class names - * into an array and compare each of them - */ - if (isAsymmetricMatcher(value)) { - return compareText(actualClass, value, options) - } - - const classes = actualClass.split(' ') - const isValueInClasses = classes.some((t) => { - return Array.isArray(value) - ? compareTextWithArray(t, value, options).result - : compareText(t, value, options).result - }) - - return { - value: actualClass, - result: isValueInClasses - } -} - -/** - * @deprecated - */ -export function toHaveClass(...args: unknown[]) { - return toHaveElementClass.call(this || {}, ...args) -} - -export async function toHaveElementClass( - received: WdioElementMaybePromise, - expectedValue: string | RegExp | Array | WdioAsymmetricMatcher, - options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS -) { - const isNot = this.isNot - const { expectation = 'class', verb = 'have' } = this - - await options.beforeAssertion?.({ - matcherName: 'toHaveElementClass', - expectedValue, - options, - }) - - const attribute = 'class' - - let el = await received?.getElement() - let attr - - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, condition, options, [attribute, expectedValue, options]) - el = result.el as WebdriverIO.Element - attr = result.values - - return result.success - }, isNot, options) - - const message = enhanceError(el, wrapExpectedWithArray(el, attr, expectedValue), attr, this, verb, expectation, '', options) - const result: ExpectWebdriverIO.AssertionResult = { - pass, - message: (): string => message - } - - await options.afterAssertion?.({ - matcherName: 'toHaveElementClass', - expectedValue, - options, - result - }) - - return result -} - -/** - * @deprecated - */ -export function toHaveClassContaining(el: WebdriverIO.Element, className: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { - return toHaveAttributeAndValue.call(this, el, 'class', className, { - ...options, - containing: true - }) -} diff --git a/src/matchers/element/toHaveComputedLabel.ts b/src/matchers/element/toHaveComputedLabel.ts index 50e2a9324..08311b52e 100644 --- a/src/matchers/element/toHaveComputedLabel.ts +++ b/src/matchers/element/toHaveComputedLabel.ts @@ -1,17 +1,17 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' import { compareText, compareTextWithArray, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' -async function condition( +async function singleElementCompare( el: WebdriverIO.Element, - label: string | RegExp | WdioAsymmetricMatcher | Array, + label: MaybeArray>, options: ExpectWebdriverIO.HTMLOptions ) { const actualLabel = await el.getComputedLabel() @@ -21,33 +21,44 @@ async function condition( return compareText(actualLabel, label, options) } +async function multipleElementsStrategyCompare( + el: WebdriverIO.Element, + label: string | RegExp | WdioAsymmetricMatcher, + options: ExpectWebdriverIO.HTMLOptions +) { + const actualLabel = await el.getComputedLabel() + return compareText(actualLabel, label, options) +} + export async function toHaveComputedLabel( - received: WdioElementMaybePromise, - expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'computed label', verb = 'have' } = this + const { expectation = 'computed label', verb = 'have', isNot, matcherName = 'toHaveComputedLabel' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveComputedLabel', + matcherName, expectedValue, options, }) - let el = await received?.getElement() + let el let actualLabel const pass = await waitUntil( async () => { - const result = await executeCommand.call(this, el, condition, options, [expectedValue, options]) - el = result.el as WebdriverIO.Element - actualLabel = result.values + const result = await executeCommand(received, + (element) => singleElementCompare(element, expectedValue, options), + (elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, (element, label) => multipleElementsStrategyCompare(element, label, options)) + ) + el = result.elementOrArray + actualLabel = result.valueOrArray - return result.success + return result }, isNot, - options + { wait: options.wait, interval: options.interval } ) const message = enhanceError( @@ -67,7 +78,7 @@ export async function toHaveComputedLabel( } await options.afterAssertion?.({ - matcherName: 'toHaveComputedLabel', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveComputedRole.ts b/src/matchers/element/toHaveComputedRole.ts index 916506b97..89a860d0d 100644 --- a/src/matchers/element/toHaveComputedRole.ts +++ b/src/matchers/element/toHaveComputedRole.ts @@ -1,17 +1,17 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' import { compareText, compareTextWithArray, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' -async function condition( +async function singleElementCompare( el: WebdriverIO.Element, - role: string | RegExp | WdioAsymmetricMatcher | Array, + role: MaybeArray>, options: ExpectWebdriverIO.HTMLOptions ) { const actualRole = await el.getComputedRole() @@ -21,33 +21,47 @@ async function condition( return compareText(actualRole, role, options) } +async function multipleElementsStrategyCompare( + el: WebdriverIO.Element, + role: string | RegExp | WdioAsymmetricMatcher, + options: ExpectWebdriverIO.HTMLOptions +) { + const actualRole = await el.getComputedRole() + return compareText(actualRole, role, options) +} + export async function toHaveComputedRole( - received: WdioElementMaybePromise, - expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'computed role', verb = 'have' } = this + const { expectation = 'computed role', verb = 'have', isNot, matcherName = 'toHaveComputedRole' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveComputedRole', + matcherName, expectedValue, options, }) - let el = await received?.getElement() + let el let actualRole const pass = await waitUntil( async () => { - const result = await executeCommand.call(this, el, condition, options, [expectedValue, options]) - el = result.el as WebdriverIO.Element - actualRole = result.values + const result = await executeCommand(received, + (element) => singleElementCompare(element, expectedValue, options), + async (elements) => defaultMultipleElementsIterationStrategy(elements, + expectedValue, + (element, expected) => multipleElementsStrategyCompare(element, expected, options) + ) + ) + el = result.elementOrArray + actualRole = result.valueOrArray - return result.success + return result }, isNot, - options + { wait: options.wait, interval: options.interval } ) const message = enhanceError( @@ -67,7 +81,7 @@ export async function toHaveComputedRole( } await options.afterAssertion?.({ - matcherName: 'toHaveComputedRole', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveElementClass.ts b/src/matchers/element/toHaveElementClass.ts new file mode 100644 index 000000000..4401ff4ff --- /dev/null +++ b/src/matchers/element/toHaveElementClass.ts @@ -0,0 +1,105 @@ +import { DEFAULT_OPTIONS } from '../../constants.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' +import { compareText, compareTextWithArray, enhanceError, isAsymmetricMatcher, waitUntil, wrapExpectedWithArray } from '../../utils.js' + +async function singleElementStrategyCompare(el: WebdriverIO.Element, attribute: string, value: MaybeArray>, options: ExpectWebdriverIO.StringOptions) { + const actualClass = await el.getAttribute(attribute) + if (typeof actualClass !== 'string') { + return { result: false } + } + + /** + * if value is an asymmetric matcher, no need to split class names + * into an array and compare each of them + */ + if (isAsymmetricMatcher(value)) { + return compareText(actualClass, value, options) + } + + const classes = actualClass.split(' ') + const isValueInClasses = classes.some((t) => { + return Array.isArray(value) + ? compareTextWithArray(t, value, options).result + : compareText(t, value, options).result + }) + + return { + result: isValueInClasses, + value: actualClass + } +} + +async function multipleElementsStrategyCompare(el: WebdriverIO.Element, attribute: string, value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { + const actualClass = await el.getAttribute(attribute) + if (typeof actualClass !== 'string') { + return { result: false } + } + + /** + * if value is an asymmetric matcher, no need to split class names + * into an array and compare each of them + */ + if (isAsymmetricMatcher(value)) { + return compareText(actualClass, value, options) + } + + const classes = actualClass.split(' ') + const isValueInClasses = classes.some((t) => compareText(t, value, options).result) + + return { + result: isValueInClasses, + value: actualClass, + } +} + +export async function toHaveElementClass( + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, + options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS +) { + const { expectation = 'class', verb = 'have', isNot, matcherName = 'toHaveElementClass' } = this + + await options.beforeAssertion?.({ + matcherName, + expectedValue, + options, + }) + + const attribute = 'class' + + let el + let attr + + const pass = await waitUntil( + async () => { + const result = await executeCommand(received, (element) => + singleElementStrategyCompare(element, attribute, expectedValue, options), + (elements) => defaultMultipleElementsIterationStrategy(elements, + expectedValue, + (element, value) => multipleElementsStrategyCompare(element, attribute, value, options)) + ) + el = result.elementOrArray + attr = result.valueOrArray + + return result + }, + isNot, + { wait: options.wait, interval: options.interval } + ) + + const message = enhanceError(el, wrapExpectedWithArray(el, attr, expectedValue), attr, this, verb, expectation, '', options) + const result: ExpectWebdriverIO.AssertionResult = { + pass, + message: (): string => message + } + + await options.afterAssertion?.({ + matcherName, + expectedValue, + options, + result + }) + + return result +} diff --git a/src/matchers/element/toHaveElementProperty.ts b/src/matchers/element/toHaveElementProperty.ts index 2c67f38d2..0fb40c2db 100644 --- a/src/matchers/element/toHaveElementProperty.ts +++ b/src/matchers/element/toHaveElementProperty.ts @@ -1,9 +1,9 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' import { compareText, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' @@ -11,12 +11,12 @@ import { async function condition( el: WebdriverIO.Element, property: string, - value: unknown, + expected: string | number | null | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { const { asString = false } = options - let prop = await el.getProperty(property) + const prop = await el.getProperty(property) // As specified in the w3c spec, cases where property does not exist if (prop === null || prop === undefined) { @@ -24,54 +24,55 @@ async function condition( } // As specified in the w3c spec, cases where property simply exists, missing undefined here? - if (value === null) { + if (expected === null) { return { result: true, value: prop } } - if (!(value instanceof RegExp) && typeof prop !== 'string' && !asString) { - return { result: prop === value, value: prop } + if (!(expected instanceof RegExp) && typeof prop !== 'string' && !asString) { + return { result: prop === expected, value: prop } } - prop = prop.toString() - return compareText(prop as string, value as string | RegExp | WdioAsymmetricMatcher, options) + // To review the cast to be more type safe but for now let's keep the existing behavior to ensure no regression + return compareText(prop.toString(), expected as string | RegExp | WdioAsymmetricMatcher, options) } export async function toHaveElementProperty( - received: WdioElementMaybePromise, + received: WdioElementOrArrayMaybePromise, property: string, - value?: string | RegExp | WdioAsymmetricMatcher | null, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'property', verb = 'have' } = this + const { expectation = 'property', verb = 'have', isNot, matcherName = 'toHaveElementProperty' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveElementProperty', - expectedValue: [property, value], + matcherName, + expectedValue: [property, expectedValue], options, }) - let el = await received?.getElement() + let el let prop: unknown const pass = await waitUntil( async () => { - const result = await executeCommand.call(this, el, condition, options, [property, value]) - el = result.el as WebdriverIO.Element - prop = result.values + const result = await executeCommand(received, undefined, + (elements) => defaultMultipleElementsIterationStrategy( + elements, + expectedValue, + (element, expected) => condition(element, property, expected, options), + { supportArrayForSingleElement: true } + ) + ) + el = result.elementOrArray + prop = result.valueOrArray - return result.success + return result }, isNot, - options + { wait: options.wait, interval: options.interval } ) - let message: string - if (value === undefined) { - message = enhanceError(el, !isNot, pass, this, verb, expectation, property, options) - } else { - const expected = wrapExpectedWithArray(el, prop, value) - message = enhanceError(el, expected, prop, this, verb, expectation, property, options) - } + const expected = wrapExpectedWithArray(el, prop, expectedValue) + const message = enhanceError(el, expected, prop, this, verb, expectation, property, options) const result: ExpectWebdriverIO.AssertionResult = { pass, @@ -79,8 +80,8 @@ export async function toHaveElementProperty( } await options.afterAssertion?.({ - matcherName: 'toHaveElementProperty', - expectedValue: [property, value], + matcherName, + expectedValue: [property, expectedValue], options, result }) diff --git a/src/matchers/element/toHaveHTML.ts b/src/matchers/element/toHaveHTML.ts index 1f7b976e2..44ed4b4fb 100644 --- a/src/matchers/element/toHaveHTML.ts +++ b/src/matchers/element/toHaveHTML.ts @@ -1,15 +1,16 @@ -import type { ChainablePromiseArray, ChainablePromiseElement } from 'webdriverio' import { DEFAULT_OPTIONS } from '../../constants.js' import { - compareText, compareTextWithArray, + compareText, + compareTextWithArray, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' -async function condition(el: WebdriverIO.Element, html: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.HTMLOptions) { +async function singleElementCompare(el: WebdriverIO.Element, html: MaybeArray>, options: ExpectWebdriverIO.HTMLOptions) { const actualHTML = await el.getHTML(options) if (Array.isArray(html)) { return compareTextWithArray(actualHTML, html, options) @@ -17,36 +18,44 @@ async function condition(el: WebdriverIO.Element, html: string | RegExp | WdioAs return compareText(actualHTML, html, options) } +async function multipleElementsStrategyCompare(el: WebdriverIO.Element, html: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.HTMLOptions) { + const actualHTML = await el.getHTML(options) + return compareText(actualHTML, html, options) +} + export async function toHaveHTML( - received: ChainablePromiseArray | ChainablePromiseElement, - expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.HTMLOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'HTML', verb = 'have' } = this + const { expectation = 'HTML', verb = 'have', isNot, matcherName = 'toHaveHTML' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveHTML', + matcherName, expectedValue, options, }) - let el = 'getElement' in received - ? await received?.getElement() - : 'getElements' in received - ? await received?.getElements() - : received + let elements let actualHTML - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, condition, options, [expectedValue, options]) - el = result.el as WebdriverIO.Element - actualHTML = result.values + const pass = await waitUntil( + async () => { + const result = await executeCommand(received, + (element) => singleElementCompare(element, expectedValue, options), + (elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, (el, html) => multipleElementsStrategyCompare(el, html, options)) + ) + elements = result.elementOrArray + actualHTML = result.valueOrArray - return result.success - }, isNot, options) + return result + }, + isNot, + { wait: options.wait, interval: options.interval } + ) - const message = enhanceError(el, wrapExpectedWithArray(el, actualHTML, expectedValue), actualHTML, this, verb, expectation, '', options) + const expectedValues = wrapExpectedWithArray(elements, actualHTML, expectedValue) + const message = enhanceError(elements, expectedValues, actualHTML, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { pass, @@ -54,7 +63,7 @@ export async function toHaveHTML( } await options.afterAssertion?.({ - matcherName: 'toHaveHTML', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveHeight.ts b/src/matchers/element/toHaveHeight.ts index 0905d2183..69611086d 100644 --- a/src/matchers/element/toHaveHeight.ts +++ b/src/matchers/element/toHaveHeight.ts @@ -1,66 +1,60 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise, WdioElements } from '../../types.js' +import { wrapExpectedWithArray } from '../../util/elementsUtil.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' +import type { NumberMatcher } from '../../util/numberOptionsUtil.js' +import { validateNumberOptionsArray } from '../../util/numberOptionsUtil.js' import { - compareNumbers, enhanceError, - executeCommand, - numberError, waitUntil, } from '../../utils.js' -async function condition(el: WebdriverIO.Element, height: number, options: ExpectWebdriverIO.NumberOptions) { +async function condition(el: WebdriverIO.Element, expected: NumberMatcher) { const actualHeight = await el.getSize('height') return { - result: compareNumbers(actualHeight, options), + result: expected.equals(actualHeight), value: actualHeight } } export async function toHaveHeight( - received: WdioElementMaybePromise, - expectedValue: number | ExpectWebdriverIO.NumberOptions, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'height', verb = 'have' } = this + const { expectation = 'height', verb = 'have', matcherName = 'toHaveHeight', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toHaveHeight', + matcherName, expectedValue, options, }) - // type check - let numberOptions: ExpectWebdriverIO.NumberOptions - if (typeof expectedValue === 'number') { - numberOptions = { eq: expectedValue } as ExpectWebdriverIO.NumberOptions - } else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) { - throw new Error('Invalid params passed to toHaveHeight.') - } else { - numberOptions = expectedValue - } + const { numberMatcher, numberCommandOptions } = validateNumberOptionsArray(expectedValue) - let el = await received?.getElement() - let actualHeight + let elements: WebdriverIO.Element | WdioElements | undefined + let actualHeight: string | number | (string | number | undefined)[] | undefined const pass = await waitUntil( async () => { - const result = await executeCommand.call(this, el, condition, numberOptions, [expectedValue, numberOptions]) + const result = await executeCommand(received, + undefined, + (elements) => defaultMultipleElementsIterationStrategy(elements, numberMatcher, condition)) - el = result.el as WebdriverIO.Element - actualHeight = result.values + elements = result.elementOrArray + actualHeight = result.valueOrArray - return result.success + return result }, isNot, - { ...numberOptions, ...options } + { wait: numberCommandOptions?.wait ?? options.wait, interval: numberCommandOptions?.interval ?? options.interval } ) - const error = numberError(numberOptions) + const expectedValues = wrapExpectedWithArray(elements, actualHeight, numberMatcher) const message = enhanceError( - el, - error, + elements, + expectedValues, actualHeight, this, verb, @@ -75,7 +69,7 @@ export async function toHaveHeight( } await options.afterAssertion?.({ - matcherName: 'toHaveHeight', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveHref.ts b/src/matchers/element/toHaveHref.ts index 05920c262..b15439b98 100644 --- a/src/matchers/element/toHaveHref.ts +++ b/src/matchers/element/toHaveHref.ts @@ -1,10 +1,10 @@ import { toHaveAttributeAndValue } from './toHaveAttribute.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toHaveHref( - el: WdioElementMaybePromise, - expectedValue: string, + el: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { await options.beforeAssertion?.({ diff --git a/src/matchers/element/toHaveId.ts b/src/matchers/element/toHaveId.ts index 6bc3d4ae2..91f2a5ed4 100644 --- a/src/matchers/element/toHaveId.ts +++ b/src/matchers/element/toHaveId.ts @@ -1,10 +1,10 @@ import { toHaveAttributeAndValue } from './toHaveAttribute.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export async function toHaveId( - el: WdioElementMaybePromise, - expectedValue: string | RegExp | WdioAsymmetricMatcher, + el: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { await options.beforeAssertion?.({ diff --git a/src/matchers/element/toHaveSize.ts b/src/matchers/element/toHaveSize.ts index a2d2cc80a..3b20d9270 100644 --- a/src/matchers/element/toHaveSize.ts +++ b/src/matchers/element/toHaveSize.ts @@ -1,47 +1,52 @@ +import type { RectReturn } from '@wdio/protocols' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { executeCommand, defaultMultipleElementsIterationStrategy } from '../../util/executeCommand.js' import { compareObject, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray, } from '../../utils.js' -async function condition(el: WebdriverIO.Element, size: { height: number; width: number }) { +export type Size = Pick + +async function condition(el: WebdriverIO.Element, size: Size): Promise<{ result: boolean, value: Size }> { const actualSize = await el.getSize() return compareObject(actualSize, size) } export async function toHaveSize( - received: WdioElementMaybePromise, - expectedValue: { height: number; width: number }, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'size', verb = 'have' } = this + const { expectation = 'size', verb = 'have', isNot, matcherName = 'toHaveSize' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveSize', + matcherName, expectedValue, options, }) - let el = await received?.getElement() + let el let actualSize const pass = await waitUntil( async () => { - const result = await executeCommand.call(this, el, condition, options, [expectedValue, options]) + const result = await executeCommand(received, + undefined, + async (elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, condition) + ) - el = result.el as WebdriverIO.Element - actualSize = result.values + el = result.elementOrArray + actualSize = result.valueOrArray - return result.success + return result }, isNot, - options + { wait: options.wait, interval: options.interval } ) const message = enhanceError( @@ -61,7 +66,7 @@ export async function toHaveSize( } await options.afterAssertion?.({ - matcherName: 'toHaveSize', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveStyle.ts b/src/matchers/element/toHaveStyle.ts index 5d61baf41..f0e032ea3 100644 --- a/src/matchers/element/toHaveStyle.ts +++ b/src/matchers/element/toHaveStyle.ts @@ -1,9 +1,9 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' import { compareStyle, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' @@ -13,29 +13,35 @@ async function condition(el: WebdriverIO.Element, style: { [key: string]: string } export async function toHaveStyle( - received: WdioElementMaybePromise, - expectedValue: { [key: string]: string; }, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray<{ [key: string]: string; }>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'style', verb = 'have' } = this + const { expectation = 'style', verb = 'have', isNot, matcherName = 'toHaveStyle' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveStyle', + matcherName, expectedValue, options, }) - let el = await received?.getElement() + let el let actualStyle - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, condition, options, [expectedValue, options]) - el = result.el as WebdriverIO.Element - actualStyle = result.values - - return result.success - }, isNot, options) + const pass = await waitUntil( + async () => { + const result = await executeCommand(received, + undefined, + (elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, (element, expected) => condition(element, expected, options)) + ) + el = result.elementOrArray + actualStyle = result.valueOrArray + + return result + }, + isNot, + { wait: options.wait, interval: options.interval } + ) const message = enhanceError(el, wrapExpectedWithArray(el, actualStyle, expectedValue), actualStyle, this, verb, expectation, '', options) @@ -45,7 +51,7 @@ export async function toHaveStyle( } await options.afterAssertion?.({ - matcherName: 'toHaveStyle', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveText.ts b/src/matchers/element/toHaveText.ts index 82d4450c7..feccb7100 100644 --- a/src/matchers/element/toHaveText.ts +++ b/src/matchers/element/toHaveText.ts @@ -1,79 +1,84 @@ -import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' import { DEFAULT_OPTIONS } from '../../constants.js' import { compareText, compareTextWithArray, enhanceError, - executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js' +import { executeCommand } from '../../util/executeCommand.js' +import type { MaybeArray, WdioElementOrArrayMaybePromise } from '../../types.js' +import { isElementArrayLike, map } from '../../util/elementsUtil.js' -async function condition(el: WebdriverIO.Element | WebdriverIO.ElementArray, text: string | RegExp | Array | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions) { - const actualTextArray: string[] = [] - const resultArray: boolean[] = [] - let checkAllValuesMatchCondition: boolean +async function singleElementCompare(el: WebdriverIO.Element, text: MaybeArray>, options: ExpectWebdriverIO.StringOptions) { + const actualText = await el.getText() + const result = Array.isArray(text) ? + compareTextWithArray(actualText, text, options).result + : compareText(actualText, text, options).result - if (Array.isArray(el)){ - for (const element of el){ - const actualText = await element.getText() - actualTextArray.push(actualText) - const result = Array.isArray(text) - ? compareTextWithArray(actualText, text, options).result - : compareText(actualText, text, options).result - resultArray.push(result) - } - checkAllValuesMatchCondition = resultArray.every(Boolean) - } else { - const actualText = await (el as WebdriverIO.Element).getText() - actualTextArray.push(actualText) - checkAllValuesMatchCondition = Array.isArray(text) - ? compareTextWithArray(actualText, text, options).result - : compareText(actualText, text, options).result + return { + value: actualText, + result } +} + +// Same as singleElementCompare (e.g `$()`) but with a deprecation notice for `compareTextWithArray` removal to have the same behavior across all matchers with `$$()` +async function multipleElementsStrategyCompare(el: WebdriverIO.Element, text: MaybeArray>, options: ExpectWebdriverIO.StringOptions) { + const actualText = await el.getText() + const checkAllValuesMatchCondition = Array.isArray(text) ? + // @deprecated: using compareTextWithArray for $$() is deprecated and will be removed in future versions since it does not do a strict comparison per element. + compareTextWithArray(actualText, text, options).result + : compareText(actualText, text, options).result return { - value: actualTextArray.length === 1 ? actualTextArray[0] : actualTextArray, + value: actualText, result: checkAllValuesMatchCondition } } export async function toHaveText( - received: ChainablePromiseElement | ChainablePromiseArray, - expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'text', verb = 'have' } = this + const { expectation = 'text', verb = 'have', isNot, matcherName = 'toHaveText' } = this await options.beforeAssertion?.({ - matcherName: 'toHaveText', + matcherName, expectedValue, options, }) - let el = 'getElement' in received - ? await received?.getElement() - : 'getElements' in received - ? await received?.getElements() - : received + let elementOrArray let actualText - const pass = await waitUntil(async () => { - const result = await executeCommand.call(this, el, condition, options, [expectedValue, options]) - el = result.el - actualText = result.values + const pass = await waitUntil( + async () => { + const commandResult = await executeCommand(received, + undefined, + async (elements) => { + if (isElementArrayLike(elements)) { + return map(elements, async (element) => multipleElementsStrategyCompare(element, expectedValue, options)) + } + return [await singleElementCompare(elements, expectedValue, options)] + } + ) + elementOrArray = commandResult.elementOrArray + actualText = commandResult.valueOrArray - return result.success - }, isNot, options) + return commandResult + }, + isNot, + { wait: options.wait, interval: options.interval } + ) - const message = enhanceError(el, wrapExpectedWithArray(el, actualText, expectedValue), actualText, this, verb, expectation, '', options) + const message = enhanceError(elementOrArray, wrapExpectedWithArray(elementOrArray, actualText, expectedValue), actualText, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { pass, message: (): string => message } await options.afterAssertion?.({ - matcherName: 'toHaveText', + matcherName, expectedValue, options, result diff --git a/src/matchers/element/toHaveValue.ts b/src/matchers/element/toHaveValue.ts index 6c032b2c8..62e13c9df 100644 --- a/src/matchers/element/toHaveValue.ts +++ b/src/matchers/element/toHaveValue.ts @@ -1,11 +1,12 @@ import { toHaveElementProperty } from './toHaveElementProperty.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise } from '../../types.js' export function toHaveValue( - el: WdioElementMaybePromise, - value: string | RegExp | WdioAsymmetricMatcher, + el: WdioElementOrArrayMaybePromise, + value: MaybeArray>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + this.matcherName = 'toHaveValue' return toHaveElementProperty.call(this, el, 'value', value, options) } diff --git a/src/matchers/element/toHaveWidth.ts b/src/matchers/element/toHaveWidth.ts index 6f706ffcb..08b1e650d 100644 --- a/src/matchers/element/toHaveWidth.ts +++ b/src/matchers/element/toHaveWidth.ts @@ -1,66 +1,60 @@ import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementMaybePromise } from '../../types.js' +import type { WdioElementOrArrayMaybePromise, WdioElements } from '../../types.js' +import { wrapExpectedWithArray } from '../../util/elementsUtil.js' +import { defaultMultipleElementsIterationStrategy, executeCommand } from '../../util/executeCommand.js' +import type { NumberMatcher } from '../../util/numberOptionsUtil.js' +import { validateNumberOptionsArray } from '../../util/numberOptionsUtil.js' import { - compareNumbers, enhanceError, - executeCommand, - numberError, waitUntil, } from '../../utils.js' -async function condition(el: WebdriverIO.Element, width: number, options: ExpectWebdriverIO.NumberOptions) { +async function condition(el: WebdriverIO.Element, expected: NumberMatcher) { const actualWidth = await el.getSize('width') return { - result: compareNumbers(actualWidth, options), + result: expected.equals(actualWidth), value: actualWidth } } export async function toHaveWidth( - received: WdioElementMaybePromise, - expectedValue: number | ExpectWebdriverIO.NumberOptions, + received: WdioElementOrArrayMaybePromise, + expectedValue: MaybeArray, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'width', verb = 'have' } = this + const { expectation = 'width', verb = 'have', matcherName = 'toHaveWidth', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toHaveWidth', + matcherName, expectedValue, options, }) - // type check - let numberOptions: ExpectWebdriverIO.NumberOptions - if (typeof expectedValue === 'number') { - numberOptions = { eq: expectedValue } as ExpectWebdriverIO.NumberOptions - } else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) { - throw new Error('Invalid params passed to toHaveHeight.') - } else { - numberOptions = expectedValue - } + const { numberMatcher, numberCommandOptions } = validateNumberOptionsArray(expectedValue) - let el = await received?.getElement() - let actualWidth + let elements: WebdriverIO.Element | WdioElements | undefined + let actualWidth: string | number | (string | number | undefined)[] | undefined const pass = await waitUntil( async () => { - const result = await executeCommand.call(this, el, condition, numberOptions, [expectedValue, numberOptions]) + const result = await executeCommand(received, + undefined, + (elements) => defaultMultipleElementsIterationStrategy(elements, numberMatcher, condition)) - el = result.el as WebdriverIO.Element - actualWidth = result.values + elements = result.elementOrArray + actualWidth = result.valueOrArray - return result.success + return result }, isNot, - { ...numberOptions, ...options } + { wait: numberCommandOptions?.wait ?? options.wait, interval: numberCommandOptions?.interval ?? options.interval } ) - const error = numberError(numberOptions) + const expectedValues = wrapExpectedWithArray(elements, actualWidth, numberMatcher) const message = enhanceError( - el, - error, + elements, + expectedValues, actualWidth, this, verb, @@ -75,7 +69,7 @@ export async function toHaveWidth( } await options.afterAssertion?.({ - matcherName: 'toHaveWidth', + matcherName, expectedValue, options, result diff --git a/src/matchers/elements/toBeElementsArrayOfSize.ts b/src/matchers/elements/toBeElementsArrayOfSize.ts index 39cd07ccd..7172f3e8c 100644 --- a/src/matchers/elements/toBeElementsArrayOfSize.ts +++ b/src/matchers/elements/toBeElementsArrayOfSize.ts @@ -1,53 +1,60 @@ -import { waitUntil, enhanceError, compareNumbers, numberError } from '../../utils.js' +import { waitUntil, enhanceError, } from '../../utils.js' import { refetchElements } from '../../util/refetchElements.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElements, WdioElementsMaybePromise } from '../../types.js' +import type { WdioElementsMaybePromise } from '../../types.js' +import { validateNumberOptions } from '../../util/numberOptionsUtil.js' +import { awaitElementArray } from '../../util/elementsUtil.js' export async function toBeElementsArrayOfSize( received: WdioElementsMaybePromise, expectedValue: number | ExpectWebdriverIO.NumberOptions, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot - const { expectation = 'elements array of size', verb = 'be' } = this + const { expectation = 'elements array of size', verb = 'be', matcherName = 'toBeElementsArrayOfSize', isNot } = this await options.beforeAssertion?.({ - matcherName: 'toBeElementsArrayOfSize', + matcherName, expectedValue, options, }) - let numberOptions: ExpectWebdriverIO.NumberOptions - if (typeof expectedValue === 'number') { - numberOptions = { eq: expectedValue } satisfies ExpectWebdriverIO.NumberOptions - } else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) { - throw new Error('Invalid params passed to toBeElementsArrayOfSize.') - } else { - numberOptions = expectedValue - } + const { numberMatcher, numberCommandOptions } = validateNumberOptions(expectedValue) - let elements = await received as WdioElements - const originalLength = elements.length - const pass = await waitUntil(async () => { - /** - * check numbers first before refetching elements - */ - const isPassing = compareNumbers(elements.length, numberOptions) - if (isPassing) { - return isPassing - } - elements = await refetchElements(elements, numberOptions.wait, true) - return false - }, isNot, { ...numberOptions, ...options }) + // eslint-disable-next-line prefer-const + let { elements, other } = await awaitElementArray(received) + + const wait = numberCommandOptions?.wait ?? options.wait ?? DEFAULT_OPTIONS.wait + const originalLength = elements ? elements.length : undefined + + const pass = await waitUntil( + async () => { + if (!elements) { + return false + } + + // Verify is size match first before refetching elements + const isPassing = numberMatcher.equals(elements.length) + if (isPassing) { + return isPassing + } + + // TODO should we do this on other matchers?? + elements = await refetchElements(elements, wait, true) + return false + }, + isNot, + { wait, interval: numberCommandOptions?.interval ?? options.interval } + ) - if (Array.isArray(received) && pass) { + // TODO By using `(await received).push(elements[index])` we could update Promises of arrays, should we support that? + if (Array.isArray(received) && pass && originalLength !== undefined && elements) { for (let index = originalLength; index < elements.length; index++) { received.push(elements[index]) } } - const error = numberError(numberOptions) - const message = enhanceError(elements, error, originalLength, this, verb, expectation, '', numberOptions) + const actual = originalLength + const message = enhanceError(elements ?? other, numberMatcher, actual, this, verb, expectation, '', { ...numberCommandOptions, ...options }) const result: ExpectWebdriverIO.AssertionResult = { pass, @@ -55,7 +62,7 @@ export async function toBeElementsArrayOfSize( } await options.afterAssertion?.({ - matcherName: 'toBeElementsArrayOfSize', + matcherName, expectedValue, options, result diff --git a/src/matchers/mock/toBeRequestedTimes.ts b/src/matchers/mock/toBeRequestedTimes.ts index 0a50b3af7..612db94d5 100644 --- a/src/matchers/mock/toBeRequestedTimes.ts +++ b/src/matchers/mock/toBeRequestedTimes.ts @@ -1,34 +1,36 @@ -import { waitUntil, enhanceError, compareNumbers } from '../../utils.js' -import { numberError } from '../../util/formatMessage.js' +import { waitUntil, enhanceError } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' +import { isNumber, validateNumberOptions } from '../../util/numberOptionsUtil.js' export async function toBeRequestedTimes( received: WebdriverIO.Mock, expectedValue: number | ExpectWebdriverIO.NumberOptions = {}, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot || false - const { expectation = `called${typeof expectedValue === 'number' ? ' ' + expectedValue : '' } time${expectedValue !== 1 ? 's' : ''}`, verb = 'be' } = this + const { + expectation = `called${isNumber(expectedValue) ? ' ' + expectedValue : '' } time${expectedValue !== 1 ? 's' : ''}`, verb = 'be', + isNot, matcherName = 'toBeRequestedTimes' + } = this await options.beforeAssertion?.({ - matcherName: 'toBeRequestedTimes', + matcherName, expectedValue, options, }) - // type check - const numberOptions: ExpectWebdriverIO.NumberOptions = typeof expectedValue === 'number' - ? { eq: expectedValue } as ExpectWebdriverIO.NumberOptions - : expectedValue || {} + const { numberMatcher, numberCommandOptions } = validateNumberOptions(expectedValue) let actual - const pass = await waitUntil(async () => { - actual = received.calls.length - return compareNumbers(actual, numberOptions) - }, isNot, { ...numberOptions, ...options }) + const pass = await waitUntil( + async () => { + actual = received.calls.length + return numberMatcher.equals(actual) + }, + isNot, + { wait: numberCommandOptions?.wait ?? options.wait, interval: numberCommandOptions?.interval ?? options.interval } + ) - const error = numberError(numberOptions) - const message = enhanceError('mock', error, actual, this, verb, expectation, '', numberOptions) + const message = enhanceError('mock', numberMatcher, actual, this, verb, expectation, '', { ...numberCommandOptions, ...options }) const result: ExpectWebdriverIO.AssertionResult = { pass, @@ -36,7 +38,7 @@ export async function toBeRequestedTimes( } await options.afterAssertion?.({ - matcherName: 'toBeRequestedTimes', + matcherName, expectedValue, options, result diff --git a/src/matchers/mock/toBeRequestedWith.ts b/src/matchers/mock/toBeRequestedWith.ts index 338fb83df..effdb7e47 100644 --- a/src/matchers/mock/toBeRequestedWith.ts +++ b/src/matchers/mock/toBeRequestedWith.ts @@ -24,11 +24,10 @@ export async function toBeRequestedWith( expectedValue: ExpectWebdriverIO.RequestedWith = {}, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - const isNot = this.isNot || false - const { expectation = 'called with', verb = 'be' } = this + const { expectation = 'called with', verb = 'be', isNot, matcherName = 'toBeRequestedWith' } = this await options.beforeAssertion?.({ - matcherName: 'toBeRequestedWith', + matcherName, expectedValue, options, }) @@ -75,7 +74,7 @@ export async function toBeRequestedWith( } await options.afterAssertion?.({ - matcherName: 'toBeRequestedWith', + matcherName, expectedValue, options, result diff --git a/src/types.ts b/src/types.ts index 7fb7d2420..b6367a78e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,8 +11,14 @@ export type WdioElementsMaybePromise = WdioElements | ChainablePromiseArray +export type WdioElementOrArrayMaybePromise = + WdioElementMaybePromise | + WdioElementsMaybePromise + export type RawMatcherFn = { (this: Context, actual: unknown, ...expected: unknown[]): ExpectationResult; } export type WdioMatchersObject = Map + +export type MaybeArray = T | T[] diff --git a/src/util/elementsUtil.ts b/src/util/elementsUtil.ts index 0c4b0f8f6..bb215c146 100644 --- a/src/util/elementsUtil.ts +++ b/src/util/elementsUtil.ts @@ -1,3 +1,5 @@ +import type { WdioElementOrArrayMaybePromise, WdioElements, WdioElementsMaybePromise } from '../types' + /** * if el is an array of elements and actual value is an array * wrap expected value with array @@ -5,13 +7,108 @@ * @param actual actual result or results array * @param expected expected result */ -export const wrapExpectedWithArray = (el: WebdriverIO.Element | WebdriverIO.ElementArray, actual: unknown, expected: unknown) => { - if (Array.isArray(el) && el.length > 1 && Array.isArray(actual)) { - expected = [expected] +export const wrapExpectedWithArray = (el: WebdriverIO.Element | WdioElements | undefined, actual: unknown, expected: unknown) => { + if (Array.isArray(el) && Array.isArray(actual) && !Array.isArray(expected)) { + expected = Array(actual.length).fill(expected) } return expected } -export const isElementArray = (obj: unknown): obj is WebdriverIO.ElementArray => { - return obj !== null && typeof obj === 'object' && 'selector' in obj && 'foundWith' in obj && 'parent' in obj +export const isStrictlyElementArray = (obj: unknown): obj is WebdriverIO.ElementArray => { + return !!obj && typeof obj === 'object' + && Array.isArray(obj) + && 'selector' in obj + && 'foundWith' in obj // Element does not have foundWith property + && 'parent' in obj // commun with Element + && 'getElements' in obj // specific to ElementArray +} + +export const isElement = (obj: unknown): obj is WebdriverIO.Element => { + // Note elementId is only for found element + return !!obj && typeof obj === 'object' + && !Array.isArray(obj) + && 'selector' in obj + && 'parent' in obj + && 'getElement' in obj // specific to Element +} + +export const isElementArrayLike = (obj: unknown): obj is WebdriverIO.ElementArray | WebdriverIO.Element[] => { + return !!obj && isStrictlyElementArray(obj) || (Array.isArray(obj) && obj.every(isElement)) +} + +export const isElementOrArrayLike = (obj: unknown): obj is WebdriverIO.ElementArray | WebdriverIO.Element[] | WebdriverIO.Element => { + return !!obj && isElement(obj) || isElementArrayLike(obj) +} + +export const isElementOrNotEmptyElementArray = (obj: unknown): obj is WebdriverIO.Element | WdioElements => { + return !!obj && isElement(obj) || (isElementArrayLike(obj) && obj.length > 0) +} +/** + * Universaly await element(s) since depending on the type received, it can become complex. + * + * Using `$()` or `$$()` return a promise as `ChainablePromiseElement/Array` that needs to be awaited and even if chainable.getElement()/getElements() can be done statically, at runtime `'getElement/getElements` in chainable` is false. + * Using `await $()` still return a `ChainablePromiseElement` but underneath it's a `WebdriverIO.Element/ElementArray` and thus `'getElement/getElements' in element` is true and can be checked and done. + * With `$$().filter()`, it returns a `Promise` that also needs to be awaited. + * When passing directly a `WebdriverIO.Element` or `WebdriverIO.ElementArray`, no need to await anything and getElement or getElements can be used on it and runtime also works too. + * + * @param received + * @returns + */ +export const awaitElementOrArray = async(received: WdioElementOrArrayMaybePromise | undefined): Promise<{ elements?: WdioElements, element?: WebdriverIO.Element, other?: unknown }> => { + let awaitedElements = received + // For non-awaited `$()` or `$$()`, so ChainablePromiseElement | ChainablePromiseArray. + // At some extend it also process non-awaited `$().getElement()`, `$$().getElements()` or `$$().filter()`, but typings does not allow it + if (awaitedElements instanceof Promise) { + awaitedElements = await awaitedElements + } + + if (!isElementOrArrayLike(awaitedElements)) { + return { other: awaitedElements } + } + + // for `await $()` or `WebdriverIO.Element` + if ('getElement' in awaitedElements) { + return { element: await awaitedElements.getElement() } + } + // for `await $$()` or `WebdriverIO.ElementArray` but not `WebdriverIO.Element[]` + if ('getElements' in awaitedElements) { + return { elements: await awaitedElements.getElements() } + } + + // for `WebdriverIO.Element[]` + return { elements: awaitedElements } +} + +export const awaitElementArray = async(received: WdioElementsMaybePromise | undefined): Promise<{ elements?: WdioElements, other?: unknown }> => { + let awaitedElements = received + // For non-awaited `$$()`, so ChainablePromiseElement | ChainablePromiseArray. + // At some extend it also process non-awaited `$$().getElements()` or `$$().filter()` (e.g. Promise), but typings does not allow it + if (awaitedElements instanceof Promise) { + awaitedElements = await awaitedElements + } + + if (!isElementArrayLike(awaitedElements)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { other: awaitedElements as any } + } + + // for `await $$()` or `WebdriverIO.ElementArray` but not `WebdriverIO.Element[]` + if ('getElements' in awaitedElements) { + return { elements: await awaitedElements.getElements() } + } + + // for `WebdriverIO.Element[]` or any other object + return { elements: awaitedElements } } + +export const map = ( + elements: WebdriverIO.ElementArray | WebdriverIO.Element[], + command: (element: WebdriverIO.Element, index: number) => Promise +): Promise => { + const results: Promise[] = [] + elements.forEach((element, index) => { + results.push(command(element, index)) + }) + return Promise.all(results) +} + diff --git a/src/util/executeCommand.ts b/src/util/executeCommand.ts index f12171606..7ed29eecc 100644 --- a/src/util/executeCommand.ts +++ b/src/util/executeCommand.ts @@ -1,23 +1,115 @@ +import type { WdioElementOrArrayMaybePromise, WdioElements } from '../types' +import { awaitElementOrArray, isElementArrayLike, map } from './elementsUtil' + /** - * Ensures that the specified condition passes for a given element or every element in an array of elements - * @param el The element or array of elements - * @param condition - The condition function to be executed on the element(s). - * @param options - Optional configuration options - * @param params - Additional parameters + * Ensures a condition passes for one or more elements. + * + * Resolves the elements and applies the appropriate strategy: + * - Single element: Uses `singleElementCompareStrategy` (fallback to `multipleElementsCompareStrategy`). + * - Multiple elements: Uses `multipleElementsCompareStrategy` (fallback to `singleElementCompareStrategy` for each). + * + * Returns failure if elements are invalid or empty. + * + * @param elements The element or array of elements. + * @param singleElementCompareStrategy Strategy for a single element. + * @param multipleElementsCompareStrategy Strategy for the element(s). + * @param options Optional configuration options. */ -export async function executeCommand( - el: WebdriverIO.Element | WebdriverIO.ElementArray, - condition: (el: WebdriverIO.Element | WebdriverIO.ElementArray, ...params: unknown[]) => Promise<{ - result: boolean; - value?: unknown; - }>, - options: ExpectWebdriverIO.DefaultOptions = {}, - params: unknown[] = [] -): Promise<{ el: WebdriverIO.Element | WebdriverIO.ElementArray; success: boolean; values: unknown; }> { - const result = await condition(el, ...params, options) +export async function executeCommand( + nonAwaitedElements: WdioElementOrArrayMaybePromise | undefined, + singleElementCompareStrategy?: (awaitedElement: WebdriverIO.Element) => Promise< + { result: boolean; value?: T } + >, + mutipleElementsCompareStrategy?: (awaitedElements: WebdriverIO.Element | WdioElements) => Promise< + { result: boolean; value?: T }[] + > +): Promise<{ elementOrArray: WdioElements | WebdriverIO.Element | undefined; success: boolean; valueOrArray: T | undefined | Array, results: boolean[] }> { + const { elements, element, other } = await awaitElementOrArray(nonAwaitedElements) + if (!elements && !element) { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- one day move up the unknown type + elementOrArray: other as any, + success: false, + valueOrArray: undefined, + results: [] + } + } else if (elements?.length === 0) { + return { + elementOrArray: elements, + success: false, + valueOrArray: undefined, + results: [] + } + } + if (!singleElementCompareStrategy && !mutipleElementsCompareStrategy) { throw new Error('No condition or customMultipleElementCompareStrategy provided to executeCommand') } + + const elementOrArray = element ? element : elements ? elements : undefined + + /* v8 ignore next -- @preserve -- should be unreachable due to checks above */ + if (!elementOrArray) { + throw new Error('No elements to process in executeCommand') + } + + let results + if (singleElementCompareStrategy && element) { + results = [await singleElementCompareStrategy(element)] + } else if (mutipleElementsCompareStrategy) { + results = await mutipleElementsCompareStrategy(elementOrArray) + } else if (singleElementCompareStrategy && elements) { + results = await map(elements, (el: WebdriverIO.Element) => singleElementCompareStrategy(el)) + } else { + /* v8 ignore next -- @preserve -- To please tsc but never reached due to checks above */ + throw new Error('Unable to process executeCommand with the provided parameters') + } + return { - el, - success: result.result === true, - values: result.value + elementOrArray: elementOrArray, + success: results.length > 0 && results.every((res) => res.result === true), + results: results.map(({ result }) => (result)), + valueOrArray: element && results.length === 1 ? results[0].value : results.map(({ value }) => value), + } +} + +/** + * Default strategy for iterating over multiple elements. + * + * - If `elements` is an array: + * - Compares each element against the corresponding value in `expectedValues` (if it's an array). + * - Compares each element against `expectedValues` (if it's a single value). + * - If `elements` is a single element: + * - Compares against `expectedValues` (must be a single value). + * + * Fails if array lengths mismatch or if a single element is compared against an array. + * + * @param elements The element or array of elements. + * @param expectedValues The expected value or array of expected values. + * @param condition The condition to execute on each element. + * @param options Optional configuration options. + */ +export async function defaultMultipleElementsIterationStrategy( + elements: WebdriverIO.Element | WdioElements, + expectedValues: MaybeArray, + condition: (awaitedElement: WebdriverIO.Element, expectedValue: Expected) => Promise< + { result: boolean; value?: Value } + >, + { supportArrayForSingleElement = false } = {} +): Promise<{ result: boolean; value?: Value | string }[]> { + if (isElementArrayLike(elements)) { + if (Array.isArray(expectedValues)) { + if (elements.length !== expectedValues.length) { + return [{ result: false, value: `Received array length ${elements.length}, expected ${expectedValues.length}` }] + } + return await map(elements, (el: WebdriverIO.Element, index: number) => condition(el, expectedValues[index])) + } + return await map(elements, (el: WebdriverIO.Element) => condition(el, expectedValues)) + + } else if ( Array.isArray(expectedValues)) { + if (!supportArrayForSingleElement) { + return [{ result: false, value: 'Expected value cannot be an array' }] + } + + // Case where a single element's value can be an array compared to an expected value array and not multiple expected values + return [await condition(elements, expectedValues as Expected)] } + return [await condition(elements, expectedValues)] } diff --git a/src/util/formatMessage.ts b/src/util/formatMessage.ts index d5bf2b665..b6db081a4 100644 --- a/src/util/formatMessage.ts +++ b/src/util/formatMessage.ts @@ -1,12 +1,11 @@ -import { printDiffOrStringify, printExpected, printReceived } from 'jest-matcher-utils' +import { printDiffOrStringify, printExpected, printReceived, RECEIVED_COLOR, EXPECTED_COLOR, INVERTED_COLOR, stringify } from 'jest-matcher-utils' import { equals } from '../jasmineUtils.js' import type { WdioElements } from '../types.js' -import { isElementArray } from './elementsUtil.js' +import { isElementArrayLike, isElementOrNotEmptyElementArray, isStrictlyElementArray } from './elementsUtil.js' +import { numberMatcherTester } from './numberOptionsUtil.js' +import { toJsonString } from './stringUtil.js' -const EXPECTED_LABEL = 'Expected' -const RECEIVED_LABEL = 'Received' -const NOT_SUFFIX = ' [not]' -const NOT_EXPECTED_LABEL = EXPECTED_LABEL + NOT_SUFFIX +const CUSTOM_EQUALITY_TESTER = [numberMatcherTester] export const getSelector = (el: WebdriverIO.Element | WebdriverIO.ElementArray) => { let result = typeof el.selector === 'string' ? el.selector : '' @@ -17,15 +16,21 @@ export const getSelector = (el: WebdriverIO.Element | WebdriverIO.ElementArray) return result } -export const getSelectors = (el: WebdriverIO.Element | WdioElements) => { +export const getSelectors = (el: WebdriverIO.Element | WdioElements): string => { const selectors = [] let parent: WebdriverIO.ElementArray['parent'] | undefined - if (isElementArray(el)) { + if (isStrictlyElementArray(el)) { selectors.push(`${(el).foundWith}(\`${getSelector(el)}\`)`) parent = el.parent } else if (!Array.isArray(el)) { parent = el + } else if (Array.isArray(el)) { + for (const element of el) { + selectors.push(getSelectors(element)) + } + // When not having more context about the common parent, return joined selectors + return selectors.join(', ') } while (parent && 'selector' in parent) { @@ -39,24 +44,24 @@ export const getSelectors = (el: WebdriverIO.Element | WdioElements) => { return selectors.reverse().join('.') } -export const not = (isNot: boolean): string => { - return `${isNot ? 'not ' : ''}` -} +const not = (isNot: boolean): string => `${isNot ? 'not ' : ''}` export const enhanceError = ( - subject: string | WebdriverIO.Element | WdioElements, + subject: string | WebdriverIO.Element | WdioElements | unknown, expected: unknown, actual: unknown, - context: { isNot: boolean }, + context: { isNot: boolean, useNotInLabel?: boolean }, verb: string, expectation: string, - arg2 = '', { + expectedValueArgument2 = '', { message = '', containing = false - }): string => { - const { isNot = false } = context + } = {}): string => { + const { isNot = false, useNotInLabel = true } = context + + const isElementsSubject = isElementArrayLike(subject) - subject = typeof subject === 'string' ? subject : getSelectors(subject) + subject = subject = isElementOrNotEmptyElementArray(subject) ? getSelectors(subject) : toJsonString(subject) let contain = '' if (containing) { @@ -67,55 +72,111 @@ export const enhanceError = ( verb += ' ' } - let diffString = isNot && equals(actual, expected) - ? `${EXPECTED_LABEL}: ${printExpected(expected)}\n${RECEIVED_LABEL}: ${printReceived(actual)}` - : printDiffOrStringify(expected, actual, EXPECTED_LABEL, RECEIVED_LABEL, true) + const isNotInLabel = useNotInLabel && isNot + const label = { + expected: isNotInLabel ? 'Expected [not]' : 'Expected', + received: isNotInLabel ? 'Received ' : 'Received' + } - if (isNot) { - diffString = diffString - .replace(EXPECTED_LABEL, NOT_EXPECTED_LABEL) - .replace(RECEIVED_LABEL, RECEIVED_LABEL + ' '.repeat(NOT_SUFFIX.length)) + let diffString = '' + + // Special formatting for .not with arrays to highlight what matched + if (isNotInLabel && isElementsSubject && Array.isArray(expected) && Array.isArray(actual) && expected.length === actual.length) { + // With multiple elements + `.not`, since `printDiffOrStringify` shows only diff and we need to highlight what matched, we do custom formatting + // Using FORCE_COLOR=1 npx vitest + console.log() can show colors in the test output console + const { expectedFormatted, receivedFormatted } = printArrayWithMatchingItemInRed(expected, actual) + diffString = `\ +${label.expected}: ${expectedFormatted} +${label.received}: ${receivedFormatted}` + } else if (equals(actual, expected, CUSTOM_EQUALITY_TESTER)) { + // Using `printDiffOrStringify()` with equals values output `Received: serializes to the same string`, so we need to tweak. + diffString = + `\ +${label.expected}: ${printExpected(expected)} +${label.received}: ${printReceived(actual)}` + } else { + diffString = printDiffOrStringify(expected, actual, label.expected, label.received, true) } if (message) { message += '\n' } - if (arg2) { - arg2 = ` ${arg2}` + if (expectedValueArgument2) { + expectedValueArgument2 = ` ${expectedValueArgument2}` } - const msg = `${message}Expect ${subject} ${not(isNot)}to ${verb}${expectation}${arg2}${contain}\n\n${diffString}` - return msg -} + const msg = `\ +${message}Expect ${subject} ${not(isNot)}to ${verb}${expectation}${expectedValueArgument2}${contain} -export const enhanceErrorBe = ( - subject: string | WebdriverIO.Element | WebdriverIO.ElementArray, - pass: boolean, - context: { isNot: boolean }, - verb: string, - expectation: string, - options: ExpectWebdriverIO.CommandOptions -) => { - return enhanceError(subject, not(context.isNot) + expectation, not(!pass) + expectation, context, verb, expectation, '', options) +${diffString}` + + return msg } -export const numberError = (options: ExpectWebdriverIO.NumberOptions = {}): string | number => { - if (typeof options.eq === 'number') { - return options.eq +// Inspired by Jest's printReceivedArrayContainExpectedItem +// Highlights matching elements when using .not to show what shouldn't have matched +const printArrayWithMatchingItemInRed = ( + expectedArray: unknown[], + actualArray: unknown[], +): { expectedFormatted: string, receivedFormatted: string } => { + // Find matching indices + const matchingIndices: number[] = [] + for (let i = 0; i < expectedArray.length; i++) { + if (equals(expectedArray[i], actualArray[i], CUSTOM_EQUALITY_TESTER)) { + matchingIndices.push(i) + } } - if (options.gte && options.lte) { - return `>= ${options.gte} && <= ${options.lte}` - } + // For .not, matching items are the problem - highlight them in red on both sides + const expectedFormatted = `[${expectedArray + .map((item, i) => { + const stringified = stringify(item) + // Problematic items (matched) in red, others in green + return matchingIndices.includes(i) + ? RECEIVED_COLOR(INVERTED_COLOR(stringified)) + : EXPECTED_COLOR(stringified) + }) + .join(', ')}]` + + const receivedFormatted = `[${actualArray + .map((item, i) => { + const stringified = stringify(item) + // Problematic items (matched) in red, others in green + return matchingIndices.includes(i) + ? RECEIVED_COLOR(INVERTED_COLOR(stringified)) + : EXPECTED_COLOR(stringified) + }) + .join(', ')}]` + + return { expectedFormatted, receivedFormatted } +} - if (options.gte) { - return `>= ${options.gte}` +export const enhanceErrorBe = ( + subject: WebdriverIO.Element | WdioElements | unknown, + results: boolean[], + context: { isNot: boolean, verb: string, expectation: string }, + options: ExpectWebdriverIO.CommandOptions +) => { + const { isNot, verb, expectation } = context + let expected + let actual + + const expectedValue = `${not(isNot)}${expectation}` + const actualValue = `${not(!isNot)}${expectation}` + + if (isElementArrayLike(subject)) { + expected = subject.length === 0? 'at least one result' : subject.map(() => expectedValue) + actual = results.map(result => isSuccess(isNot, result) ? `${not(isNot)}${expectation}` : `${not(!isNot)}${expectation}`) + } else { + expected = expectedValue + actual = actualValue } - if (options.lte) { - return ` <= ${options.lte}` - } + return enhanceError(subject, expected, actual, { ...context, useNotInLabel: false }, verb, expectation, '', options) +} - return 'no params' +const isSuccess = (isNot: boolean, result: boolean): boolean => { + return isNot ? !result : result } + diff --git a/src/util/numberOptionsUtil.ts b/src/util/numberOptionsUtil.ts new file mode 100644 index 000000000..2965f3eac --- /dev/null +++ b/src/util/numberOptionsUtil.ts @@ -0,0 +1,105 @@ + +export const isNumber = (value: unknown): value is number => typeof value === 'number' +const isDefined = (value: unknown): boolean => value !== undefined && value !== null + +export function validateNumberOptions(expectedValue: number | ExpectWebdriverIO.NumberOptions): { numberMatcher: NumberMatcher, numberCommandOptions?: ExpectWebdriverIO.CommandOptions } { + let numberOptions: ExpectWebdriverIO.NumberOptions + if (isNumber(expectedValue)) { + numberOptions = { eq: expectedValue } satisfies ExpectWebdriverIO.NumberOptions + return { numberMatcher: new NumberMatcher(numberOptions) } + } else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) { + throw new Error(`Invalid NumberOptions. Received: ${JSON.stringify(expectedValue)}`) + } else { + const { eq, gte, lte, ...commandOptions } = expectedValue + return { numberMatcher: new NumberMatcher( { eq, gte, lte }), numberCommandOptions: commandOptions } + } + +} + +export function validateNumberOptionsArray(expectedValues: MaybeArray) { + if (Array.isArray(expectedValues)) { + // TODO: deprecated NumberOptions as options in favor of ExpectedType and realy only on commandOptions param + overloaded function + const allNumbers = expectedValues.map((value) => validateNumberOptions(value)) + // Options in numberOptions are not supported when passing an array of expected values + return { numberMatcher: allNumbers.map( ({ numberMatcher }) => numberMatcher), numberCommandOptions: undefined } + } + return validateNumberOptions(expectedValues) +} + +/** + * Using a class to univerally handle number matching and stringification the same way everywhere and with Global Apis like equal() toString() and toJSON() + */ +export class NumberMatcher { + constructor(private options: ExpectWebdriverIO.NumberOptions = {}) {} + + equals(actual: number | undefined): boolean { + if ( actual === undefined ) { + return false + } + + if (isNumber(this.options.eq)) { + return actual === this.options.eq + } + + if (isNumber(this.options.gte) && isNumber(this.options.lte)) { + return actual >= this.options.gte && actual <= this.options.lte + } + + if (isNumber(this.options.gte)) { + return actual >= this.options.gte + } + + if (isNumber(this.options.lte)) { + return actual <= this.options.lte + } + + return false + } + + toString(): string { + if (isNumber(this.options.eq)) { + return String(this.options.eq) + } + + if (isDefined(this.options.gte) && isDefined(this.options.lte)) { + return `>= ${this.options.gte} && <= ${this.options.lte}` + } + + if (isDefined(this.options.gte)) { + return `>= ${this.options.gte}` + } + + if (isDefined(this.options.lte)) { + return `<= ${this.options.lte}` + } + + return 'Incorrect number options provided' + } + + toJSON(): string | number { + // Return the actual number for exact equality, so it serializes as 0 not "0" + if (isNumber(this.options.eq)) { + return this.options.eq + } + return this.toString() + } +} + +/** + * Custom tester for number matchers to be used by the equal of expect during failure message generation + */ +export const numberMatcherTester = (a: unknown, b: unknown): boolean | undefined => { + const isNumberMatcherA = a instanceof NumberMatcher + const isNumberMatcherB = b instanceof NumberMatcher + + if (isNumberMatcherA && isNumber(b)) { + return a.equals(b) + } + + if (isNumberMatcherB && isNumber(a)) { + return b.equals(a) + } + + // Return undefined to let other testers handle it + return undefined +} diff --git a/src/util/refetchElements.ts b/src/util/refetchElements.ts index 403c7539c..c2094619e 100644 --- a/src/util/refetchElements.ts +++ b/src/util/refetchElements.ts @@ -1,6 +1,6 @@ import { DEFAULT_OPTIONS } from '../constants.js' import type { WdioElements } from '../types.js' -import { isElementArray } from './elementsUtil.js' +import { isStrictlyElementArray } from './elementsUtil.js' /** * Refetch elements array or return when elements is not of type WebdriverIO.ElementArray @@ -11,10 +11,10 @@ export const refetchElements = async ( wait = DEFAULT_OPTIONS.wait, full = false ): Promise => { - if (elements && wait > 0 && (elements.length === 0 || full) && isElementArray(elements) && elements.parent && elements.foundWith && elements.foundWith in elements.parent) { + if (elements && wait > 0 && (elements.length === 0 || full) && isStrictlyElementArray(elements)) { const browser = elements.parent const $$ = browser[elements.foundWith as keyof typeof browser] as Function - elements = await $$.call(browser, elements.selector, ...elements.props) + return await $$.call(browser, elements.selector, ...elements.props) } return elements } diff --git a/src/util/stringUtil.ts b/src/util/stringUtil.ts new file mode 100644 index 000000000..b1031286e --- /dev/null +++ b/src/util/stringUtil.ts @@ -0,0 +1,12 @@ +export const isString = (value: unknown): value is string => typeof value === 'string' + +export const toJsonString = (value: unknown): string => { + if (isString(value)) { + return value + } + try { + return JSON.stringify(value) + } catch { + return String(value) + } +} diff --git a/src/util/waitUntil.ts b/src/util/waitUntil.ts new file mode 100644 index 000000000..1a70fdbd2 --- /dev/null +++ b/src/util/waitUntil.ts @@ -0,0 +1,67 @@ +import { DEFAULT_OPTIONS } from '../constants.js' + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +export type ConditionResult = { success: boolean; results: boolean[] } + +/** + * Wait for condition result to succeed (true) or to fail (false) when using `.not`. + * For a success result with isNot the condition must return false since Jest's expect inverts the result later. + * + * @param condition function + * @param isNot when using `expect().not`, see https://jestjs.io/docs/expect#thisisnot + * @param options wait, interval + */ +export const waitUntil = async ( + condition: () => Promise, + isNot: boolean | undefined, + { wait = DEFAULT_OPTIONS.wait, interval = DEFAULT_OPTIONS.interval } +): Promise => { + isNot = isNot ?? false + + const start = Date.now() + let error: unknown + let result: boolean | ConditionResult = false + let pass: boolean = false + + do { + try { + result = await condition() + error = undefined + if (isBoolean(result)) { + pass = result + } else { + // Waiting for all to be true. Or all to be false when using `.not` (pass=false since inverted later by Jest) + pass = isNot ? !isAllFalse(result.results) : isAllTrue(result.results) + } + + // Waiting for the condition to succeed or to fail when using `.not` + if (isNot ? !pass : pass) { + break + } + + } catch (err) { + error = err + } + + // No need to sleep again if time is already over + if (canWait(start, wait)) { + await sleep(interval) + } + } while (canWait(start, wait)) + + if (error) { throw error } + + // When no results were found, ensure the waitUntil return failure even with `.not` + if (!isBoolean(result) && result.results.length === 0) { + // To fails with .not, we need pass=true, so it's inverted later by Jest's expect framework + return isNot + } + + return pass +} +const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean' + +const isAllTrue = (results: boolean[]): boolean => results.every((res) => res === true) +const isAllFalse = (results: boolean[]): boolean => results.every((res) => res === false) +const canWait = (start: number, wait: number): boolean => (Date.now() - start) < wait diff --git a/src/utils.ts b/src/utils.ts index 3987241ab..bcf13cb12 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,13 +3,12 @@ import type { ParsedCSSValue } from 'webdriverio' import { expect } from 'expect' -import { DEFAULT_OPTIONS } from './constants.js' -import type { WdioElementMaybePromise } from './types.js' +import type { WdioElementOrArrayMaybePromise, WdioElements } from './types.js' import { wrapExpectedWithArray } from './util/elementsUtil.js' import { executeCommand } from './util/executeCommand.js' -import { enhanceError, enhanceErrorBe, numberError } from './util/formatMessage.js' - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +import { enhanceError, enhanceErrorBe } from './util/formatMessage.js' +import { waitUntil } from './util/waitUntil.js' +import { DEFAULT_OPTIONS } from './constants.js' const asymmetricMatcher = typeof Symbol === 'function' && Symbol.for @@ -31,83 +30,33 @@ function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetri return isAsymmetricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString()) } -/** - * wait for expectation to succeed - * @param condition function - * @param isNot https://jestjs.io/docs/expect#thisisnot - * @param options wait, interval, etc - */ -const waitUntil = async ( - condition: () => Promise, - isNot = false, - { wait = DEFAULT_OPTIONS.wait, interval = DEFAULT_OPTIONS.interval } = {} -): Promise => { - // single attempt - if (wait === 0) { - return await condition() - } - - let error: Error | undefined - - // wait for condition to be truthy - try { - const start = Date.now() - while (true) { - if (Date.now() - start > wait) { - throw new Error('timeout') - } - - try { - const result = isNot !== (await condition()) - error = undefined - if (result) { - break - } - await sleep(interval) - } catch (err) { - error = err - await sleep(interval) - } - } - - if (error) { - throw error - } - - return !isNot - } catch { - if (error) { - throw error - } - - return isNot - } -} - async function executeCommandBe( - received: WdioElementMaybePromise, + nonAwaitedElements: WdioElementOrArrayMaybePromise | undefined, command: (el: WebdriverIO.Element) => Promise, options: ExpectWebdriverIO.CommandOptions ): ExpectWebdriverIO.AsyncAssertionResult { - const { isNot, expectation, verb = 'be' } = this + const { wait = DEFAULT_OPTIONS.wait, interval = DEFAULT_OPTIONS.interval } = options - let el = await received?.getElement() + let awaitedElements: WdioElements | WebdriverIO.Element | undefined + let allResults: boolean[] = [] const pass = await waitUntil( async () => { - const result = await executeCommand.call( - this, - el, - async (element ) => ({ result: await command(element as WebdriverIO.Element) }), - options + const { elementOrArray, success, results } = await executeCommand( + nonAwaitedElements, + async (element) => ({ result: await command(element) }) ) - el = result.el as WebdriverIO.Element - return result.success + + awaitedElements = elementOrArray + allResults = results + + return { success, results } }, - isNot, - options + this.isNot, + { wait, interval } ) - const message = enhanceErrorBe(el, pass, this, verb, expectation, options) + const { verb = 'be' } = this + const message = enhanceErrorBe(awaitedElements, allResults, { ...this, verb }, options) return { pass, @@ -224,12 +173,21 @@ export const compareText = ( } } +/** + * Compare actual text with array of expected texts in a non-strict way + * if the actual text matches with any of the expected texts, it returns true + * + * @param actual + * @param expectedArray + * @param param2 + * @returns + */ export const compareTextWithArray = ( actual: string, expectedArray: Array>, { ignoreCase = false, - trim = false, + trim = true, containing = false, atStart = false, atEnd = false, @@ -292,7 +250,7 @@ export const compareTextWithArray = ( } } -export const compareObject = (actual: object | number, expected: string | number | object) => { +export const compareObject = (actual: T, expected: unknown) => { if (typeof actual !== 'object' || Array.isArray(actual)) { return { value: actual, @@ -365,25 +323,9 @@ export const compareStyle = async ( } } -function aliasFn( - fn: (...args: unknown[]) => void, - { - verb, - expectation, - }: { - verb?: string - expectation?: string - } = {}, - ...args: unknown[] -): unknown { - this.verb = verb - this.expectation = expectation - return fn.apply(this, args) -} - export { - aliasFn, compareNumbers, enhanceError, executeCommand, - executeCommandBe, numberError, waitUntil, wrapExpectedWithArray + compareNumbers, enhanceError, + executeCommandBe, waitUntil, wrapExpectedWithArray } function replaceActual( diff --git a/test/__fixtures__/utils.ts b/test/__fixtures__/utils.ts index 95bca72a4..c6b254b4f 100644 --- a/test/__fixtures__/utils.ts +++ b/test/__fixtures__/utils.ts @@ -1,30 +1,11 @@ -export function matcherNameToString(matcherName: string) { - return matcherName.replace(/([A-Z])/g, ' $1').toLowerCase() -} - -export function matcherLastWordName(matcherName: string) { +export function matcherNameLastWords(matcherName: string) { return matcherName.replace(/^toHave/, '').replace(/^toBe/, '') .replace(/([A-Z])/g, ' $1').trim().toLowerCase() } -export function getExpectMessage(msg: string) { - return msg.split('\n')[0] -} - -export function getExpected(msg: string) { - return getReceivedOrExpected(msg, 'Expected') -} - -export function getReceived(msg: string) { - return getReceivedOrExpected(msg, 'Received') -} - -function getReceivedOrExpected(msg: string, type: string) { - return msg.split('\n').find((line, idx) => idx > 1 && line.startsWith(type)) -} - -export function removeColors(msg: string) { - // eslint-disable-next-line no-control-regex - const s = msg.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') - return s +export function lastMatcherWords(matcherName: string) { + return matcherName.replace(/^(toBe|toHave|to)/, '') + .replace(/([A-Z])/g, ' $1') + .trim() + .toLowerCase() } diff --git a/test/__mocks__/@wdio/globals.ts b/test/__mocks__/@wdio/globals.ts index d9f4a3b4a..72dba88b9 100644 --- a/test/__mocks__/@wdio/globals.ts +++ b/test/__mocks__/@wdio/globals.ts @@ -3,10 +3,35 @@ * This file exist for better typed mock implementation, so that we can follow wdio/globals API updates more easily. */ import { vi } from 'vitest' -import type { ChainablePromiseArray, ChainablePromiseElement } from 'webdriverio' +import type { ChainablePromiseArray, ChainablePromiseElement, ParsedCSSValue } from 'webdriverio' +import type { Size } from '../../../src/matchers.js' -import type { RectReturn } from '@wdio/protocols' -export type Size = Pick +vi.mock('@wdio/globals') +vi.mock('../../../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../../../src/constants.js')).DEFAULT_OPTIONS, + // speed up tests by lowering default wait timeout + wait : 1 + } +})) + +vi.mock('../../../src/util/waitUntil.js', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + waitUntil: vi.spyOn(actual, 'waitUntil') + } +}) +vi.mock('../../../src/utils.js', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + executeCommandBe: vi.spyOn(actual, 'executeCommandBe'), + } +}) const getElementMethods = () => ({ isDisplayed: vi.spyOn({ isDisplayed: async () => true }, 'isDisplayed'), @@ -20,55 +45,183 @@ const getElementMethods = () => ({ getHTML: vi.spyOn({ getHTML: async () => { return '' } }, 'getHTML'), getComputedLabel: vi.spyOn({ getComputedLabel: async () => 'Computed Label' }, 'getComputedLabel'), getComputedRole: vi.spyOn({ getComputedRole: async () => 'Computed Role' }, 'getComputedRole'), - getSize: vi.spyOn({ getSize: async (prop?: 'width' | 'height') => { - if (prop === 'width') { return 100 } - if (prop === 'height') { return 50 } - return { width: 100, height: 50 } satisfies Size - } }, 'getSize') as unknown as WebdriverIO.Element['getSize'], getAttribute: vi.spyOn({ getAttribute: async (_attr: string) => 'some attribute' }, 'getAttribute'), + getCSSProperty: vi.spyOn({ getCSSProperty: async (_prop: string, _pseudo?: string) => + ({ value: 'colorValue', parsed: {} } satisfies ParsedCSSValue) }, 'getCSSProperty'), + // We cannot type-safely mock overloaded functions, so we force the below implementation + getSize: vi.fn().mockImplementation(async function(this: WebdriverIO.Element, prop?: 'width' | 'height'): Promise { + if (prop === 'width') { return Promise.resolve(100) } + if (prop === 'height') { return Promise.resolve(50) } + return Promise.resolve({ width: 100, height: 50 }) + }), + $, + $$, } satisfies Partial) -function $(_selector: string) { - const element = { +export const elementFactory = (_selector: string, index?: number, parent: WebdriverIO.Browser | WebdriverIO.Element = browser): WebdriverIO.Element => { + const partialElement = { selector: _selector, ...getElementMethods(), + index, $, - $$ - } satisfies Partial as unknown as WebdriverIO.Element - element.getElement = async () => Promise.resolve(element) - return element as unknown as ChainablePromiseElement + $$, + parent + } satisfies Partial + + const element = partialElement as unknown as WebdriverIO.Element + element.getElement = vi.fn().mockResolvedValue(element) + + // Note: an element found has element.elementId while a not found has element.error + element.elementId = `${_selector}${index ? '-' + index : ''}` + + return element } -function $$(selector: string) { - const length = (this)?._length || 2 - const elements = Array(length).fill(null).map((_, index) => { - const element = { - selector, - index, - ...getElementMethods(), - $, - $$ - } satisfies Partial as unknown as WebdriverIO.Element - element.getElement = async () => Promise.resolve(element) - return element - }) satisfies WebdriverIO.Element[] as unknown as WebdriverIO.ElementArray - - elements.foundWith = '$$' - elements.props = [] - elements.props.length = length - elements.selector = selector - elements.getElements = async () => elements - elements.length = length - return elements as unknown as ChainablePromiseArray +export const notFoundElementFactory = (_selector: string, index?: number, parent: WebdriverIO.Browser | WebdriverIO.Element = browser): WebdriverIO.Element => { + const partialElement = { + selector: _selector, + index, + $, + $$, + isExisting: vi.fn().mockResolvedValue(false), + parent + } satisfies Partial + + const element = partialElement as unknown as WebdriverIO.Element + + // Note: an element found has element.elementId while a not found has element.error + const elementId = `${_selector}${index ? '-' + index : ''}` + const error = (functionName: string) => new Error(`Can't call ${functionName} on element with selector ${elementId} because element wasn't found`) + + // Mimic element not found by throwing error on any method call besides isExisting + const notFoundElement = new Proxy(element, { + get(target, prop) { + if (prop in element) { + const value = element[prop as keyof WebdriverIO.Element] + return value + } + if (['then', 'catch', 'toStringTag'].includes(prop as string) || typeof prop === 'symbol') { + const value = Reflect.get(target, prop) + return typeof value === 'function' ? value.bind(target) : value + } + element.error = error(prop as string) + return () => { throw element.error } + } + }) + + element.getElement = vi.fn().mockResolvedValue(notFoundElement) + + return notFoundElement } -export const browser = { - $, - $$, - execute: vi.fn(), - setPermissions: vi.spyOn({ setPermissions: async () => {} }, 'setPermissions'), - getUrl: vi.spyOn({ getUrl: async () => ' Valid text ' }, 'getUrl'), - getTitle: vi.spyOn({ getTitle: async () => 'Example Domain' }, 'getTitle'), - call(fn: Function) { return fn() }, -} satisfies Partial as unknown as WebdriverIO.Browser +const $ = vi.fn((_selector: string) => { + const element = elementFactory(_selector) + + // Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior + const chainablePromiseElement = Promise.resolve(element) as unknown as ChainablePromiseElement + + // Ensure `'getElement' in chainableElement` is false while allowing to use `await chainableElement.getElement()` + const runtimeChainableElement = new Proxy(chainablePromiseElement, { + get(target, prop) { + if (prop in element) { + return element[prop as keyof WebdriverIO.Element] + } + const value = Reflect.get(target, prop) + return typeof value === 'function' ? value.bind(target) : value + } + }) + return runtimeChainableElement +}) + +const $$ = vi.fn((selector: string) => { + return chainableElementArrayFactory(selector, 2) +}) + +export function elementArrayFactory(selector: string, length?: number): WebdriverIO.ElementArray { + const elements: WebdriverIO.Element[] = Array(length).fill(null).map((_, index) => elementFactory(selector, index)) + + const elementArray = elements as unknown as WebdriverIO.ElementArray + + elementArray.foundWith = '$$' + elementArray.props = [] + elementArray.selector = selector + elementArray.getElements = vi.fn().mockResolvedValue(elementArray) + elementArray.filter = async (fn: (element: WebdriverIO.Element, index: number, array: T[]) => boolean | Promise) => { + const results = await Promise.all(elements.map((el, i) => fn(el, i, elements as unknown as T[]))) + return Array.prototype.filter.call(elements, (_, i) => results[i]) + } + elementArray.parent = browser + + // Ensure critical array methods are properly accessible for type compatibility with MultiRemoteElement[] + // Note: WebdriverIO.ElementArray has async versions of some methods (map, forEach, some, every, find, findIndex) + // so we only bind the synchronous array methods that don't conflict + // const arrayPrototype = Array.prototype + // elementArray.slice = arrayPrototype.slice.bind(elementArray) + // elementArray.concat = arrayPrototype.concat.bind(elementArray) + // elementArray.join = arrayPrototype.join.bind(elementArray) + // elementArray.indexOf = arrayPrototype.indexOf.bind(elementArray) + // elementArray.lastIndexOf = arrayPrototype.lastIndexOf.bind(elementArray) + // elementArray.reduce = arrayPrototype.reduce.bind(elementArray) + // elementArray.reduceRight = arrayPrototype.reduceRight.bind(elementArray) + // elementArray.reverse = arrayPrototype.reverse.bind(elementArray) + // elementArray.sort = arrayPrototype.sort.bind(elementArray) + // elementArray.splice = arrayPrototype.splice.bind(elementArray) + // elementArray.push = arrayPrototype.push.bind(elementArray) + // elementArray.pop = arrayPrototype.pop.bind(elementArray) + // elementArray.shift = arrayPrototype.shift.bind(elementArray) + // elementArray.unshift = arrayPrototype.unshift.bind(elementArray) + // elementArray.fill = arrayPrototype.fill.bind(elementArray) + // elementArray.copyWithin = arrayPrototype.copyWithin.bind(elementArray) + // Note: keys, values, entries, and Symbol.iterator inherit from the array prototype + // map, forEach, some, every, find, findIndex are async in WebdriverIO.ElementArray + + return elementArray +} + +export function chainableElementArrayFactory(selector: string, length: number) { + const elementArray = elementArrayFactory(selector, length) + + // Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior + const chainablePromiseArray = Promise.resolve(elementArray) as unknown as ChainablePromiseArray + + // Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()` + const runtimeChainablePromiseArray = new Proxy(chainablePromiseArray, { + get(target, prop) { + if (typeof prop === 'string' && /^\d+$/.test(prop)) { + const index = parseInt(prop, 10) + if (index >= length) { + const error = new Error(`Index out of bounds! $$(${selector}) returned only ${length} elements.`) + return new Proxy(Promise.resolve(), { + get(target, prop) { + if (prop === 'then') { + return (resolve: any, reject: any) => reject(error) + } + return () => Promise.reject(error) + } + }) + } + } + if (elementArray && prop in elementArray) { + return elementArray[prop as keyof WebdriverIO.ElementArray] + } + const value = Reflect.get(target, prop) + return typeof value === 'function' ? value.bind(target) : value + } + }) + + return runtimeChainablePromiseArray +} + +export const browserFactory = (): WebdriverIO.Browser => { + return { + $, + $$, + execute: vi.fn(), + setPermissions: vi.spyOn({ setPermissions: async () => {} }, 'setPermissions'), + getUrl: vi.spyOn({ getUrl: async () => ' Valid text ' }, 'getUrl'), + getTitle: vi.spyOn({ getTitle: async () => 'Example Domain' }, 'getTitle'), + call(fn: Function) { return fn() }, + } satisfies Partial as unknown as WebdriverIO.Browser +} +export const browser = browserFactory() diff --git a/test/globals_mock.test.ts b/test/globals_mock.test.ts new file mode 100644 index 000000000..60cf5e5aa --- /dev/null +++ b/test/globals_mock.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect, vi } from 'vitest' +import { $, $$ } from '@wdio/globals' +import { notFoundElementFactory } from './__mocks__/@wdio/globals.js' + +vi.mock('@wdio/globals') + +describe('globals mock', () => { + describe($, () => { + it('should return a ChainablePromiseElement', async () => { + const el = $('foo') + + // It behaves like a promise + expect(el).toHaveProperty('then') + // @ts-expect-error + expect(typeof el.then).toBe('function') + }) + + it('should resolve to an element', async () => { + const el = await $('foo') + + expect(el.selector).toBe('foo') + // The resolved element should not be the proxy, but the underlying mock + expect(el.getElement).toBeDefined() + }) + + it('should allow calling getElement on the chainable promise', async () => { + const chainable = $('foo') + + // 'getElement' should not be present in the chainable object if checked via `in` + // based on user request logs: 'getElements' in elements false + expect('getElement' in chainable).toBe(false) + + // But it should be callable + const el = chainable.getElement() + expect(el).toBeInstanceOf(Promise) + + const awaitedEl = await el + expect(awaitedEl.selector).toBe('foo') + expect(awaitedEl.getElement).toBeDefined() + }) + + it('should allow calling methods like isEnabled on the chainable promise', async () => { + const check = $('foo').isEnabled() + expect(check).toBeInstanceOf(Promise) + + const result = await check + expect(result).toBe(true) + }) + + it('should allow chaining simple methods', async () => { + const text = await $('foo').getText() + + expect(text).toBe(' Valid Text ') + }) + }) + + describe($$, () => { + it('should return a ChainablePromiseArray', async () => { + const els = $$('foo') + expect(els).toHaveProperty('then') + // @ts-expect-error + expect(typeof els.then).toBe('function') + }) + + it('should resolve to an element array', async () => { + const els = await $$('foo') + expect(Array.isArray(els)).toBe(true) + expect(els).toHaveLength(2) // Default length in mock + expect(els.selector).toBe('foo') + }) + + it('should allow calling getElements on the chainable promise', async () => { + const chainable = $$('foo') + // 'getElements' should not be present in the chainable object if checked via `in` + expect('getElements' in chainable).toBe(false) + + // But it should be callable + const els = await chainable.getElements() + expect(els).toHaveLength(2) // Default length + }) + + it('should allow iterating if awaited', async () => { + const els = await $$('foo') + // map is available on the resolved array + const selectors = els.map(el => el.selector) + expect(selectors).toEqual(['foo', 'foo']) + }) + + it('should returns ElementArray on getElements', async () => { + const els = await $$('foo') + + expect(await els.getElements()).toEqual(els) + }) + + it('should return a promise-like object when accessing index out of bounds', () => { + const el = $$('foo')[3] + // It shouldn't throw synchronously + expect(el).toBeDefined() + expect(el).toBeInstanceOf(Promise) + expect(typeof (el as any).then).toBe('function') + + // Methods should return a Promise + const p1 = el.getElement() + expect(p1).toBeInstanceOf(Promise) + // catch unhandled rejection to avoid warnings + p1.catch(() => {}) + + const p2 = el.getText() + expect(p2).toBeInstanceOf(Promise) + // catch unhandled rejection to avoid warnings + p2.catch(() => {}) + }) + + it('should throw "Index out of bounds" when awaiting index out of bounds', async () => { + await expect(async () => await $$('foo')[3]).rejects.toThrow('Index out of bounds! $$(foo) returned only 2 elements.') + await expect(async () => await $$('foo')[3].getElement()).rejects.toThrow('Index out of bounds! $$(foo) returned only 2 elements.') + await expect(async () => await $$('foo')[3].getText()).rejects.toThrow('Index out of bounds! $$(foo) returned only 2 elements.') + }) + }) + + describe('notFoundElementFactory', () => { + it('should return false for isExisting', async () => { + const el = notFoundElementFactory('not-found') + expect(await el.isExisting()).toBe(false) + }) + + it('should resolve to itself when calling getElement', async () => { + const el = notFoundElementFactory('not-found') + expect(await el.getElement()).toBe(el) + }) + + it('should throw error on method calls', async () => { + const el = notFoundElementFactory('not-found') + expect(() => el.click()).toThrow("Can't call click on element with selector not-found because element wasn't found") + }) + + it('should throw error when awaiting a method call (sync throw)', async () => { + const el = notFoundElementFactory('not-found') + expect(() => el.getText()).toThrow("Can't call getText on element with selector not-found because element wasn't found") + }) + }) +}) diff --git a/test/index.test.ts b/test/index.test.ts index 25c6c95d7..bdaf5e160 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -5,5 +5,5 @@ test('index', () => { expect(setOptions.name).toBe('setDefaultOptions') expect(expectExport).toBeDefined() expect(utils.compareText).toBeDefined() - expect(matchers.size).toEqual(41) + expect(matchers.size).toEqual(39) }) diff --git a/test/matchers.defaultOptions.test.ts b/test/matchers.defaultOptions.test.ts new file mode 100644 index 000000000..7eac0ca57 --- /dev/null +++ b/test/matchers.defaultOptions.test.ts @@ -0,0 +1,52 @@ + +import { test, expect, vi, describe } from 'vitest' +import { expect as expectLib, getConfig, setDefaultOptions } from '../src/index.js' +import { $ } from '@wdio/globals' +import { waitUntil } from '../src/utils.js' + +vi.mock('@wdio/globals') + +vi.mock('../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../src/constants.js')).DEFAULT_OPTIONS, + } +})) +vi.mock('../src/util/waitUntil.js', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + waitUntil: vi.spyOn(actual, 'waitUntil') + } +}) + +describe('DEFAULT_OPTIONS', () => { + + test('should use wait 2000 and interval 100 from default options by default', async () => { + const el = await $('selector') + vi.mocked(el.isDisplayed) + .mockResolvedValue(false) + + await expect(expectLib(el).toBeDisplayed()).rejects.toThrowError() + + expect(waitUntil).toHaveBeenCalledWith( + expect.any(Function), + false, + expect.objectContaining({ interval: 100, wait: 2000 }) + ) + expect(el.isDisplayed).toHaveBeenCalledTimes(20) + }) + + test('should allow to customized global DEFAULT_OPTIONS', async () => { + setDefaultOptions({ wait: 500, interval: 50 }) + + const config = getConfig() + + expect(config).toEqual(expect.objectContaining({ + wait: 500, + interval: 50 + })) + }) +}) + diff --git a/test/matchers.test.ts b/test/matchers.test.ts index a1534d539..a70dd2e59 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -1,9 +1,19 @@ import { test, expect, vi, describe } from 'vitest' import { matchers, expect as expectLib } from '../src/index.js' -import { $ } from '@wdio/globals' +import { $, $$ } from '@wdio/globals' vi.mock('@wdio/globals') +vi.mock('../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../src/constants.js')).DEFAULT_OPTIONS, + // speed up tests by lowering default wait timeout + wait : 15, + interval: 5 + } +})) + const ALL_MATCHERS = [ // browser 'toHaveClipboardText', @@ -26,11 +36,9 @@ const ALL_MATCHERS = [ 'toHaveAttribute', 'toHaveAttr', 'toHaveChildren', - 'toHaveClass', - 'toHaveElementClass', - 'toHaveClassContaining', 'toHaveComputedLabel', 'toHaveComputedRole', + 'toHaveElementClass', 'toHaveElementProperty', 'toHaveHeight', 'toHaveHref', @@ -102,6 +110,19 @@ describe('Custom Wdio Matchers Integration Tests', async () => { await expectLib(el).toHaveAttr('someAttribute', 'some attribute') await expectLib(el).toHaveElementProperty('someProperty', '1') }) + + test('toHave works with stringContaining asymmetric matcher', async () => { + await expectLib(el).toHaveText([expectLib.stringContaining('Valid'), expectLib.stringContaining('Valid')]) + }) + + // TODO to support one day? + test.skip('toHave works with arrayContaining asymmetric matcher', async () => { + await expectLib(el).toHaveText( + expectLib.arrayContaining([ + expectLib.stringContaining('Valid'), + expectLib.stringContaining('Valid') + ])) + }) }) describe('Matchers fails when using `.not` with proper message', async () => { @@ -111,43 +132,43 @@ describe('Custom Wdio Matchers Integration Tests', async () => { await expect(() => expectLib(el).not.toBeDisplayed({ wait: 1 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be displayed -Expected [not]: "not displayed" -Received : "displayed"` +Expected: "not displayed" +Received: "displayed"` ) await expect(() => expectLib(el).not.toBeExisting({ wait: 1 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be existing -Expected [not]: "not existing" -Received : "existing"` +Expected: "not existing" +Received: "existing"` ) await expect(() => expectLib(el).not.toBeEnabled({ wait: 1 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be enabled -Expected [not]: "not enabled" -Received : "enabled"` +Expected: "not enabled" +Received: "enabled"` ) await expect(() => expectLib(el).not.toBeClickable({ wait: 1 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be clickable -Expected [not]: "not clickable" -Received : "clickable"` +Expected: "not clickable" +Received: "clickable"` ) await expect(() => expectLib(el).not.toBeFocused({ wait: 1 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be focused -Expected [not]: "not focused" -Received : "focused"` +Expected: "not focused" +Received: "focused"` ) await expect(() => expectLib(el).not.toBeSelected({ wait: 1 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be selected -Expected [not]: "not selected" -Received : "selected"` +Expected: "not selected" +Received: "selected"` ) }) @@ -159,21 +180,21 @@ Expected [not]: " Valid Text " Received : " Valid Text "` ) - await expect(() => expectLib(el).not.toHaveHTML('', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toHaveHTML('')).rejects.toThrow(`\ Expect $(\`selector\`) not to have HTML Expected [not]: "" Received : ""` ) - await expect(() => expectLib(el).not.toHaveComputedLabel('Computed Label', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toHaveComputedLabel('Computed Label')).rejects.toThrow(`\ Expect $(\`selector\`) not to have computed label Expected [not]: "Computed Label" Received : "Computed Label"` ) - await expect(() => expectLib(el).not.toHaveComputedRole('Computed Role', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toHaveComputedRole('Computed Role')).rejects.toThrow(`\ Expect $(\`selector\`) not to have computed role Expected [not]: "Computed Role" @@ -182,21 +203,21 @@ Received : "Computed Role"` }) test('size matchers', async () => { - await expect(() => expectLib(el).not.toHaveSize({ width: 100, height: 50 }, { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toHaveSize({ width: 100, height: 50 })).rejects.toThrow(`\ Expect $(\`selector\`) not to have size Expected [not]: {"height": 50, "width": 100} Received : {"height": 50, "width": 100}` ) - await expect(() => expectLib(el).not.toHaveHeight(50, { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toHaveHeight(50)).rejects.toThrow(`\ Expect $(\`selector\`) not to have height Expected [not]: 50 Received : 50` ) - await expect(() => expectLib(el).not.toHaveWidth(100, { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toHaveWidth(100)).rejects.toThrow(`\ Expect $(\`selector\`) not to have width Expected [not]: 100 @@ -289,31 +310,31 @@ Received: "not selected"`) }) test('Ensure toHave matchers throws and show proper failing message', async () => { - await expect(() => expectLib(el).toHaveText('Some other text', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveText('Some other text')).rejects.toThrow(`\ Expect $(\`selector\`) to have text Expected: "Some other text" Received: " Valid Text "`) - await expect(() => expectLib(el).toHaveHTML('', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveHTML('')).rejects.toThrow(`\ Expect $(\`selector\`) to have HTML Expected: "" Received: ""`) - await expect(() => expectLib(el).toHaveComputedLabel('Some Other Computed Label', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveComputedLabel('Some Other Computed Label')).rejects.toThrow(`\ Expect $(\`selector\`) to have computed label Expected: "Some Other Computed Label" Received: "Computed Label"`) - await expect(() => expectLib(el).toHaveComputedRole('Some Other Computed Role', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveComputedRole('Some Other Computed Role')).rejects.toThrow(`\ Expect $(\`selector\`) to have computed role Expected: "Some Other Computed Role" Received: "Computed Role"`) - await expect(() => expectLib(el).toHaveElementProperty('someProperty', 'some other value', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveElementProperty('someProperty', 'some other value')).rejects.toThrow(`\ Expect $(\`selector\`) to have property someProperty Expected: "some other value" @@ -322,7 +343,7 @@ Received: "1"`) }) test('Ensure toHaveAttribute matchers throw and show proper failing message', async () => { - await expect(() => expectLib(el).toHaveAttribute('someAttribute', 'some other attribute', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveAttribute('someAttribute', 'some other attribute')).rejects.toThrow(`\ Expect $(\`selector\`) to have attribute someAttribute Expected: "some other attribute" @@ -334,7 +355,7 @@ Expect $(\`selector\`) to have attribute notExistingAttribute Expected: true Received: false`) - await expect(() => expectLib(el).toHaveAttr('someAttribute', 'some other attribute', { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveAttr('someAttribute', 'some other attribute')).rejects.toThrow(`\ Expect $(\`selector\`) to have attribute someAttribute Expected: "some other attribute" @@ -342,7 +363,7 @@ Received: null`) }) test('Ensure toHaveSize, toHaveHeight, toHaveWidth matchers throw and show proper failing message', async () => { - await expect(() => expectLib(el).toHaveSize({ width: 200, height: 100 }, { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveSize({ width: 200, height: 100 })).rejects.toThrow(`\ Expect $(\`selector\`) to have size - Expected - 2 @@ -354,13 +375,13 @@ Expect $(\`selector\`) to have size + "height": 50, + "width": 100, }`) - await expect(() => expectLib(el).toHaveHeight(100, { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveHeight(100)).rejects.toThrow(`\ Expect $(\`selector\`) to have height Expected: 100 Received: 50`) - await expect(() => expectLib(el).toHaveWidth(200, { wait: 1 })).rejects.toThrow(`\ + await expect(() => expectLib(el).toHaveWidth(200)).rejects.toThrow(`\ Expect $(\`selector\`) to have width Expected: 200 @@ -388,16 +409,16 @@ Received: 100`) }) test('toHave matchers', async () => { - await expectLib(el).not.toHaveText('Some other text', { wait: 1 }) - await expectLib(el).not.toHaveHTML('', { wait: 1 }) - await expectLib(el).not.toHaveComputedLabel('Some Other Computed Label', { wait: 1 }) - await expectLib(el).not.toHaveComputedRole('Some Other Computed Role', { wait: 1 }) - await expectLib(el).not.toHaveElementProperty('someProperty', 'some other value', { wait: 1 }) - await expectLib(el).not.toHaveAttribute('someAttribute', 'some other attribute', { wait: 1 }) - await expectLib(el).not.toHaveAttr('someAttribute', 'some other attribute', { wait: 1 }) - await expectLib(el).not.toHaveSize({ width: 200, height: 100 }, { wait: 1 }) - await expectLib(el).not.toHaveHeight(100, { wait: 1 }) - await expectLib(el).not.toHaveWidth(200, { wait: 1 }) + await expectLib(el).not.toHaveText('Some other text') + await expectLib(el).not.toHaveHTML('') + await expectLib(el).not.toHaveComputedLabel('Some Other Computed Label') + await expectLib(el).not.toHaveComputedRole('Some Other Computed Role') + await expectLib(el).not.toHaveElementProperty('someProperty', 'some other value') + await expectLib(el).not.toHaveAttribute('someAttribute', 'some other attribute') + await expectLib(el).not.toHaveAttr('someAttribute', 'some other attribute') + await expectLib(el).not.toHaveSize({ width: 200, height: 100 }) + await expectLib(el).not.toHaveHeight(100) + await expectLib(el).not.toHaveWidth(200) }) }) @@ -413,7 +434,7 @@ Received: 100`) .mockResolvedValueOnce(true) // Passes when element becomes displayed - await expectLib(el).toBeDisplayed({ wait: 300, interval: 100 }) + await expectLib(el).toBeDisplayed({ wait: 300, interval: 5 }) vi.mocked(el.isDisplayed) .mockResolvedValueOnce(false) @@ -421,11 +442,11 @@ Received: 100`) .mockResolvedValueOnce(true) // Should not pass with the same scenario to be consistent - await expect(() => expectLib(el).not.toBeDisplayed({ wait: 300, interval: 100 })).rejects.toThrow(`\ + await expect(() => expectLib(el).not.toBeDisplayed({ wait: 300, interval: 5 })).rejects.toThrow(`\ Expect $(\`selector\`) not to be displayed -Expected [not]: "not displayed" -Received : "displayed"`) +Expected: "not displayed" +Received: "displayed"`) expect(el.isDisplayed).toHaveBeenCalledTimes(6) }) @@ -433,9 +454,7 @@ Received : "displayed"`) test('when element eventually is not displayed, matcher and .not matcher should be consistent', async () => { const el = await $('selector') vi.mocked(el.isDisplayed) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(false) + .mockResolvedValue(false) // Does not pass since element never becomes displayed await expect(expectLib(el).toBeDisplayed({ wait: 300, interval: 100 })).rejects.toThrow(`\ @@ -445,9 +464,7 @@ Expected: "displayed" Received: "not displayed"`) vi.mocked(el.isDisplayed) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(false) + .mockResolvedValue(false) // Should pass with the same scenario to be consistent await expectLib(el).not.toBeDisplayed({ wait: 300, interval: 100 }) @@ -481,4 +498,61 @@ Received: "not displayed"`) await expectLib(el).not.toBeDisplayed() }) }) + + describe('Matchers pass with success when using valid element array', async () => { + const elements = await $$('selector') + + test('toBe matchers', async () => { + await expectLib(elements).toBeDisplayed() + await expectLib(elements).toBeExisting() + await expectLib(elements).toBeEnabled() + await expectLib(elements).toBeClickable() + await expectLib(elements).toBeFocused() + await expectLib(elements).toBeSelected() + }) + + test('toHave matchers', async () => { + await expectLib(elements).toHaveText('Valid Text') + await expectLib(elements).toHaveHTML('') + await expectLib(elements).toHaveComputedLabel('Computed Label') + await expectLib(elements).toHaveComputedRole('Computed Role') + await expectLib(elements).toHaveSize({ width: 100, height: 50 }) + await expectLib(elements).toHaveHeight(50) + await expectLib(elements).toHaveWidth(100) + await expectLib(elements).toHaveAttribute('someAttribute', 'some attribute') + await expectLib(elements).toHaveAttribute('someAttribute') + await expectLib(elements).toHaveAttr('someAttribute', 'some attribute') + await expectLib(elements).toHaveElementProperty('someProperty', '1') + await expectLib(elements).toBeElementsArrayOfSize(2) + }) + + test('toBe matchers and toHve matchers work with .not', async () => { + await expectLib(elements).not.toBeDisabled() + await expectLib(elements).not.toHaveText('Some other text') + await expectLib(elements).not.toHaveHTML('') + await expectLib(elements).not.toHaveComputedLabel('Some Other Computed Label') + await expectLib(elements).not.toHaveComputedRole('Some Other Computed Role') + await expectLib(elements).not.toHaveElementProperty('someProperty', 'some other value') + await expectLib(elements).not.toHaveAttribute('someAttribute', 'some other attribute') + await expectLib(elements).not.toHaveAttr('someAttribute', 'some other attribute') + await expectLib(elements).not.toHaveSize({ width: 200, height: 100 }) + await expectLib(elements).not.toHaveHeight(100) + await expectLib(elements).not.toHaveWidth(200) + await expectLib(elements).not.toBeElementsArrayOfSize(3) + }) + + test('toHave works with stringContaining asymmetric matcher', async () => { + await expectLib(elements).toHaveText([expectLib.stringContaining('Valid'), expectLib.stringContaining('Valid')]) + await expectLib(elements).not.toHaveText([expectLib.stringContaining('Test'), expectLib.stringContaining('Test')]) + }) + + // TODO to support one day? + test.skip('toHave works with arrayContaining asymmetric matcher', async () => { + await expectLib(elements).toHaveText( + expectLib.arrayContaining([ + expectLib.stringContaining('Valid'), + expectLib.stringContaining('Valid') + ])) + }) + }) }) diff --git a/test/matchers/beMatchers.test.ts b/test/matchers/beMatchers.test.ts index c2eba4864..f76fdbe75 100644 --- a/test/matchers/beMatchers.test.ts +++ b/test/matchers/beMatchers.test.ts @@ -1,139 +1,471 @@ -import { vi, test, describe, expect } from 'vitest' -import { $ } from '@wdio/globals' -import { matcherLastWordName } from '../__fixtures__/utils.js' +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' +import { lastMatcherWords } from '../__fixtures__/utils.js' import * as Matchers from '../../src/matchers.js' +import { executeCommandBe, waitUntil } from '../../src/utils.js' +import { toBeChecked, toBeClickable, toBeDisplayedInViewport, toBeEnabled, toBeExisting, toBeFocused, toBePresent, toBeSelected, toExist } from '../../src/matchers.js' vi.mock('@wdio/globals') -const ignoredMatchers = ['toBeElementsArrayOfSize', 'toBeDisabled', 'toBeDisplayed', 'toBeRequested', 'toBeRequestedTimes', 'toBeRequestedWithResponse', 'toBeRequestedWith'] -const beMatchers = { - 'toBeChecked': 'isSelected', - 'toBeClickable': 'isClickable', - 'toBeDisplayedInViewport': 'isDisplayed', - 'toBeEnabled': 'isEnabled', - 'toBeExisting': 'isExisting', - 'toBeFocused': 'isFocused', - 'toBePresent': 'isExisting', - 'toBeSelected': 'isSelected', -} satisfies Partial> +const ignoredMatchers = ['toBeElementsArrayOfSize', 'toBeRequested', 'toBeRequestedTimes', 'toBeRequestedWithResponse', 'toBeRequestedWith', 'toBeDisplayed', 'toBeDisabled'] + +const beMatchers = new Map([ + [toBeChecked, 'isSelected' satisfies keyof WebdriverIO.Element], + [toBeClickable, 'isClickable' satisfies keyof WebdriverIO.Element], + [toBeDisplayedInViewport, 'isDisplayed' satisfies keyof WebdriverIO.Element], + [toBeEnabled, 'isEnabled' satisfies keyof WebdriverIO.Element], + [toBeExisting, 'isExisting' satisfies keyof WebdriverIO.Element], + [toBeFocused, 'isFocused' satisfies keyof WebdriverIO.Element], + [toBePresent, 'isExisting' satisfies keyof WebdriverIO.Element], + [toBeSelected, 'isSelected' satisfies keyof WebdriverIO.Element], + [toExist, 'isExisting' satisfies keyof WebdriverIO.Element], +]) describe('be* matchers', () => { describe('Ensure all toBe matchers are covered', () => { test('all toBe matchers are covered in beMatchers', () => { - const matcherNames = Object.keys(Matchers).filter(name => name.startsWith('toBe') && !ignoredMatchers.includes(name)) - matcherNames.sort() + const matcherFnNames = Object.keys(Matchers).filter(name => name.startsWith('toBe') && !ignoredMatchers.includes(name)) + matcherFnNames.push('toExist') + matcherFnNames.sort() - expect(Object.keys(beMatchers)).toEqual(matcherNames) + const beMatcherNames = Array.from(beMatchers.keys()).map(matcher => matcher.name) + expect(beMatcherNames).toEqual(matcherFnNames) }) }) - Object.entries(beMatchers).forEach(([matcherName, elementFnName]) => { - const matcherFn = Matchers[matcherName as keyof typeof Matchers] - - describe(matcherName, () => { - test('wait for success', async () => { - const el = await $('sel') + beMatchers.forEach((elFnName, matcherFn) => { + const elementFnName = elFnName as keyof WebdriverIO.Element + const selectorName = '$$(`sel`)' - el[elementFnName] = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValueOnce(true) + describe(matcherFn.name, () => { + let thisContext: { matcherFn: typeof matcherFn } + let thisNotContext: { isNot: true, matcherFn: typeof matcherFn } - const result = await matcherFn.call({}, el) as ExpectWebdriverIO.AssertionResult + let el: ChainablePromiseElement - expect(result.pass).toBe(true) - expect(el[elementFnName]).toHaveBeenCalledTimes(3) + beforeEach(async () => { + thisContext = { matcherFn } + thisNotContext = { isNot: true, matcherFn } + el = await $('sel') }) - test('wait but failure', async () => { - const el = await $('sel') + describe('given single element', () => { + test('wait for success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + vi.mocked(el[elementFnName]).mockResolvedValueOnce(false).mockResolvedValueOnce(true) - el[elementFnName] = vi.fn().mockRejectedValue(new Error('some error')) + const result = await thisContext.matcherFn(el, { beforeAssertion, afterAssertion, wait: 125, interval: 50 }) - await expect(() => matcherFn.call({}, el, 10, {})) - .rejects.toThrow('some error') - }) + expect(result.pass).toBe(true) + expect(el[elementFnName]).toHaveBeenCalledTimes(2) - test('success on the first attempt', async () => { - const el = await $('sel') + expect(executeCommandBe).toHaveBeenCalledExactlyOnceWith(el, expect.any(Function), + { + afterAssertion, + beforeAssertion, + wait: 125, + interval: 50 + }, + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 125, interval: 50 }) + expect(beforeAssertion).toBeCalledWith({ + matcherName: matcherFn.name, + options: { beforeAssertion, afterAssertion, wait: 125, interval: 50 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: matcherFn.name, + options: { beforeAssertion, afterAssertion, wait: 125, interval: 50 }, + result + }) + }) - el[elementFnName] = vi.fn().mockResolvedValue(true) + test('wait but error', async () => { + const el = await $('sel') - const result = await matcherFn.call({}, el) as ExpectWebdriverIO.AssertionResult - expect(result.pass).toBe(true) - expect(el[elementFnName]).toHaveBeenCalledTimes(1) - }) + vi.mocked(el[elementFnName]).mockRejectedValue(new Error('some error')) - test('no wait - failure', async () => { - const el = await $('sel') + await expect(() => thisContext.matcherFn(el)) + .rejects.toThrow('some error') + }) - el[elementFnName] = vi.fn().mockResolvedValue(false) + test('success on the first attempt', async () => { + const el = await $('sel') - const result = await matcherFn.call({}, el, { wait: 0 }) as ExpectWebdriverIO.AssertionResult - expect(result.pass).toBe(false) - expect(el[elementFnName]).toHaveBeenCalledTimes(1) - }) + vi.mocked(el[elementFnName]).mockResolvedValue(true) - test('no wait - success', async () => { - const el = await $('sel') + const result = await thisContext.matcherFn(el) + expect(result.pass).toBe(true) + expect(el[elementFnName]).toHaveBeenCalledTimes(1) + }) - el[elementFnName] = vi.fn().mockResolvedValue(true) + test('no wait - failure', async () => { + const el = await $('sel') - const result = await matcherFn.call({}, el, { wait: 0 }) as ExpectWebdriverIO.AssertionResult - expect(result.pass).toBe(true) - expect(el[elementFnName]).toHaveBeenCalledTimes(1) - }) + vi.mocked(el[elementFnName]).mockResolvedValue(false) - test('not - failure - pass should be true', async () => { - const el = await $('sel') + const result = await thisContext.matcherFn(el, { wait: 0 }) + expect(result.pass).toBe(false) + expect(el[elementFnName]).toHaveBeenCalledTimes(1) + }) - const result = await matcherFn.call({ isNot: true }, el, { wait: 0 }) as ExpectWebdriverIO.AssertionResult + test('no wait - success', async () => { + const el = await $('sel') - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to be ${matcherLastWordName(matcherName)} + vi.mocked(el[elementFnName]).mockResolvedValue(true) -Expected [not]: "not ${matcherLastWordName(matcherName)}" -Received : "${matcherLastWordName(matcherName)}"` - ) - }) + const result = await thisContext.matcherFn(el, { wait: 0 }) + expect(result.pass).toBe(true) + expect(el[elementFnName]).toHaveBeenCalledTimes(1) + }) - test('not - success - pass should be false', async () => { - const el = await $('sel') + test('not - failure - pass should be true', async () => { + const el = await $('sel') - el[elementFnName] = vi.fn().mockResolvedValue(false) + const result = await thisNotContext.matcherFn(el) - const result = await matcherFn.call({ isNot: true }, el, { wait: 0 }) as ExpectWebdriverIO.AssertionResult + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + if (matcherFn.name === 'toExist') {return} + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to be ${lastMatcherWords(matcherFn.name)} - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) +Expected: "not ${lastMatcherWords(matcherFn.name)}" +Received: "${lastMatcherWords(matcherFn.name)}"` + ) + }) - test('not - failure (with wait) - pass should be true', async () => { - const el = await $('sel') + test('not - success - pass should be false', async () => { + const el = await $('sel') + vi.mocked(el[elementFnName]).mockResolvedValue(false) - const result = await matcherFn.call({ isNot: true }, el, { wait: 1 }) as ExpectWebdriverIO.AssertionResult + const result = await thisNotContext.matcherFn(el, { wait: 0 }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - test('not - success (with wait) - pass should be false', async () => { - const el = await $('sel') - el[elementFnName] = vi.fn().mockResolvedValue(false) - const result = await matcherFn.call({ isNot: true }, el, { wait: 1 }) as ExpectWebdriverIO.AssertionResult + test('not - failure (with wait) - pass should be true', async () => { + const el = await $('sel') - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + const result = await thisNotContext.matcherFn(el) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + }) + + test('not - success (with wait) - pass should be false', async () => { + const el = await $('sel') + vi.mocked(el[elementFnName]).mockResolvedValue(false) + + const result = await thisNotContext.matcherFn(el) - test('message', async () => { - const el = await $('sel') - el[elementFnName] = vi.fn().mockResolvedValue(false) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - const result = await matcherFn.call({}, el, { wait: 1 }) as ExpectWebdriverIO.AssertionResult - expect(result.pass).toBe(false) - expect(result.message()).toBe(`\ -Expect $(\`sel\`) to be ${matcherLastWordName(matcherName)} + test('message', async () => { + const el = await $('sel') + vi.mocked(el[elementFnName]).mockResolvedValue(false) + + const result = await thisContext.matcherFn(el, { wait: 0 }) + + expect(result.pass).toBe(false) + if (matcherFn.name === 'toExist') {return} + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to be ${lastMatcherWords(matcherFn.name)} + +Expected: "${lastMatcherWords(matcherFn.name)}" +Received: "not ${lastMatcherWords(matcherFn.name)}"`) + }) + }) -Expected: "${matcherLastWordName(matcherName)}" -Received: "not ${matcherLastWordName(matcherName)}"` - ) + describe('given multiple elements', () => { + let elements: ChainablePromiseArray + + beforeEach(async () => { + elements = await $$('sel') + for (const element of elements) { + vi.mocked(element[elementFnName]).mockResolvedValue(true) + } + expect(elements).toHaveLength(2) + }) + + test('wait for success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.matcherFn(elements, { beforeAssertion, afterAssertion, wait: 500 }) + + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalled() + } + + expect(executeCommandBe).toHaveBeenCalledExactlyOnceWith(elements, expect.any(Function), + { + afterAssertion, + beforeAssertion, + wait: 500 + }, + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 500, interval: 100 }) + expect(result.pass).toEqual(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: matcherFn.name, + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: matcherFn.name, + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) + + test('success with matcherFn and custom command options', async () => { + const result = await thisContext.matcherFn(elements, { wait: 4, interval: 99 }) + + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalledOnce() + } + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 4, interval: 99 }) + expect(result.pass).toBe(true) + }) + + test('success with matcherFn and custom command options - only interval', async () => { + const result = await thisContext.matcherFn(elements, { interval: 99 }) + + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalledOnce() + } + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 1, interval: 99 }) + expect(result.pass).toBe(true) + }) + + test('success with matcherFn and default command options', async () => { + const result = await thisContext.matcherFn(elements) + + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalledOnce() + } + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 1, interval: 100 }) + expect(result.pass).toBe(true) + }) + + test('wait but failure', async () => { + vi.mocked(elements[0][elementFnName]).mockRejectedValue(new Error('some error')) + + await expect(() => thisContext.matcherFn(elements)) + .rejects.toThrow('some error') + }) + + test('success on the first attempt', async () => { + const result = await thisContext.matcherFn(elements) + + expect(result.pass).toBe(true) + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalledTimes(1) + } + }) + + test('no wait - failure', async () => { + vi.mocked(elements[0][elementFnName]).mockResolvedValue(false) + + const result = await thisContext.matcherFn(elements, { wait: 0 }) + + expect(result.pass).toBe(false) + expect(elements[0][elementFnName]).toHaveBeenCalledTimes(1) + expect(elements[1][elementFnName]).toHaveBeenCalledTimes(1) + }) + + test('no wait - success', async () => { + const result = await thisContext.matcherFn(elements) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 1, + interval: 100, + }) + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalled() + } + expect(result.pass).toBe(true) + }) + + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.matcherFn(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + const verb = matcherFn.name === 'toExist' ? 'to' : 'to be' + expect(result.message()).toEqual(`\ +Expect ${selectorName} not ${verb} ${lastMatcherWords(matcherFn.name)} + +- Expected - 2 ++ Received + 2 + + Array [ +- "not ${lastMatcherWords(matcherFn.name)}", +- "not ${lastMatcherWords(matcherFn.name)}", ++ "${lastMatcherWords(matcherFn.name)}", ++ "${lastMatcherWords(matcherFn.name)}", + ]` + ) + }) + + test('not - success - pass should be false', async () => { + for (const element of elements) { + vi.mocked(element[elementFnName]).mockResolvedValue(false, { wait: 0 }) + } + + const result = await thisNotContext.matcherFn(elements) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure (with wait) - pass should be true', async () => { + const result = await thisNotContext.matcherFn(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + }) + + test('not - success (with wait) - pass should be false', async () => { + for (const element of elements) { + vi.mocked(element[elementFnName]).mockResolvedValue(false) + } + + const result = await thisNotContext.matcherFn(elements) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), true, { + wait: 1, + interval: 100, + }) + for (const element of elements) { + expect(element[elementFnName]).toHaveBeenCalled() + } + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('message when both elements fail', async () => { + const elements = await $$('sel') + + for (const element of elements) { + vi.mocked(element[elementFnName]).mockResolvedValue(false) + } + + const result = await thisContext.matcherFn(elements) + const verb = matcherFn.name === 'toExist' ? 'to' : 'to be' + expect(result.message()).toEqual(`\ +Expect ${selectorName} ${verb} ${lastMatcherWords(matcherFn.name)} + +- Expected - 2 ++ Received + 2 + + Array [ +- "${lastMatcherWords(matcherFn.name)}", +- "${lastMatcherWords(matcherFn.name)}", ++ "not ${lastMatcherWords(matcherFn.name)}", ++ "not ${lastMatcherWords(matcherFn.name)}", + ]`) + }) + + test('message when a single element fails', async () => { + vi.mocked(elements[0][elementFnName]).mockResolvedValue(false) + + const result = await thisContext.matcherFn(elements) + const verb = matcherFn.name === 'toExist' ? 'to' : 'to be' + expect(result.message()).toEqual(`\ +Expect ${selectorName} ${verb} ${lastMatcherWords(matcherFn.name)} + +- Expected - 1 ++ Received + 1 + + Array [ +- "${lastMatcherWords(matcherFn.name)}", ++ "not ${lastMatcherWords(matcherFn.name)}", + "${lastMatcherWords(matcherFn.name)}", + ]`) + }) + + describe('fails with ElementArray', () => { + let elementsArray: WebdriverIO.ElementArray + + beforeEach(async () => { + elementsArray = await $$('sel').getElements() + for (const element of elementsArray) { + vi.mocked(element[elementFnName]).mockResolvedValue(true) + } + expect(elementsArray).toHaveLength(2) + }) + + test('success with ElementArray', async () => { + const result = await thisContext.matcherFn(elementsArray) + + for (const element of elementsArray) { + expect(element[elementFnName]).toHaveBeenCalled() + } + + expect(result.pass).toBe(true) + }) + + test('fails with ElementArray', async () => { + vi.mocked(elementsArray[1][elementFnName]).mockResolvedValue(false) + + const result = await thisContext.matcherFn(elementsArray, { wait: 0 }) + + for (const element of elementsArray) { + expect(element[elementFnName]).toHaveBeenCalled() + } + + expect(result.pass).toBe(false) + const verb = matcherFn.name === 'toExist' ? 'to' : 'to be' + expect(result.message()).toEqual(`\ +Expect ${selectorName} ${verb} ${lastMatcherWords(matcherFn.name)} + +- Expected - 1 ++ Received + 1 + + Array [ + "${lastMatcherWords(matcherFn.name)}", +- "${lastMatcherWords(matcherFn.name)}", ++ "not ${lastMatcherWords(matcherFn.name)}", + ]`) + }) + + describe('given filtered elememts (Element[])', () => { + let filteredElements: WebdriverIO.Element[] + test('success with Element[]', async () => { + filteredElements = await elementsArray.filter((element) => element.isExisting()) + + const result = await thisContext.matcherFn(filteredElements) + + for (const element of filteredElements) { + expect(element[elementFnName]).toHaveBeenCalled() + } + expect(result.pass).toBe(true) + }) + + test('fails with Element[]', async () => { + filteredElements = await elementsArray.filter((element) => element.isExisting()) + + vi.mocked(filteredElements[1][elementFnName]).mockResolvedValue(false) + + const result = await thisContext.matcherFn(filteredElements) + + for (const element of filteredElements) { + expect(element[elementFnName]).toHaveBeenCalled() + } + + expect(result.pass).toBe(false) + const verb = matcherFn.name === 'toExist' ? 'to' : 'to be' + expect(result.message()).toEqual(`\ +Expect $(\`sel\`), $$(\`sel\`)[1] ${verb} ${lastMatcherWords(matcherFn.name)} + +- Expected - 1 ++ Received + 1 + + Array [ + "${lastMatcherWords(matcherFn.name)}", +- "${lastMatcherWords(matcherFn.name)}", ++ "not ${lastMatcherWords(matcherFn.name)}", + ]`) + }) + }) + }) }) }) }) diff --git a/test/matchers/browser/toHaveClipboardText.test.ts b/test/matchers/browser/toHaveClipboardText.test.ts index 109b97018..9e652d254 100644 --- a/test/matchers/browser/toHaveClipboardText.test.ts +++ b/test/matchers/browser/toHaveClipboardText.test.ts @@ -1,4 +1,4 @@ -import { vi, test, expect } from 'vitest' +import { vi, test, expect, describe } from 'vitest' import { browser } from '@wdio/globals' import { toHaveClipboardText } from '../../../src/matchers/browser/toHaveClipboardText' @@ -8,20 +8,46 @@ vi.mock('@wdio/globals') const beforeAssertion = vi.fn() const afterAssertion = vi.fn() -test('toHaveClipboardText', async () => { - browser.execute = vi.fn().mockResolvedValue('some clipboard text') +describe(toHaveClipboardText, () => { + test('success', async () => { + vi.mocked(browser.execute).mockResolvedValue('some clipboard text') - const result = await toHaveClipboardText.call({}, browser, 'some ClipBoard text', { ignoreCase: true, beforeAssertion, afterAssertion }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveClipboardText', - expectedValue: 'some ClipBoard text', - options: { ignoreCase: true, beforeAssertion, afterAssertion } + const result = await toHaveClipboardText(browser, 'some ClipBoard text', { ignoreCase: true, beforeAssertion, afterAssertion }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveClipboardText', + expectedValue: 'some ClipBoard text', + options: { ignoreCase: true, beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveClipboardText', + expectedValue: 'some ClipBoard text', + options: { ignoreCase: true, beforeAssertion, afterAssertion }, + result + }) }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveClipboardText', - expectedValue: 'some ClipBoard text', - options: { ignoreCase: true, beforeAssertion, afterAssertion }, - result + + test('failure check with message', async () => { + vi.mocked(browser.execute).mockResolvedValue('actual text') + + const result = await toHaveClipboardText(browser, 'expected text', { wait: 1 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect browser to have clipboard text + +Expected: "expected text" +Received: "actual text"` + ) + }) + + test('should log warning if setPermissions fails', async () => { + vi.mocked(browser.execute).mockResolvedValue('text') + vi.mocked(browser.setPermissions).mockRejectedValueOnce(new Error('unsupported')) + + const result = await toHaveClipboardText(browser, 'text', { wait: 0 }) + + expect(result.pass).toBe(true) + expect(browser.setPermissions).toHaveBeenCalledWith({ name: 'clipboard-read' }, 'granted') }) }) diff --git a/test/matchers/browserMatchers.test.ts b/test/matchers/browserMatchers.test.ts index f017b5674..bebcb0dc8 100644 --- a/test/matchers/browserMatchers.test.ts +++ b/test/matchers/browserMatchers.test.ts @@ -1,73 +1,80 @@ -import { vi, test, describe, expect } from 'vitest' +import { vi, test, describe, expect, beforeEach } from 'vitest' import { browser } from '@wdio/globals' - -import { getExpectMessage, matcherNameToString, matcherLastWordName } from '../__fixtures__/utils.js' -import * as Matchers from '../../src/matchers.js' +import { toHaveUrl } from '../../src/matchers/browser/toHaveUrl.js' +import { toHaveTitle } from '../../src/matchers/browser/toHaveTitle.js' +import { matcherNameLastWords } from '../__fixtures__/utils' vi.mock('@wdio/globals') -const browserMatchers = { - 'toHaveUrl': 'getUrl', - 'toHaveTitle': 'getTitle' -} satisfies Partial> +const browserMatchers = new Map([ + [toHaveUrl, browser.getUrl], + [toHaveTitle, browser.getTitle], +]) const validText = ' Valid Text ' const wrongText = ' Wrong Text ' describe('browser matchers', () => { - Object.entries(browserMatchers).forEach(([matcherName, browserFnName]) => { - const matcherFn = Matchers[matcherName as keyof typeof Matchers] + browserMatchers.forEach((browserFn, matcherFn) => { + + let thisContext: { matcherFn: typeof matcherFn } + let thisNotContext: { isNot: true, matcherFn: typeof matcherFn } - describe(matcherName, () => { + beforeEach(() => { + thisContext = { matcherFn } + thisNotContext = { isNot: true, matcherFn } + }) + + describe(matcherFn, () => { test('wait for success', async () => { - browser[browserFnName] = vi.fn().mockResolvedValueOnce(wrongText).mockResolvedValueOnce(wrongText).mockResolvedValueOnce(validText) + vi.mocked(browserFn).mockResolvedValueOnce(wrongText).mockResolvedValueOnce(wrongText).mockResolvedValueOnce(validText) - const result = await matcherFn.call({}, browser, validText, { trim: false }) as ExpectWebdriverIO.AssertionResult + const result = await thisContext.matcherFn(browser, validText, { trim: false, wait: 500 }) expect(result.pass).toBe(true) - expect(browser[browserFnName]).toHaveBeenCalledTimes(3) + expect(browserFn).toHaveBeenCalledTimes(3) }) test('wait but error', async () => { - browser[browserFnName] = vi.fn().mockRejectedValue(new Error('some error')) + vi.mocked(browserFn).mockRejectedValue(new Error('some error')) - await expect(() => matcherFn.call({}, browser, validText, { trim: false })) + await expect(() => thisContext.matcherFn(browser, validText, { trim: false, wait: 1 })) .rejects.toThrow('some error') }) test('success on the first attempt', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(validText) + vi.mocked(browserFn).mockResolvedValue(validText) - const result = await matcherFn.call({}, browser, validText, { trim: false }) as ExpectWebdriverIO.AssertionResult + const result = await thisContext.matcherFn(browser, validText, { trim: false, wait: 1 }) expect(result.pass).toBe(true) - expect(browser[browserFnName]).toHaveBeenCalledTimes(1) + expect(browserFn).toHaveBeenCalledTimes(1) }) test('no wait - failure', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(wrongText) + vi.mocked(browserFn).mockResolvedValue(wrongText) - const result = await matcherFn.call({}, browser, validText, { wait: 0, trim: false }) as ExpectWebdriverIO.AssertionResult + const result = await thisContext.matcherFn(browser, validText, { wait: 0, trim: false }) expect(result.pass).toBe(false) - expect(browser[browserFnName]).toHaveBeenCalledTimes(1) + expect(browserFn).toHaveBeenCalledTimes(1) }) test('no wait - success', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(validText) + vi.mocked(browserFn).mockResolvedValue(validText) - const result = await matcherFn.call({}, browser, validText, { wait: 0, trim: false }) as ExpectWebdriverIO.AssertionResult + const result = await thisContext.matcherFn(browser, validText, { wait: 0, trim: false }) expect(result.pass).toBe(true) - expect(browser[browserFnName]).toHaveBeenCalledTimes(1) + expect(browserFn).toHaveBeenCalledTimes(1) }) test('not - failure - pass should be true', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(validText) - const result = await matcherFn.call({ isNot: true }, browser, validText, { wait: 0, trim: false }) as ExpectWebdriverIO.AssertionResult + vi.mocked(browserFn).mockResolvedValue(validText) + const result = await thisNotContext.matcherFn(browser, validText, { wait: 0, trim: false }) expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` expect(result.message()).toEqual(`\ -Expect window not to have ${matcherLastWordName(matcherName)} +Expect window not to have ${matcherNameLastWords(matcherFn.name)} Expected [not]: " Valid Text " Received : " Valid Text "` @@ -75,21 +82,21 @@ Received : " Valid Text "` }) test('not - success - pass should be false', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(wrongText) + vi.mocked(browserFn).mockResolvedValue(wrongText) - const result = await matcherFn.call({ isNot: true }, browser, validText, { wait: 0 }) as ExpectWebdriverIO.AssertionResult + const result = await thisNotContext.matcherFn(browser, validText) expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` }) test('not - failure (with wait) - pass should be true', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(validText) + vi.mocked(browserFn).mockResolvedValue(validText) - const result = await matcherFn.call({ isNot: true }, browser, validText, { wait: 1, trim: false }) as ExpectWebdriverIO.AssertionResult + const result = await thisNotContext.matcherFn(browser, validText, { wait: 1, trim: false }) expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` expect(result.message()).toEqual(`\ -Expect window not to have ${matcherLastWordName(matcherName)} +Expect window not to have ${matcherNameLastWords(matcherFn.name)} Expected [not]: " Valid Text " Received : " Valid Text "` @@ -97,16 +104,24 @@ Received : " Valid Text "` }) test('not - success (with wait) - pass should be false', async () => { - browser[browserFnName] = vi.fn().mockResolvedValue(wrongText) + vi.mocked(browserFn).mockResolvedValue(wrongText) - const result = await matcherFn.call({ isNot: true }, browser, validText, { wait: 1 }) as ExpectWebdriverIO.AssertionResult + const result = await thisNotContext.matcherFn(browser, validText) expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` }) test('message', async () => { - const result = await matcherFn.call({}, browser) as ExpectWebdriverIO.AssertionResult - expect(getExpectMessage(result.message())).toContain(matcherNameToString(matcherName)) + vi.mocked(browserFn).mockResolvedValue(wrongText) + const result = await thisContext.matcherFn(browser, validText) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect window to have ${matcherNameLastWords(matcherFn.name)} + +Expected: " Valid Text " +Received: " Wrong Text "` + ) }) }) }) diff --git a/test/matchers/element/toBeDisabled.test.ts b/test/matchers/element/toBeDisabled.test.ts index 1b0dac3e4..d6445a688 100644 --- a/test/matchers/element/toBeDisabled.test.ts +++ b/test/matchers/element/toBeDisabled.test.ts @@ -1,132 +1,310 @@ -import { vi, test, describe, expect } from 'vitest' -import { $ } from '@wdio/globals' - +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' import { toBeDisabled } from '../../../src/matchers/element/toBeDisabled.js' +import { executeCommandBe, waitUntil } from '../../../src/utils.js' vi.mock('@wdio/globals') -describe('toBeDisabled', () => { +describe(toBeDisabled, () => { + let thisContext: { toBeDisabled: typeof toBeDisabled } + let thisNotContext: { isNot: true; toBeDisabled: typeof toBeDisabled } + /** - * result is inverted for toBeDisplayed because it inverts isEnabled result + * result is inverted for toBeDisabled because it inverts isEnabled result * `!await el.isEnabled()` */ - test('wait for success', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(true).mockResolvedValueOnce(false) - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() + beforeEach(async () => { + thisContext = { toBeDisabled } + thisNotContext = { isNot: true, toBeDisabled } + }) - const result = await toBeDisabled.call({}, el, { beforeAssertion, afterAssertion }) + describe('given single element', () => { + let el: ChainablePromiseElement - expect(result.pass).toBe(true) - expect(el.isEnabled).toHaveBeenCalledTimes(3) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toBeDisabled', - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toBeDisabled', - options: { beforeAssertion, afterAssertion }, - result + beforeEach(async () => { + thisContext = { toBeDisabled } + thisNotContext = { isNot: true, toBeDisabled } + + el = await $('sel') + vi.mocked(el.isEnabled).mockResolvedValue(false) }) - }) - test('wait but failure', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockRejectedValue(new Error('some error')) + test('wait for success', async () => { + vi.mocked(el.isEnabled).mockResolvedValueOnce(true).mockResolvedValueOnce(false) + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toBeDisabled(el, { beforeAssertion, afterAssertion, wait: 500 }) + + expect(result.pass).toBe(true) + expect(el.isEnabled).toHaveBeenCalledTimes(2) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeDisabled', + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeDisabled', + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - await expect(() => toBeDisabled.call({}, el)) - .rejects.toThrow('some error') - }) + test('wait but error', async () => { + vi.mocked(el.isEnabled).mockRejectedValue(new Error('some error')) - test('success on the first attempt', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(false) + await expect(() => thisContext.toBeDisabled(el)) + .rejects.toThrow('some error') + }) - const result = await toBeDisabled.call({}, el) - expect(result.pass).toBe(true) - expect(el.isEnabled).toHaveBeenCalledTimes(1) - }) + test('success on the first attempt', async () => { + const result = await thisContext.toBeDisabled(el) - test('no wait - failure', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(true) + expect(result.pass).toBe(true) + expect(el.isEnabled).toHaveBeenCalledTimes(1) + }) - const result = await toBeDisabled.call({}, el, { wait: 0 }) + test('no wait - failure', async () => { + vi.mocked(el.isEnabled).mockResolvedValue(true) - expect(result.pass).toBe(false) - expect(el.isEnabled).toHaveBeenCalledTimes(1) - }) + const result = await thisContext.toBeDisabled(el, { wait: 0 }) - test('no wait - success', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(false) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to be disabled - const result = await toBeDisabled.call({}, el, { wait: 0 }) +Expected: "disabled" +Received: "not disabled"`) + expect(el.isEnabled).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(true) - expect(el.isEnabled).toHaveBeenCalledTimes(1) - }) + test('no wait - success', async () => { + const result = await thisContext.toBeDisabled(el, { wait: 0 }) - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(false) + expect(result.pass).toBe(true) + expect(el.isEnabled).toHaveBeenCalledTimes(1) + }) - const result = await toBeDisabled.call({ isNot: true }, el, { wait: 0 }) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toBeDisabled(el) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to be disabled -Expected [not]: "not disabled" -Received : "disabled"` - ) - }) +Expected: "not disabled" +Received: "disabled"`) + }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(true) + test('not - success - pass should be false', async () => { + const el = await $('sel') + vi.mocked(el.isEnabled).mockResolvedValue(true) - const result = await toBeDisabled.call({ isNot: true }, el, { wait: 0 }) + const result = await thisNotContext.toBeDisabled(el) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - test('not - failure (with wait) - pass should be true', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(false) + test('not - failure (with wait) - pass should be true', async () => { + const el = await $('sel') + vi.mocked(el.isEnabled).mockResolvedValue(false) - const result = await toBeDisabled.call({ isNot: true }, el, { wait: 1 }) + const result = await thisNotContext.toBeDisabled(el) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to be disabled + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + }) -Expected [not]: "not disabled" -Received : "disabled"` - ) + test('not - success (with wait) - pass should be false', async () => { + const el = await $('sel') + vi.mocked(el.isEnabled).mockResolvedValue(true) + + const result = await thisNotContext.toBeDisabled(el) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) }) - test('not - success (with wait) - pass should be false', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(true) + describe('given multiple elements', () => { + let elements: ChainablePromiseArray - const result = await toBeDisabled.call({ isNot: true }, el, { wait: 1 }) + beforeEach(async () => { + elements = await $$('sel') - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + elements.forEach(element => { + vi.mocked(element.isEnabled).mockResolvedValue(false) + }) + expect(elements).toHaveLength(2) + }) - test('message', async () => { - const el = await $('sel') - el.isEnabled = vi.fn().mockResolvedValue(true) + test('wait for success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toBeDisabled(elements, { beforeAssertion, afterAssertion, wait: 500 }) + + for (const element of elements) { + expect(element.isEnabled).toHaveBeenCalledExactlyOnceWith() + } + + expect(executeCommandBe).toHaveBeenCalledExactlyOnceWith(elements, expect.any(Function), + { + afterAssertion, + beforeAssertion, + wait: 500, + }, + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 500, interval: 100 }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeDisabled', + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeDisabled', + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - const result = await toBeDisabled.call({}, el) - expect(result.pass).toBe(false) - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) to be disabled + test('success with toBeDisabled and command options', async () => { + const result = await thisContext.toBeDisabled(elements) -Expected: "disabled" -Received: "not disabled"` - ) + elements.forEach(element => { + expect(element.isEnabled).toHaveBeenCalledExactlyOnceWith() + }) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 1, interval: 100 }) + expect(result.pass).toBe(true) + }) + + test('wait but failure', async () => { + vi.mocked(elements[0].isEnabled).mockRejectedValue(new Error('some error')) + + await expect(() => thisContext.toBeDisabled(elements)) + .rejects.toThrow('some error') + }) + + test('success on the first attempt', async () => { + const result = await thisContext.toBeDisabled(elements) + + expect(result.pass).toBe(true) + elements.forEach(element => { + expect(element.isEnabled).toHaveBeenCalledTimes(1) + }) + }) + + test('no wait - failure', async () => { + vi.mocked(elements[0].isEnabled).mockResolvedValue(true) + + const result = await thisContext.toBeDisabled(elements, { wait: 0 }) + + expect(result.pass).toBe(false) + expect(elements[0].isEnabled).toHaveBeenCalledTimes(1) + expect(elements[1].isEnabled).toHaveBeenCalledTimes(1) + }) + + test('no wait - success', async () => { + const result = await thisContext.toBeDisabled(elements) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 1, + interval: 100, + }) + elements.forEach(element => { + expect(element.isEnabled).toHaveBeenCalledExactlyOnceWith() + }) + expect(result.pass).toBe(true) + }) + + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toBeDisabled(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to be disabled + +- Expected - 2 ++ Received + 2 + + Array [ +- "not disabled", +- "not disabled", ++ "disabled", ++ "disabled", + ]` + ) + }) + + test('not - success - pass should be false', async () => { + elements.forEach(element => { + vi.mocked(element.isEnabled).mockResolvedValue(true) + }) + + const result = await thisNotContext.toBeDisabled(elements) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure (with wait) - pass should be true', async () => { + const result = await thisNotContext.toBeDisabled(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + }) + + test('not - success (with wait) - pass should be false', async () => { + elements.forEach(element => { + vi.mocked(element.isEnabled).mockResolvedValueOnce(false) + vi.mocked(element.isEnabled).mockResolvedValueOnce(false) + vi.mocked(element.isEnabled).mockResolvedValue(true) + }) + + const result = await thisNotContext.toBeDisabled(elements, { wait: 500 }) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), true, { + wait: 500, + interval: 100, + }) + expect(elements[0].isEnabled).toHaveBeenCalledTimes(3) + expect(elements[1].isEnabled).toHaveBeenCalledTimes(3) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('message when both elements fail', async () => { + const elements = await $$('sel') + + elements.forEach(element => { + vi.mocked(element.isEnabled).mockResolvedValue(true) + }) + + const result = await thisContext.toBeDisabled(elements) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to be disabled + +- Expected - 2 ++ Received + 2 + + Array [ +- "disabled", +- "disabled", ++ "not disabled", ++ "not disabled", + ]`) + }) + + test('message when a single element fails', async () => { + vi.mocked(elements[0].isEnabled).mockResolvedValue(true) + + const result = await thisContext.toBeDisabled(elements) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to be disabled + +- Expected - 1 ++ Received + 1 + + Array [ +- "disabled", ++ "not disabled", + "disabled", + ]`) + }) }) }) diff --git a/test/matchers/element/toBeDisplayed.test.ts b/test/matchers/element/toBeDisplayed.test.ts index 250ead0bd..56270aa6c 100644 --- a/test/matchers/element/toBeDisplayed.test.ts +++ b/test/matchers/element/toBeDisplayed.test.ts @@ -1,192 +1,565 @@ -import { vi, test, describe, expect } from 'vitest' -import { $ } from '@wdio/globals' +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' import { toBeDisplayed } from '../../../src/matchers/element/toBeDisplayed.js' -import { executeCommandBe } from '../../../src/utils.js' -import { DEFAULT_OPTIONS } from '../../../src/constants.js' +import { executeCommandBe, waitUntil } from '../../../src/utils.js' +import { notFoundElementFactory } from '../../__mocks__/@wdio/globals.js' vi.mock('@wdio/globals') -vi.mock('../../../src/utils.js', async (importOriginal) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports - const actual = await importOriginal() - return { - ...actual, - executeCommandBe: vi.fn(actual.executeCommandBe) - } -}) -describe('toBeDisplayed', () => { - /** - * result is inverted for toBeDisplayed because it inverts isEnabled result - * `!await el.isEnabled()` - */ - test('wait for success', async () => { - const el = await $('sel') - el.isDisplayed = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValueOnce(true) - - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - - const result = await toBeDisplayed.call({}, el, { beforeAssertion, afterAssertion }) - - expect(el.isDisplayed).toHaveBeenCalledWith( - { - withinViewport: false, - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true - } - ) - expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ - wait: DEFAULT_OPTIONS.wait, - interval: DEFAULT_OPTIONS.interval - })) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toBeDisplayed', - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toBeDisplayed', - options: { beforeAssertion, afterAssertion }, - result - }) - }) +describe(toBeDisplayed, async () => { + let thisContext: { toBeDisplayed: typeof toBeDisplayed } + let thisNotContext: { isNot: true; toBeDisplayed: typeof toBeDisplayed } + + beforeEach(async () => { + thisContext = { toBeDisplayed } + thisNotContext = { isNot: true, toBeDisplayed } - test('success with ToBeDisplayed and command options', async () => { - const el = await $('sel') - - const result = await toBeDisplayed.call({}, el, { wait: 1, withinViewport: true }) - - expect(el.isDisplayed).toHaveBeenCalledWith( - { - withinViewport: true, - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true - } - ) - expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ - wait: 1, - interval: DEFAULT_OPTIONS.interval - })) - expect(result.pass).toBe(true) }) - test('wait but failure', async () => { - const el = await $('sel') + describe.each([ + { element: await $('sel'), title: 'awaited ChainablePromiseElement' }, + { element: await $('sel').getElement(), title: 'awaited getElement of ChainablePromiseElement (e.g. WebdriverIO.Element)' }, + { element: $('sel'), title: 'non-awaited of ChainablePromiseElement' } + ])('given a single element when $title', ({ element: el }) => { + let element: ChainablePromiseElement | WebdriverIO.Element - el.isDisplayed = vi.fn().mockRejectedValue(new Error('some error')) + beforeEach(async () => { + thisContext = { toBeDisplayed } + thisNotContext = { isNot: true, toBeDisplayed } - await expect(() => toBeDisplayed.call({}, el)) - .rejects.toThrow('some error') - }) + element = el + vi.mocked(element.isDisplayed).mockResolvedValue(true) + }) - test('success on the first attempt', async () => { - const el = await $('sel') + test('wait for success', async () => { + vi.mocked(element.isDisplayed).mockResolvedValueOnce(false).mockResolvedValueOnce(true) + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toBeDisplayed(element, { beforeAssertion, afterAssertion, wait: 500 }) + + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(executeCommandBe).toHaveBeenCalledExactlyOnceWith(element, expect.any(Function), + { + beforeAssertion: beforeAssertion, + afterAssertion: afterAssertion, + interval: 100, + wait: 500, + }, + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 500, + interval: 100, + }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeDisplayed', + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeDisplayed', + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - const result = await toBeDisplayed.call({}, el) - expect(result.pass).toBe(true) - expect(el.isDisplayed).toHaveBeenCalledTimes(1) - }) + test('success with ToBeDisplayed and command options', async () => { + const result = await thisContext.toBeDisplayed(element, { wait: 1, withinViewport: true }) + + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: true, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 1, + interval: 100, + }) + expect(result.pass).toBe(true) + }) - test('no wait - failure', async () => { - const el = await $('sel') - el.isDisplayed = vi.fn().mockResolvedValue(false) + test('wait but throws', async () => { + vi.mocked(element.isDisplayed).mockRejectedValue(new Error('some error')) - const result = await toBeDisplayed.call({}, el, { wait: 0 }) + await expect(() => thisContext.toBeDisplayed(element)) + .rejects.toThrow('some error') + }) - expect(result.pass).toBe(false) - expect(el.isDisplayed).toHaveBeenCalledTimes(1) - }) + test('success on the first attempt', async () => { + const result = await thisContext.toBeDisplayed(element) - test('no wait - success', async () => { - const el = await $('sel') - - const result = await toBeDisplayed.call({}, el, { wait: 0 }) - - expect(el.isDisplayed).toHaveBeenCalledWith( - { - withinViewport: false, - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true - } - ) - expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ - wait: 0, - interval: DEFAULT_OPTIONS.interval - })) - - expect(result.pass).toBe(true) - expect(el.isDisplayed).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(element.isDisplayed).toHaveBeenCalledTimes(1) + }) - test('not - failure - pass must be true', async () => { - const el = await $('sel') - const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 0 }) + test('no wait - failure', async () => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - }) + const result = await thisContext.toBeDisplayed(element, { wait: 0 }) - test('not - success - pass should be false', async () => { - const el = await $('sel') + expect(result.pass).toBe(false) + expect(element.isDisplayed).toHaveBeenCalledTimes(1) + }) - el.isDisplayed = vi.fn().mockResolvedValue(false) + test('no wait - success', async () => { + const result = await thisContext.toBeDisplayed(element, { wait: 0 }) + + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 0, + interval: 100, + }) + + expect(result.pass).toBe(true) + expect(element.isDisplayed).toHaveBeenCalledTimes(1) + }) - const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 0 }) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toBeDisplayed(element) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to be displayed + +Expected: "not displayed" +Received: "displayed"`) + }) - test('not - failure (with wait) - pass should be true', async () => { - const el = await $('sel') - const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 1 }) + test('not - success - pass should be false', async () => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to be displayed + const result = await thisNotContext.toBeDisplayed(element) -Expected [not]: "not displayed" -Received : "displayed"` - ) - }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure (with wait) - pass should be true', async () => { + const result = await thisNotContext.toBeDisplayed(element) + + expect(result.pass).toBe(true) // success, boolean is inverted later because of `.not` + }) + + test('not - success (with wait) - pass should be false', async () => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) + + const result = await thisNotContext.toBeDisplayed(element) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), true, { + wait: 1, + interval: 100, + }) + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('message', async () => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) - test('not - success (with wait) - pass should be false', async () => { - const el = await $('sel') - - el.isDisplayed = vi.fn().mockResolvedValue(false) - - const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 1 }) - - expect(el.isDisplayed).toHaveBeenCalledWith( - { - withinViewport: false, - contentVisibilityAuto: true, - opacityProperty: true, - visibilityProperty: true - } - ) - expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ - wait: 1, - interval: DEFAULT_OPTIONS.interval - })) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + const result = await thisContext.toBeDisplayed(element) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to be displayed + +Expected: "displayed" +Received: "not displayed"`) + }) + + test('undefined - failure', async () => { + const element = undefined as unknown as WebdriverIO.Element + + const result = await thisContext.toBeDisplayed(element) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect undefined to be displayed + +Expected: "displayed" +Received: "not displayed"`) + }) }) - test('message', async () => { - const el = await $('sel') + describe.each([ + { elements: await $$('sel'), title: 'awaited ChainablePromiseArray' }, + { elements: await $$('sel').getElements(), title: 'awaited getElements of ChainablePromiseArray (e.g. WebdriverIO.ElementArray)' }, + { elements: await $$('sel').filter((t) => t.isEnabled()), title: 'awaited filtered ChainablePromiseArray (e.g. WebdriverIO.Element[])' }, + { elements: $$('sel'), title: 'non-awaited of ChainablePromiseArray' } + ])('given multiple elements when $title', ({ elements : els, title }) => { + let elements: ChainablePromiseArray | WebdriverIO.ElementArray | WebdriverIO.Element[] + let awaitedElements: typeof elements + + const selectorName = title.includes('filtered') ? '$(`sel`), $$(`sel`)[1]': '$$(`sel`)' + + beforeEach(async () => { + elements = els + + awaitedElements = await elements + awaitedElements.forEach((element) => { + vi.mocked(element.isDisplayed).mockResolvedValue(true) + }) + expect(awaitedElements).toHaveLength(2) + }) + + test('wait for success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toBeDisplayed(elements, { beforeAssertion, afterAssertion, wait: 500 }) + + awaitedElements.forEach((element) => { + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + }) + expect(executeCommandBe).toHaveBeenCalledExactlyOnceWith(elements, expect.any(Function), + { + beforeAssertion: beforeAssertion, + afterAssertion: afterAssertion, + interval: 100, + wait: 500, + }, + ) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 500, + interval: 100, + }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeDisplayed', + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeDisplayed', + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) + + test('success with ToBeDisplayed and command options', async () => { + const result = await thisContext.toBeDisplayed(elements, { wait: 1, withinViewport: true }) + + awaitedElements.forEach((element) => { + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: true, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + }) + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 1, + interval: 100, + }) + expect(result.pass).toBe(true) + }) + + test('wait but error', async () => { + vi.mocked(awaitedElements[0].isDisplayed).mockRejectedValue(new Error('some error')) + + await expect(() => thisContext.toBeDisplayed(elements)) + .rejects.toThrow('some error') + }) + + test('failure when no elements exist', async () => { + const noElementsFound: WebdriverIO.Element[] = [] + const result = await thisContext.toBeDisplayed(noElementsFound) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect [] to be displayed + +Expected: "at least one result" +Received: []`) + }) + + test('success on the first attempt', async () => { + const result = await thisContext.toBeDisplayed(elements) + + expect(result.pass).toBe(true) + awaitedElements.forEach((element) => { + expect(element.isDisplayed).toHaveBeenCalledTimes(1) + }) + }) + + test('no wait - failure', async () => { + vi.mocked(awaitedElements[0].isDisplayed).mockResolvedValue(false) + + const result = await thisContext.toBeDisplayed(elements, { wait: 0 }) + + expect(result.pass).toBe(false) + awaitedElements.forEach((element) => { + expect(element.isDisplayed).toHaveBeenCalledTimes(1) + }) + }) + + test('no wait - success', async () => { + const result = await thisContext.toBeDisplayed(elements, { wait: 0 }) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { + wait: 0, + interval: 100, + }) + awaitedElements.forEach((element) => { + expect(element.isDisplayed).toHaveBeenNthCalledWith(1, + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + }) + expect(result.pass).toBe(true) + }) + + test('not - failure - all elements - pass should be true', async () => { + const result = await thisNotContext.toBeDisplayed(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be displayed + +- Expected - 2 ++ Received + 2 + + Array [ +- "not displayed", +- "not displayed", ++ "displayed", ++ "displayed", + ]`) + }) + + test('not - failure when no elements - pass should be true', async () => { + const noElementsFound: WebdriverIO.Element[] = [] + + const result = await thisNotContext.toBeDisplayed(noElementsFound) + + expect(result.pass).toBe(true) // success, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect [] not to be displayed + +Expected: "at least one result" +Received: []`) + }) + + test('not - failure - when only first element is not displayed - pass should be true', async () => { + vi.mocked(awaitedElements[0].isDisplayed).mockResolvedValue(false) + vi.mocked(awaitedElements[1].isDisplayed).mockResolvedValue(true) + + const result = await thisNotContext.toBeDisplayed(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ + "not displayed", +- "not displayed", ++ "displayed", + ]`) + }) + + test('not - failure - when only second element is not displayed - pass should be true', async () => { + vi.mocked(awaitedElements[0].isDisplayed).mockResolvedValue(true) + vi.mocked(awaitedElements[1].isDisplayed).mockResolvedValue(false) + + const result = await thisNotContext.toBeDisplayed(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ +- "not displayed", ++ "displayed", + "not displayed", + ]`) + }) + + test('not - success - pass should be false', async () => { + awaitedElements.forEach((element) => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) + }) + + const result = await thisNotContext.toBeDisplayed(elements) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure (with wait) - pass should be true', async () => { + const result = await thisNotContext.toBeDisplayed(elements) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + }) + + test('not - success (with wait) - pass should be false', async () => { + awaitedElements.forEach((element) => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) + }) + + const result = await thisNotContext.toBeDisplayed(elements, { wait: 300 }) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), true, { + wait: 300, + interval: 100, + }) + awaitedElements.forEach((element) => { + expect(element.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('message when both elements fail', async () => { + awaitedElements.forEach((element) => { + vi.mocked(element.isDisplayed).mockResolvedValue(false) + }) + + const result = await thisContext.toBeDisplayed(elements) + + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be displayed + +- Expected - 2 ++ Received + 2 + + Array [ +- "displayed", +- "displayed", ++ "not displayed", ++ "not displayed", + ]`) + }) + + test('message when first element fails', async () => { + vi.mocked(awaitedElements[0].isDisplayed).mockResolvedValue(false) + vi.mocked(awaitedElements[1].isDisplayed).mockResolvedValue(true) - el.isDisplayed = vi.fn().mockResolvedValue(false) + const result = await thisContext.toBeDisplayed(elements) - const result = await toBeDisplayed.call({}, el, { wait: 0 }) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ +- "displayed", ++ "not displayed", + "displayed", + ]`) + }) + + test('message when second element fails', async () => { + vi.mocked(awaitedElements[0].isDisplayed).mockResolvedValue(true) + vi.mocked(awaitedElements[1].isDisplayed).mockResolvedValue(false) + + const result = await thisContext.toBeDisplayed(elements) + + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ + "displayed", +- "displayed", ++ "not displayed", + ]`) + }) + + test('message when no element fails', async () => { + const noElementsFound: WebdriverIO.Element[] = [] + + const result = await thisContext.toBeDisplayed(noElementsFound) + + expect(result.message()).toEqual(`\ +Expect [] to be displayed + +Expected: "at least one result" +Received: []`) + }) + }) + + test.for([ + { els: undefined, selectorName: 'undefined' }, + { els: null, selectorName: 'null' }, + { els: 0, selectorName: '0' }, + { els: 1, selectorName: '1' }, + { els: true, selectorName: 'true' }, + { els: false, selectorName: 'false' }, + { els: '', selectorName: '' }, + { els: 'test', selectorName: 'test' }, + { els: {}, selectorName: '{}' }, + { els: [1, 'test'], selectorName: '[1,"test"]' }, + { els: Promise.resolve(true), selectorName: 'true' } + ])('fails for %s', async ({ els, selectorName }) => { + const result = await thisContext.toBeDisplayed(els as any) expect(result.pass).toBe(false) expect(result.message()).toEqual(`\ -Expect $(\`sel\`) to be displayed +Expect ${selectorName} to be displayed Expected: "displayed" -Received: "not displayed"` - ) +Received: "not displayed"`) + }) + + describe('not found element', async () => { + let element: WebdriverIO.Element + + beforeEach(async () => { + element = notFoundElementFactory('sel') + }) + + test('throws error when an element does not exists', async () => { + await expect(thisContext.toBeDisplayed(element)).rejects.toThrow("Can't call isDisplayed on element with selector sel because element wasn't found") + }) }) }) diff --git a/test/matchers/element/toHaveAttribute.test.ts b/test/matchers/element/toHaveAttribute.test.ts index 42b21f627..2a09c4788 100644 --- a/test/matchers/element/toHaveAttribute.test.ts +++ b/test/matchers/element/toHaveAttribute.test.ts @@ -1,141 +1,435 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' -import { $ } from '@wdio/globals' +import { $, $$ } from '@wdio/globals' -import { getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' import { toHaveAttribute } from '../../../src/matchers/element/toHaveAttribute.js' -import type { AssertionResult } from 'expect-webdriverio' vi.mock('@wdio/globals') -describe('toHaveAttribute', () => { - let el: ChainablePromiseElement +describe(toHaveAttribute, () => { + let thisContext: { toHaveAttribute: typeof toHaveAttribute } + let thisIsNotContext: { isNot: boolean, toHaveAttribute: typeof toHaveAttribute } - beforeEach(async () => { - el = await $('sel') + beforeEach(() => { + thisContext = { toHaveAttribute } + thisIsNotContext = { isNot: true, toHaveAttribute } }) - describe('attribute exists', () => { - test('success when present', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - el.getAttribute = vi.fn().mockResolvedValue('Correct Value') + describe('given single element', () => { + let el: ChainablePromiseElement - const result = await toHaveAttribute.call({}, el, 'attribute_name', undefined, { beforeAssertion, afterAssertion }) + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getAttribute).mockResolvedValue('Correct Value') + }) + + describe('attribute exists', () => { + test('success when present', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', undefined, { beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveAttribute', + expectedValue: ['attribute_name', undefined], + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveAttribute', + expectedValue: ['attribute_name', undefined], + options: { beforeAssertion, afterAssertion }, + result + }) + }) + + test('not - failure when present for %s - pass should be true', async () => { + const result = await thisIsNotContext.toHaveAttribute(el, 'attribute_name') + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + }) + + test('not - failure when present for %s - pass should be true', async () => { + const result = await thisIsNotContext.toHaveAttribute(el, 'attribute_name', undefined) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveAttribute', - expectedValue: ['attribute_name', undefined], - options: { beforeAssertion, afterAssertion } + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveAttribute', - expectedValue: ['attribute_name', undefined], - options: { beforeAssertion, afterAssertion }, - result + + describe('message shows correctly', () => { + test('expect message', async () => { + vi.mocked(el.getAttribute).mockResolvedValue(null as unknown as string) + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', undefined) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have attribute attribute_name + +Expected: true +Received: false` + ) + }) }) }) - test('failure when not present', async () => { - el.getAttribute = vi.fn().mockResolvedValue(null) + describe('attribute has value', () => { + test('success with correct value', async () => { + const result = await thisContext.toHaveAttribute(el, 'attribute_name', 'Correct Value', { ignoreCase: true, wait: 1 }) - const result = await toHaveAttribute.call({}, el, 'attribute_name') + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(false) + test('success with RegExp and correct value', async () => { + const result = await thisContext.toHaveAttribute(el, 'attribute_name', /cOrReCt VaLuE/i) + + expect(result.pass).toBe(true) + }) + + test('failure with wrong value', async () => { + vi.mocked(el.getAttribute).mockResolvedValue('Wrong Value') + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', 'Correct Value', { ignoreCase: true, wait: 1 }) + + expect(result.pass).toBe(false) + }) + + test('failure with non-string attribute value as actual', async () => { + vi.mocked(el.getAttribute).mockResolvedValue(123 as unknown as string) + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', 'Correct Value', { ignoreCase: true, wait: 1 }) + + expect(result.pass).toBe(false) + }) + + test('failure with non-string attribute value as expected', async () => { + // @ts-expect-error invalid type + const result = await thisContext.toHaveAttribute(el, 'attribute_name', 123, { ignoreCase: true, wait: 1 }) + + expect(result.pass).toBe(false) + }) + + test.for([ + undefined, + null + ])('not - failure when present for %s - pass should be false', async ( attributeValue) => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + + const result = await thisIsNotContext.toHaveAttribute(el, 'attribute_name') + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + describe('message shows correctly', () => { + test('expect message', async () => { + vi.mocked(el.getAttribute).mockResolvedValue('Wrong') + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', 'Correct') + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have attribute attribute_name + +Expected: "Correct" +Received: "Wrong"` + ) + }) + }) + + describe('failure with RegExp, message shows correctly', () => { + test('expect message', async () => { + vi.mocked(el.getAttribute).mockResolvedValue('Wrong') + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', /WDIO/) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have attribute attribute_name + +Expected: /WDIO/ +Received: "Wrong"` + ) + }) + }) }) - describe('message shows correctly', () => { - let result: AssertionResult + describe('attribute does not exist or does not have a value', () => { + test.for([ + undefined, + null + ])('failure when not present for %s', async ( attributeValue) => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) - beforeEach(async () => { - el.getAttribute = vi.fn().mockResolvedValue(null) + const result = await thisContext.toHaveAttribute(el, 'attribute_name') - result = await toHaveAttribute.call({}, el, 'attribute_name') + expect(result.pass).toBe(false) }) - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have attribute') + + test.for([ + undefined, + null + ])('failure when not present for %s', async ( attributeValue) => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + + const result = await thisContext.toHaveAttribute(el, 'attribute_name', undefined) + + expect(result.pass).toBe(false) }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('true') + + test.for([ + undefined, + null + ])('not - success when not present for %s - pass should be false', async ( attributeValue) => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + + const result = await thisIsNotContext.toHaveAttribute(el, 'attribute_name') + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` }) - test('received message', () => { - expect(getReceived(result.message())).toContain('false') + + test.for([ + undefined, + null + ])('not - success when not present for %s - pass should be false', async ( attributeValue) => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + + const result = await thisIsNotContext.toHaveAttribute(el, 'attribute_name', undefined) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` }) }) }) - describe('attribute has value', () => { - test('success with correct value', async () => { - el.getAttribute = vi.fn().mockResolvedValue('Correct Value') + describe('given multiple elements', () => { + let els: ChainablePromiseArray - const result = await toHaveAttribute.call({}, el, 'attribute_name', 'Correct Value', { ignoreCase: true }) + beforeEach(async () => { + els = await $$('sel') - expect(result.pass).toBe(true) + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue('Correct Value') + }) + + expect(els).toHaveLength(2) }) - test('success with RegExp and correct value', async () => { - el.getAttribute = vi.fn().mockResolvedValue('Correct Value') - const result = await toHaveAttribute.call({}, el, 'attribute_name', /cOrReCt VaLuE/i) + describe('attribute exists', () => { + test('success when present', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - expect(result.pass).toBe(true) - }) - test('failure with wrong value', async () => { - el.getAttribute = vi.fn().mockResolvedValue('Wrong Value') + const result = await thisContext.toHaveAttribute(els, 'attribute_name', undefined, { beforeAssertion, afterAssertion }) - const result = await toHaveAttribute.call({}, el, 'attribute_name', 'Correct Value', { ignoreCase: true }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveAttribute', + expectedValue: ['attribute_name', undefined], + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveAttribute', + expectedValue: ['attribute_name', undefined], + options: { beforeAssertion, afterAssertion }, + result + }) + }) - expect(result.pass).toBe(false) - }) - test('failure with non-string attribute value as actual', async () => { - el.getAttribute = vi.fn().mockResolvedValue(123) + test('failure when not present', async () => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(null as unknown as string) + }) - const result = await toHaveAttribute.call({}, el, 'attribute_name', 'Correct Value', { ignoreCase: true }) + const result = await thisContext.toHaveAttribute(els, 'attribute_name', undefined) - expect(result.pass).toBe(false) - }) - test('failure with non-string attribute value as expected', async () => { - el.getAttribute = vi.fn().mockResolvedValue('Correct Value') + expect(result.pass).toBe(false) + }) - // @ts-expect-error invalid type - const result = await toHaveAttribute.call({}, el, 'attribute_name', 123, { ignoreCase: true }) + describe('message shows correctly', () => { - expect(result.pass).toBe(false) + test('expect message', async () => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(null as unknown as string) + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', undefined) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have attribute attribute_name + +Expected: true +Received: false` + ) + }) + }) }) - describe('message shows correctly', () => { - let result: AssertionResult - beforeEach(async () => { - el.getAttribute = vi.fn().mockResolvedValue('Wrong') + describe('attribute has value', () => { + test('success with correct value', async () => { + const result = await thisContext.toHaveAttribute(els, 'attribute_name', 'Correct Value', { ignoreCase: true, wait: 1 }) - result = await toHaveAttribute.call({}, el, 'attribute_name', 'Correct') + expect(result.pass).toBe(true) }) - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have attribute') + test('success with RegExp and correct value', async () => { + const result = await thisContext.toHaveAttribute(els, 'attribute_name', /cOrReCt VaLuE/i) + + expect(result.pass).toBe(true) }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('Correct') + test('failure with wrong value', async () => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue('Wrong Value') + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', 'Correct Value', { ignoreCase: true, wait: 1 }) + + expect(result.pass).toBe(false) }) - test('received message', () => { - expect(getReceived(result.message())).toContain('Wrong') + test('failure with non-string attribute value as actual', async () => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(123 as unknown as string) + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', 'Correct Value', { ignoreCase: true, wait: 1 }) + + expect(result.pass).toBe(false) + }) + test('failure with non-string attribute value as expected', async () => { + const result = await thisContext.toHaveAttribute(els, 'attribute_name', 123 as unknown as string, { ignoreCase: true, wait: 1 }) + + expect(result.pass).toBe(false) + }) + describe('message shows correctly', () => { + + test('expect message', async () => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue('Wrong') + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', 'Correct') + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have attribute attribute_name + +- Expected - 2 ++ Received + 2 + + Array [ +- "Correct", +- "Correct", ++ "Wrong", ++ "Wrong", + ]` + ) + }) + }) + describe('failure with RegExp, message shows correctly', () => { + + test('expect message', async () => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue('Wrong') + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', /WDIO/) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have attribute attribute_name + +- Expected - 2 ++ Received + 2 + + Array [ +- /WDIO/, +- /WDIO/, ++ "Wrong", ++ "Wrong", + ]` + ) + }) }) }) - describe('failure with RegExp, message shows correctly', () => { - let result: AssertionResult - beforeEach(async () => { - el.getAttribute = vi.fn().mockResolvedValue('Wrong') + describe('attribute does not exist or does not have a value', () => { + test.for([ + undefined, + null + ])('failure when not present for %s', async ( attributeValue) => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name') - result = await toHaveAttribute.call({}, el, 'attribute_name', /WDIO/) + expect(result.pass).toBe(false) }) - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have attribute') + + test.for([ + undefined, + null + ])('failure when not present for %s', async ( attributeValue) => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + }) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', undefined) + + expect(result.pass).toBe(false) + }) + + test.for([ + undefined, + null + ])('not - success when not present for %s - pass should be false', async ( attributeValue) => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + }) + + const result = await thisIsNotContext.toHaveAttribute(els, 'attribute_name') + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test.for([ + undefined, + null + ])('not - success when not present for %s - pass should be false', async ( attributeValue) => { + els.forEach(el => { + vi.mocked(el.getAttribute).mockResolvedValue(attributeValue as unknown as string) + }) + + const result = await thisIsNotContext.toHaveAttribute(els, 'attribute_name', undefined) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('/WDIO/') + + test('not - failure when one is present for %s - pass should be true', async () => { + vi.mocked(els[0].getAttribute).mockResolvedValue('Some Value') + vi.mocked(els[1].getAttribute).mockResolvedValue(null as unknown as string) + + const result = await thisIsNotContext.toHaveAttribute(els, 'attribute_name', undefined) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` }) - test('received message', () => { - expect(getReceived(result.message())).toContain('Wrong') + + test('failure when only one is present for %s', async () => { + vi.mocked(els[0].getAttribute).mockResolvedValue('Some Value') + vi.mocked(els[1].getAttribute).mockResolvedValue(null as unknown as string) + + const result = await thisContext.toHaveAttribute(els, 'attribute_name', undefined) + + expect(result.pass).toBe(false) }) }) + + test('fails when no elements are provided', async () => { + const result = await thisContext.toHaveAttribute([], 'attribute_name', 'some value') + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect [] to have attribute attribute_name + +Expected: "some value" +Received: undefined`) + }) }) }) diff --git a/test/matchers/element/toHaveChildren.test.ts b/test/matchers/element/toHaveChildren.test.ts index 4699cb5dc..772636b10 100644 --- a/test/matchers/element/toHaveChildren.test.ts +++ b/test/matchers/element/toHaveChildren.test.ts @@ -1,90 +1,449 @@ -import { vi, test, describe, expect } from 'vitest' -import { $ } from '@wdio/globals' +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' -import { toHaveChildren } from '../../../src/matchers/element/toHaveChildren.js' +import { toHaveChildren } from '../../../src/matchers/element/toHaveChildren' +import { waitUntil } from '../../../src/util/waitUntil' +import { chainableElementArrayFactory } from '../../__mocks__/@wdio/globals' +import type { ChainablePromiseArray } from 'webdriverio' vi.mock('@wdio/globals') -describe('toHaveChildren', () => { - test('no value', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const el = await $('sel') +describe(toHaveChildren, () => { + const thisContext = { toHaveChildren } + const thisNotContext = { isNot: true, toHaveChildren } - const result = await toHaveChildren.call({}, el, undefined, { wait: 0, beforeAssertion, afterAssertion }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveChildren', - options: { wait: 0, beforeAssertion, afterAssertion } + describe('given a single element', () => { + let el: ChainablePromiseElement + + beforeEach(async () => { + el = await $('sel') }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveChildren', - options: { wait: 0, beforeAssertion, afterAssertion }, - result + + test('no value - success - default to gte 1 and with command options', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveChildren(el, undefined, { wait: 0, interval: 5, beforeAssertion, afterAssertion }) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 0, interval: 5 }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { wait: 0, interval: 5, beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { wait: 0, interval: 5, beforeAssertion, afterAssertion }, + result + }) }) - }) - test('If no options passed in + children exists', async () => { - const el = await $('sel') + test('use numberOption wait and internal', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - const result = await toHaveChildren.call({}, el, {}) - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveChildren(el, { eq: 2, wait: 0, interval: 5 }, { beforeAssertion, afterAssertion } ) - test('exact number value', async () => { - const el = await $('sel') + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 0, interval: 5 }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { beforeAssertion, afterAssertion }, + expectedValue: { eq: 2, wait: 0, interval: 5 } - const result = await toHaveChildren.call({}, el, 2, { wait: 1 }) - expect(result.pass).toBe(true) - }) + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { beforeAssertion, afterAssertion }, + result, + expectedValue: { eq: 2, wait: 0, interval: 5 } + }) + }) - test('exact value', async () => { - const el = await $('sel') + test('success - If no options passed in + children exists', async () => { + const result = await thisContext.toHaveChildren(el) - const result = await toHaveChildren.call({}, el, { eq: 2, wait: 1 }) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + }) - test('gte value', async () => { - const el = await $('sel') + test('fails - If no options passed in + children do not exist', async () => { + vi.mocked(el.$$).mockReturnValueOnce(chainableElementArrayFactory('./child', 0)) - const result = await toHaveChildren.call({}, el, { gte: 2, wait: 1 }) - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveChildren(el, undefined) - test('exact value - failure', async () => { - const el = await $('sel') + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have children - const result = await toHaveChildren.call({}, el, { eq: 3, wait: 1 }) - expect(result.pass).toBe(false) - }) +Expected: ">= 1" +Received: 0` + ) + }) - test('lte value - failure', async () => { - const el = await $('sel') + test('exact number value', async () => { + const result = await thisContext.toHaveChildren(el, 2) - const result = await toHaveChildren.call({}, el, { lte: 1, wait: 0 }) - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(true) + }) + + test('exact value', async () => { + const result = await thisContext.toHaveChildren(el, { eq: 2, wait: 1 }) + + expect(result.pass).toBe(true) + }) + + test('gte value', async () => { + const result = await thisContext.toHaveChildren(el, { gte: 2 }) + + expect(result.pass).toBe(true) + }) + + test('exact value - failure', async () => { + const result = await thisContext.toHaveChildren(el, { eq: 3 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have children + +Expected: 3 +Received: 2` + ) + }) + + test('lte value - failure', async () => { + const result = await thisContext.toHaveChildren(el, { lte: 1 }) - test('.not exact value - failure - pass should be true', async () => { - const el = await $('sel') + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have children - const result = await toHaveChildren.bind({ isNot: true })(el, { eq: 2, wait: 0 }) +Expected: "<= 1" +Received: 2` + ) + }) + + test('.not, exact value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(el, { eq: 2 }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to have children Expected [not]: 2 -Received : 2`) +Received : 2` + ) + }) + + // This is not outputting the right colors in the test output console, to enhance! + test('.not, lte value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(el, { lte: 2 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to have children + +Expected [not]: "<= 2" +Received : 2` + ) + }) + + test('.not, exact value - success - pass should be false', async () => { + const result = await thisNotContext.toHaveChildren(el, { eq: 3 }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) }) - test('.not exact value - success - pass should be false', async () => { - const el = await $('sel') + describe('given a multiple elements', () => { + let elements: ChainablePromiseArray + + beforeEach(async () => { + elements = await $$('sel') + }) + + describe('given a single expected value', () => { + test('no value - success - default to gte 1 and with command options', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveChildren(elements, undefined, { wait: 0, interval: 5, beforeAssertion, afterAssertion }) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 0, interval: 5 }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { wait: 0, interval: 5, beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { wait: 0, interval: 5, beforeAssertion, afterAssertion }, + result + }) + }) + + test('use numberOption wait and internal', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveChildren(elements, { eq: 2, wait: 0, interval: 5 }, { beforeAssertion, afterAssertion } ) + + expect(waitUntil).toHaveBeenCalledExactlyOnceWith(expect.any(Function), undefined, { wait: 0, interval: 5 }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { beforeAssertion, afterAssertion }, + expectedValue: { eq: 2, wait: 0, interval: 5 } + + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveChildren', + options: { beforeAssertion, afterAssertion }, + result, + expectedValue: { eq: 2, wait: 0, interval: 5 } + }) + }) + + test('success - If no options passed in + children exists', async () => { + const result = await thisContext.toHaveChildren(elements) + expect(result.pass).toBe(true) + }) + + // TODO failure message show 2 expected missing while only one should, to enhance later + test('fails - If no options passed in + children do not exist', async () => { + vi.mocked(elements[0].$$).mockReturnValueOnce(chainableElementArrayFactory('./child', 0)) + + const result = await thisContext.toHaveChildren(elements, undefined) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have children + +- Expected - 2 ++ Received + 2 + + Array [ +- ">= 1", +- ">= 1", ++ 0, ++ 2, + ]` + ) + }) + + test('exact number value', async () => { + const result = await thisContext.toHaveChildren(elements, 2) - const result = await toHaveChildren.bind({ isNot: true })(el, { eq: 3, wait: 1 }) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + test('exact value', async () => { + const result = await thisContext.toHaveChildren(elements, { eq: 2, wait: 1 }) + + expect(result.pass).toBe(true) + }) + + test('gte value', async () => { + const result = await thisContext.toHaveChildren(elements, { gte: 2 }) + + expect(result.pass).toBe(true) + }) + + test('exact value - failure', async () => { + const result = await thisContext.toHaveChildren(elements, { eq: 3 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have children + +- Expected - 2 ++ Received + 2 + + Array [ +- 3, +- 3, ++ 2, ++ 2, + ]` + ) + }) + + test('lte value - failure', async () => { + const result = await thisContext.toHaveChildren(elements, { lte: 1 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have children + +- Expected - 2 ++ Received + 2 + + Array [ +- "<= 1", +- "<= 1", ++ 2, ++ 2, + ]` + ) + }) + + test('.not, exact value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(elements, { eq: 2 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have children + +Expected [not]: [2, 2] +Received : [2, 2]` + ) + }) + + test('.not, lte value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(elements, { lte: 2 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have children + +Expected [not]: ["<= 2", "<= 2"] +Received : [2, 2]` + ) + }) + test('.not, exact value - success - pass should be false', async () => { + const result = await thisNotContext.toHaveChildren(elements, { eq: 3 }) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + }) + + describe('given a multiple expected value', () => { + test('exact number value', async () => { + const result = await thisContext.toHaveChildren(elements, [2, 2]) + + expect(result.pass).toBe(true) + }) + + test('exact value', async () => { + const result = await thisContext.toHaveChildren(elements, [{ eq: 2 }, { eq: 2 }]) + + expect(result.pass).toBe(true) + }) + + test('gte value', async () => { + const result = await thisContext.toHaveChildren(elements, [{ gte: 2 }, { gte: 2 }]) + + expect(result.pass).toBe(true) + }) + + test('gte & lte value', async () => { + const result = await thisContext.toHaveChildren(elements, [{ gte: 2 }, { lte: 2 }]) + + expect(result.pass).toBe(true) + }) + + test('exact value - failure', async () => { + const result = await thisContext.toHaveChildren(elements, [{ eq: 3 }, { eq: 3 }]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have children + +- Expected - 2 ++ Received + 2 + + Array [ +- 3, +- 3, ++ 2, ++ 2, + ]` + ) + }) + + test('lte value - failure', async () => { + const result = await thisContext.toHaveChildren(elements, [{ lte: 1 }, { lte: 1 }]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have children + +- Expected - 2 ++ Received + 2 + + Array [ +- "<= 1", +- "<= 1", ++ 2, ++ 2, + ]` + ) + }) + + test('lte & gte value - failure', async () => { + const result = await thisContext.toHaveChildren(elements, [{ lte: 1 }, { gte: 1 }]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have children + +- Expected - 2 ++ Received + 2 + + Array [ +- "<= 1", +- ">= 1", ++ 2, ++ 2, + ]` + ) + }) + + test('.not, exact value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(elements, [{ eq: 2 }, { eq: 2 }]) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have children + +Expected [not]: [2, 2] +Received : [2, 2]`) + }) + + test('.not, lte value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(elements, [{ lte: 2 }, { lte: 2 }]) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have children + +Expected [not]: ["<= 2", "<= 2"] +Received : [2, 2]`) + }) + + test('.not, lte & gte value - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(elements, [{ lte: 2 }, { gte: 2 }]) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have children + +Expected [not]: ["<= 2", ">= 2"] +Received : [2, 2]`) + }) + + test('.not, exact value - success - pass should be false', async () => { + const result = await thisNotContext.toHaveChildren(elements, [{ eq: 3 }, { eq: 3 }]) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('.not, exact value on one element - success - pass should be true', async () => { + const result = await thisNotContext.toHaveChildren(elements, [{ eq: 2 }, { eq: 3 }]) + + expect(result.pass).toBe(true) // success, boolean is inverted later because of `.not` + }) + }) }) }) diff --git a/test/matchers/element/toHaveComputedLabel.test.ts b/test/matchers/element/toHaveComputedLabel.test.ts index 02f95a5b2..ab73c72c1 100644 --- a/test/matchers/element/toHaveComputedLabel.test.ts +++ b/test/matchers/element/toHaveComputedLabel.test.ts @@ -1,288 +1,273 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' - -import { getExpectMessage, getReceived, getExpected } from '../../__fixtures__/utils.js' import { toHaveComputedLabel } from '../../../src/matchers/element/toHaveComputedLabel.js' vi.mock('@wdio/globals') -describe('toHaveComputedLabel', () => { - test('wait for success', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValueOnce('') - .mockResolvedValueOnce('') - .mockResolvedValueOnce('WebdriverIO') - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - - const result = await toHaveComputedLabel.call({}, el, 'WebdriverIO', { ignoreCase: true, beforeAssertion, afterAssertion }) - - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(3) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveComputedLabel', - expectedValue: 'WebdriverIO', - options: { ignoreCase: true, beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveComputedLabel', - expectedValue: 'WebdriverIO', - options: { ignoreCase: true, beforeAssertion, afterAssertion }, - result - }) +describe(toHaveComputedLabel, () => { + let thisContext: { toHaveComputedLabel: typeof toHaveComputedLabel } + let thisNotContext: { isNot: true; toHaveComputedLabel: typeof toHaveComputedLabel } + + beforeEach(async () => { + thisContext = { toHaveComputedLabel } + thisNotContext = { isNot: true, toHaveComputedLabel } }) + describe('given a single element', () => { + let el: ChainablePromiseElement - test('wait but failure', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockRejectedValue(new Error('some error')) + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getComputedLabel).mockResolvedValue('WebdriverIO') + }) - await expect(() => toHaveComputedLabel.call({}, el, 'WebdriverIO', { ignoreCase: true })) - .rejects.toThrow('some error') - }) + test('wait for success', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValueOnce('') + .mockResolvedValueOnce('') + .mockResolvedValueOnce('WebdriverIO') + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - test('success on the first attempt', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + const result = await thisContext.toHaveComputedLabel(el, 'WebdriverIO', { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }) - const result = await toHaveComputedLabel.call({}, el, 'WebdriverIO', { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(3) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveComputedLabel', + expectedValue: 'WebdriverIO', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveComputedLabel', + expectedValue: 'WebdriverIO', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - test('no wait - failure', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('wait but failure', async () => { + vi.mocked(el.getComputedLabel).mockRejectedValue(new Error('some error')) - const result = await toHaveComputedLabel.call({}, el, 'foo', { wait: 0 }) - expect(result.pass).toBe(false) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) + await expect(() => thisContext.toHaveComputedLabel(el, 'WebdriverIO', { ignoreCase: true, wait: 1 })) + .rejects.toThrow('some error') + }) - test('no wait - success', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('success on the first attempt', async () => { + const result = await thisContext.toHaveComputedLabel(el, 'WebdriverIO', { ignoreCase: true, wait: 1 }) - const result = await toHaveComputedLabel.call({}, el, 'WebdriverIO', { wait: 0 }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) + }) - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('no wait - failure', async () => { + const result = await thisContext.toHaveComputedLabel(el, 'foo', { wait: 0 }) + + expect(result.pass).toBe(false) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) + }) + + test('no wait - success', async () => { + const result = await thisContext.toHaveComputedLabel(el, 'WebdriverIO', { wait: 0 }) + + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) + }) - const result = await toHaveComputedLabel.call({ isNot: true }, el, 'WebdriverIO', { wait: 0 }) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveComputedLabel(el, 'WebdriverIO', { wait: 0 }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to have computed label Expected [not]: "WebdriverIO" Received : "WebdriverIO"` - ) - }) - - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + ) + }) - const result = await toHaveComputedLabel.call({ isNot: true }, el, 'not WebdriverIO', { wait: 0 }) + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveComputedLabel(el, 'foobar') - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - test('should return true if computed labels match', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual computed label + single replacer matches the expected computed label', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue('WebdriverIO') - const result = await toHaveComputedLabel.bind({})(el, 'WebdriverIO', { wait: 1 }) - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveComputedLabel(el, 'BrowserdriverIO', { + replace: ['Web', 'Browser'], + wait: 1, + }) + expect(result.pass).toBe(true) + }) - test('should return true if actual computed label + single replacer matches the expected computed label', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual computed label + replace (string) matches the expected computed label', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue('WebdriverIO') - const result = await toHaveComputedLabel.bind({})(el, 'BrowserdriverIO', { - replace: ['Web', 'Browser'], + const result = await thisContext.toHaveComputedLabel(el, 'BrowserdriverIO', { + replace: [['Web', 'Browser']], + wait: 1, + }) + expect(result.pass).toBe(true) }) - expect(result.pass).toBe(true) - }) - - test('should return true if actual computed label + replace (string) matches the expected computed label', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual computed label + replace (regex) matches the expected computed label', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue('WebdriverIO') - const result = await toHaveComputedLabel.bind({})(el, 'BrowserdriverIO', { - replace: [['Web', 'Browser']], + const result = await thisContext.toHaveComputedLabel(el, 'BrowserdriverIO', { + replace: [[/Web/, 'Browser']], + wait: 1, + }) + expect(result.pass).toBe(true) }) - expect(result.pass).toBe(true) - }) - test('should return true if actual computed label + replace (regex) matches the expected computed label', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual computed label starts with expected computed label', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue('WebdriverIO') - const result = await toHaveComputedLabel.bind({})(el, 'BrowserdriverIO', { - replace: [[/Web/, 'Browser']], + const result = await thisContext.toHaveComputedLabel(el, 'Webd', { atStart: true, wait: 1 }) + expect(result.pass).toBe(true) }) - expect(result.pass).toBe(true) - }) - test('should return true if actual computed label starts with expected computed label', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual computed label ends with expected computed label', async () => { + const result = await thisContext.toHaveComputedLabel(el, 'erIO', { atEnd: true, wait: 1 }) - const result = await toHaveComputedLabel.bind({})(el, 'Webd', { atStart: true }) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + }) - test('should return true if actual computed label ends with expected computed label', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual computed label contains the expected computed label at the given index', async () => { + const result = await thisContext.toHaveComputedLabel(el, 'iver', { atIndex: 5, wait: 1 }) - const result = await toHaveComputedLabel.bind({})(el, 'erIO', { atEnd: true }) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + }) - test('should return true if actual computed label contains the expected computed label at the given index', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('message', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue('') - const result = await toHaveComputedLabel.bind({})(el, 'iver', { atIndex: 5 }) - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveComputedLabel(el, 'WebdriverIO') - test('message', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('') + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have computed label - const result = await toHaveComputedLabel.call({}, el, 'WebdriverIO') +Expected: "WebdriverIO" +Received: ""`) + }) - expect(getExpectMessage(result.message())).toContain('to have computed label') - }) + test('success if array matches with computed label and ignoreCase', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', 'WebdriverIO'], { ignoreCase: true, wait: 1 }) - test('success if array matches with computed label and ignoreCase', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) + }) - const result = await toHaveComputedLabel.call({}, el, ['div', 'WebdriverIO'], { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) + test('success if array matches with computed label and trim', async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue(' WebdriverIO ') - test('success if array matches with computed label and trim', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue(' WebdriverIO ') + const result = await thisContext.toHaveComputedLabel(el, ['div', 'WebdriverIO', 'toto'], { + trim: true, + wait: 1, + }) - const result = await toHaveComputedLabel.call({}, el, ['div', 'WebdriverIO', 'toto'], { - trim: true, + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) + test('success if array matches with computed label and replace (string)', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', 'BrowserdriverIO', 'toto'], { + replace: [['Web', 'Browser']], + wait: 1, + }) + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) + }) - test('success if array matches with computed label and replace (string)', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('success if array matches with computed label and replace (regex)', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', 'BrowserdriverIO', 'toto'], { + replace: [[/Web/g, 'Browser']], + wait: 1, + }) - const result = await toHaveComputedLabel.call({}, el, ['div', 'BrowserdriverIO', 'toto'], { - replace: [['Web', 'Browser']], + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) - test('success if array matches with computed label and replace (regex)', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('success if array matches with computed label and multiple replacers and one of the replacers is a function', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', 'browserdriverio', 'toto'], { + replace: [ + [/Web/g, 'Browser'], + [/[A-Z]/g, (match: string) => match.toLowerCase()], + ], + wait: 1, + }) - const result = await toHaveComputedLabel.call({}, el, ['div', 'BrowserdriverIO', 'toto'], { - replace: [[/Web/g, 'Browser']], + expect(result.pass).toBe(true) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) - test('success if array matches with computed label and multiple replacers and one of the replacers is a function', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + test('failure if array does not match with computed label', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', 'foo']) - const result = await toHaveComputedLabel.call({}, el, ['div', 'browserdriverio', 'toto'], { - replace: [ - [/Web/g, 'Browser'], - [/[A-Z]/g, (match: string) => match.toLowerCase()], - ], + expect(result.pass).toBe(false) + expect(el.getComputedLabel).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) - test('failure if array does not match with computed label', async () => { - const el = await $('sel') - el.getComputedLabel = vi.fn().mockResolvedValue('WebdriverIO') + describe('with RegExp', () => { + beforeEach(async () => { + vi.mocked(el.getComputedLabel).mockResolvedValue('This is example computed label') + }) + + test('success if match', async () => { + const result = await thisContext.toHaveComputedLabel(el, /ExAmplE/i) + expect(result.pass).toBe(true) + }) - const result = await toHaveComputedLabel.call({}, el, ['div', 'foo'], { wait: 1 }) - expect(result.pass).toBe(false) - expect(el.getComputedLabel).toHaveBeenCalledTimes(1) - }) + test('success if array matches with RegExp', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', /ExAmPlE/i]) + expect(result.pass).toBe(true) + }) - describe('with RegExp', () => { - let el: ChainablePromiseElement + test('success if array matches with computed label', async () => { + const result = await thisContext.toHaveComputedLabel(el, [ + 'This is example computed label', + /Webdriver/i, + ]) + expect(result.pass).toBe(true) + }) - beforeEach(async () => { - el = await $('sel') - el.getComputedLabel = vi.fn().mockImplementation(() => { - return 'This is example computed label' + test('success if array matches with computed label and ignoreCase', async () => { + const result = await thisContext.toHaveComputedLabel( + el, + ['ThIs Is ExAmPlE computed label', /Webdriver/i], + { + ignoreCase: true, + wait: 1, + } + ) + expect(result.pass).toBe(true) }) - }) - test('success if match', async () => { - const result = await toHaveComputedLabel.call({}, el, /ExAmplE/i) - expect(result.pass).toBe(true) - }) + test('failure if no match', async () => { + const result = await thisContext.toHaveComputedLabel(el, /Webdriver/i) - test('success if array matches with RegExp', async () => { - const result = await toHaveComputedLabel.call({}, el, ['div', /ExAmPlE/i]) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have computed label - test('success if array matches with computed label', async () => { - const result = await toHaveComputedLabel.call({}, el, [ - 'This is example computed label', - /Webdriver/i, - ]) - expect(result.pass).toBe(true) - }) +Expected: /Webdriver/i +Received: "This is example computed label"` + ) + }) - test('success if array matches with computed label and ignoreCase', async () => { - const result = await toHaveComputedLabel.call( - {}, - el, - ['ThIs Is ExAmPlE computed label', /Webdriver/i], - { - ignoreCase: true, - } - ) - expect(result.pass).toBe(true) - }) + test('failure if array does not match with computed label', async () => { + const result = await thisContext.toHaveComputedLabel(el, ['div', /Webdriver/i]) - test('failure if no match', async () => { - const result = await toHaveComputedLabel.call({}, el, /Webdriver/i) - expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have computed label') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getReceived(result.message())).toContain('This is example computed label') - }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have computed label - test('failure if array does not match with computed label', async () => { - const result = await toHaveComputedLabel.call({}, el, ['div', /Webdriver/i]) - expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have computed label') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getExpected(result.message())).toContain('div') +Expected: ["div", /Webdriver/i] +Received: "This is example computed label"` + ) + }) }) }) }) diff --git a/test/matchers/element/toHaveComputedRole.test.ts b/test/matchers/element/toHaveComputedRole.test.ts index f01df18af..9b77ee71f 100644 --- a/test/matchers/element/toHaveComputedRole.test.ts +++ b/test/matchers/element/toHaveComputedRole.test.ts @@ -1,290 +1,308 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' - -import { getExpectMessage, getReceived, getExpected } from '../../__fixtures__/utils.js' import { toHaveComputedRole } from '../../../src/matchers/element/toHaveComputedRole.js' vi.mock('@wdio/globals') -describe('toHaveComputedcomputed role', () => { - test('wait for success', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('').mockResolvedValueOnce('').mockResolvedValueOnce('WebdriverIO') - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - - const result = await toHaveComputedRole.call({}, el, 'WebdriverIO', { ignoreCase: true, beforeAssertion, afterAssertion }) - - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(3) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveComputedRole', - expectedValue: 'WebdriverIO', - options: { ignoreCase: true, beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveComputedRole', - expectedValue: 'WebdriverIO', - options: { ignoreCase: true, beforeAssertion, afterAssertion }, - result - }) - }) - - test('wait but failure', async () => { - const el = await $('sel') - - el.getComputedRole = vi.fn().mockRejectedValue(new Error('some error')) - - await expect(() => toHaveComputedRole.call({}, el, 'WebdriverIO', { ignoreCase: true })) - .rejects.toThrow('some error') - }) - - test('success on the first attempt', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') - - const result = await toHaveComputedRole.call({}, el, 'WebdriverIO', { ignoreCase: true }) +describe(toHaveComputedRole, () => { + let thisContext: { toHaveComputedRole: typeof toHaveComputedRole } + let thisNotContext: { isNot: true; toHaveComputedRole: typeof toHaveComputedRole } - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) + beforeEach(async () => { + thisContext = { toHaveComputedRole } + thisNotContext = { isNot: true, toHaveComputedRole } }) - test('no wait - failure', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + describe('given single element', () => { + let el: ChainablePromiseElement - const result = await toHaveComputedRole.call({}, el, 'foo', { wait: 0 }) + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValue('WebdriverIO') + }) - expect(result.pass).toBe(false) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + test('wait for success', async () => { + vi.mocked(el.getComputedRole).mockResolvedValueOnce('').mockResolvedValueOnce('WebdriverIO') + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - test('no wait - success', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + const result = await thisContext.toHaveComputedRole(el, 'WebdriverIO', { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }) - const result = await toHaveComputedRole.call({}, el, 'WebdriverIO', { wait: 0 }) + expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(2) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveComputedRole', + expectedValue: 'WebdriverIO', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveComputedRole', + expectedValue: 'WebdriverIO', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + test('wait but failure', async () => { + const el = await $('sel') - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + vi.mocked(el.getComputedRole).mockRejectedValue(new Error('some error')) - const result = await toHaveComputedRole.call({ isNot: true }, el, 'WebdriverIO', { wait: 0 }) + await expect(() => thisContext.toHaveComputedRole(el, 'WebdriverIO', { ignoreCase: true, wait: 1 })) + .rejects.toThrow('some error') + }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to have computed role + test('success on the first attempt', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') -Expected [not]: "WebdriverIO" -Received : "WebdriverIO"` - ) - }) + const result = await thisContext.toHaveComputedRole(el, 'WebdriverIO', { ignoreCase: true }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) + }) - const result = await toHaveComputedRole.call({ isNot: true }, el, 'not WebdriverIO', { wait: 0 }) + test('no wait - failure', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + const result = await thisContext.toHaveComputedRole(el, 'foo', { wait: 0 }) - test('should return true if computed roles match', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + expect(result.pass).toBe(false) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) + }) - const result = await toHaveComputedRole.bind({})(el, 'WebdriverIO', { wait: 1 }) - expect(result.pass).toBe(true) - }) + test('no wait - success', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - test('should return true if actual computed role + single replacer matches the expected computed role', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + const result = await thisContext.toHaveComputedRole(el, 'WebdriverIO', { wait: 0 }) - const result = await toHaveComputedRole.bind({})(el, 'BrowserdriverIO', { - replace: ['Web', 'Browser'], + expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - }) - test('should return true if actual computed role + replace (string) matches the expected computed role', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + test('not - failure - pass should be true', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - const result = await toHaveComputedRole.bind({})(el, 'BrowserdriverIO', { - replace: [['Web', 'Browser']], - }) - expect(result.pass).toBe(true) - }) + const result = await thisNotContext.toHaveComputedRole(el, 'WebdriverIO') - test('should return true if actual computed role + replace (regex) matches the expected computed role', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to have computed role - const result = await toHaveComputedRole.bind({})(el, 'BrowserdriverIO', { - replace: [[/Web/, 'Browser']], +Expected [not]: "WebdriverIO" +Received : "WebdriverIO"` + ) }) - expect(result.pass).toBe(true) - }) - test('should return true if actual computed role starts with expected computed role', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + test('not - success - pass should be false', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - const result = await toHaveComputedRole.bind({})(el, 'Webd', { atStart: true }) - expect(result.pass).toBe(true) - }) + const result = await thisNotContext.toHaveComputedRole(el, 'foobar') - test('should return true if actual computed role ends with expected computed role', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - const result = await toHaveComputedRole.bind({})(el, 'erIO', { atEnd: true }) - expect(result.pass).toBe(true) - }) + test('should return true if actual computed role + single replacer matches the expected computed role', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - test('should return true if actual computed role contains the expected computed role at the given index', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + const result = await thisContext.toHaveComputedRole(el, 'BrowserdriverIO', { + replace: ['Web', 'Browser'], + }) + expect(result.pass).toBe(true) + }) - const result = await toHaveComputedRole.bind({})(el, 'iver', { atIndex: 5 }) - expect(result.pass).toBe(true) - }) + test('should return true if actual computed role + replace (string) matches the expected computed role', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - test('message', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('') + const result = await thisContext.toHaveComputedRole(el, 'BrowserdriverIO', { + replace: [['Web', 'Browser']], + }) + expect(result.pass).toBe(true) + }) - const result = await toHaveComputedRole.call({}, el, 'WebdriverIO') + test('should return true if actual computed role + replace (regex) matches the expected computed role', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - expect(getExpectMessage(result.message())).toContain('to have computed role') - }) + const result = await thisContext.toHaveComputedRole(el, 'BrowserdriverIO', { + replace: [[/Web/, 'Browser']], + }) + expect(result.pass).toBe(true) + }) - test('success if array matches with computed role and ignoreCase', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + test('should return true if actual computed role starts with expected computed role', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - const result = await toHaveComputedRole.call({}, el, ['div', 'WebdriverIO'], { ignoreCase: true }) + const result = await thisContext.toHaveComputedRole(el, 'Webd', { atStart: true }) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + test('should return true if actual computed role ends with expected computed role', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - test('success if array matches with computed role and trim', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce(' WebdriverIO ') + const result = await thisContext.toHaveComputedRole(el, 'erIO', { atEnd: true }) + expect(result.pass).toBe(true) + }) - const result = await toHaveComputedRole.call({}, el, ['div', 'WebdriverIO', 'toto'], { - trim: true, + test('should return true if actual computed role contains the expected computed role at the given index', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') + const result = await thisContext.toHaveComputedRole(el, 'iver', { atIndex: 5 }) + expect(result.pass).toBe(true) }) - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + test('message', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('') + + const result = await thisContext.toHaveComputedRole(el, 'WebdriverIO') - test('success if array matches with computed role and replace (string)', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have computed role - const result = await toHaveComputedRole.call({}, el, ['div', 'BrowserdriverIO', 'toto'], { - replace: [['Web', 'Browser']], +Expected: "WebdriverIO" +Received: ""`) }) - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + test('success if array matches with computed role and ignoreCase', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - test('success if array matches with computed role and replace (regex)', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + const result = await thisContext.toHaveComputedRole(el, ['div', 'WebdriverIO'], { ignoreCase: true }) - const result = await toHaveComputedRole.call({}, el, ['div', 'BrowserdriverIO', 'toto'], { - replace: [[/Web/g, 'Browser']], + expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) - test('success if array matches with computed role and multiple replacers and one of the replacers is a function', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + test('success if array matches with computed role and trim', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce(' WebdriverIO ') - const result = await toHaveComputedRole.call({}, el, ['div', 'browserdriverio', 'toto'], { - replace: [ - [/Web/g, 'Browser'], - [/[A-Z]/g, (match: string) => match.toLowerCase()], - ], - }) - expect(result.pass).toBe(true) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + const result = await thisContext.toHaveComputedRole(el, ['div', 'WebdriverIO', 'toto'], { + trim: true, - test('failure if array does not match with computed role', async () => { - const el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValueOnce('WebdriverIO') + }) - const result = await toHaveComputedRole.call({}, el, ['div', 'foo'], { wait: 1 }) - expect(result.pass).toBe(false) - expect(el.getComputedRole).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) + }) - describe('with RegExp', () => { - let el: ChainablePromiseElement + test('success if array matches with computed role and replace (string)', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - beforeEach(async () => { - el = await $('sel') - el.getComputedRole = vi.fn().mockResolvedValue('This is example computed role') - }) + const result = await thisContext.toHaveComputedRole(el, ['div', 'BrowserdriverIO', 'toto'], { + replace: [['Web', 'Browser']], + }) - test('success if match', async () => { - const result = await toHaveComputedRole.call({}, el, /ExAmplE/i) expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) }) - test('success if array matches with RegExp', async () => { - const result = await toHaveComputedRole.call({}, el, ['div', /ExAmPlE/i]) - expect(result.pass).toBe(true) - }) + test('success if array matches with computed role and replace (regex)', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') - test('success if array matches with computed role', async () => { - const result = await toHaveComputedRole.call({}, el, [ - 'This is example computed role', - /Webdriver/i, - ]) + const result = await thisContext.toHaveComputedRole(el, ['div', 'BrowserdriverIO', 'toto'], { + replace: [[/Web/g, 'Browser']], + }) expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) }) - test('success if array matches with computed role and ignoreCase', async () => { - const result = await toHaveComputedRole.call( - {}, - el, - ['ThIs Is ExAmPlE computed role', /Webdriver/i], - { - ignoreCase: true, - } - ) + test('success if array matches with computed role and multiple replacers and one of the replacers is a function', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') + + const result = await thisContext.toHaveComputedRole(el, ['div', 'browserdriverio', 'toto'], { + replace: [ + [/Web/g, 'Browser'], + [/[A-Z]/g, (match: string) => match.toLowerCase()], + ], + }) expect(result.pass).toBe(true) + expect(el.getComputedRole).toHaveBeenCalledTimes(1) }) - test('failure if no match', async () => { - const result = await toHaveComputedRole.call({}, el, /Webdriver/i) + test('failure if array does not match with computed role', async () => { + const el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValueOnce('WebdriverIO') + + const result = await thisContext.toHaveComputedRole(el, ['div', 'foo']) expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have computed role') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getReceived(result.message())).toContain('This is example computed role') + expect(el.getComputedRole).toHaveBeenCalledTimes(1) }) - test('failure if array does not match with computed role', async () => { - const result = await toHaveComputedRole.call({}, el, ['div', /Webdriver/i]) - expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have computed role') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getExpected(result.message())).toContain('div') + describe('with RegExp', () => { + let el: ChainablePromiseElement + + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getComputedRole).mockResolvedValue('This is example computed role') + }) + + test('success if match', async () => { + const result = await thisContext.toHaveComputedRole(el, /ExAmplE/i) + expect(result.pass).toBe(true) + }) + + test('success if array matches with RegExp', async () => { + const result = await thisContext.toHaveComputedRole(el, ['div', /ExAmPlE/i]) + expect(result.pass).toBe(true) + }) + + test('success if array matches with computed role', async () => { + const result = await thisContext.toHaveComputedRole(el, [ + 'This is example computed role', + /Webdriver/i, + ]) + expect(result.pass).toBe(true) + }) + + test('success if array matches with computed role and ignoreCase', async () => { + const result = await thisContext.toHaveComputedRole( + el, + ['ThIs Is ExAmPlE computed role', /Webdriver/i], + { + wait: 1, + ignoreCase: true, + } + ) + expect(result.pass).toBe(true) + }) + + test('failure if no match', async () => { + const result = await thisContext.toHaveComputedRole(el, /Webdriver/i) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have computed role + +Expected: /Webdriver/i +Received: "This is example computed role"` + ) + }) + + test('failure if array does not match with computed role', async () => { + const result = await thisContext.toHaveComputedRole(el, ['div', /Webdriver/i]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have computed role + +Expected: ["div", /Webdriver/i] +Received: "This is example computed role"` + ) + }) }) }) }) diff --git a/test/matchers/element/toHaveElementClass.test.ts b/test/matchers/element/toHaveElementClass.test.ts index 13f3845bd..6b35fb062 100644 --- a/test/matchers/element/toHaveElementClass.test.ts +++ b/test/matchers/element/toHaveElementClass.test.ts @@ -1,147 +1,403 @@ -import { $ } from '@wdio/globals' +import { $, $$ } from '@wdio/globals' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' -import { toHaveElementClass } from '../../../src/matchers/element/toHaveClass.js' +import { toHaveElementClass } from '../../../src/matchers/element/toHaveElementClass.js' import type { AssertionResult } from 'expect-webdriverio' vi.mock('@wdio/globals') -describe('toHaveElementClass', () => { - let el: ChainablePromiseElement +describe(toHaveElementClass, () => { - beforeEach(async () => { - el = await $('sel') - el.getAttribute = vi.fn().mockImplementation((attribute: string) => { - if (attribute === 'class') { - return 'some-class another-class yet-another-class' - } - return null - }) - }) + let thisContext: { toHaveElementClass: typeof toHaveElementClass } + let thisNotContext: { isNot: true; toHaveElementClass: typeof toHaveElementClass } - test('success when class name is present', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const result = await toHaveElementClass.call({}, el, 'some-class', { beforeAssertion, afterAssertion }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveElementClass', - expectedValue: 'some-class', - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveElementClass', - expectedValue: 'some-class', - options: { beforeAssertion, afterAssertion }, - result - }) + beforeEach(() => { + thisContext = { toHaveElementClass } + thisNotContext = { isNot: true, toHaveElementClass } }) - test('success when including surrounding spaces and asymmetric matcher', async () => { - const result = await toHaveElementClass.call({}, el, expect.stringContaining('some-class ')) - expect(result.pass).toBe(true) - const result2 = await toHaveElementClass.call({}, el, expect.stringContaining(' another-class ')) - expect(result2.pass).toBe(true) - }) + describe('given a single element', () => { + let el: ChainablePromiseElement - test('success with RegExp when class name is present', async () => { - const result = await toHaveElementClass.call({}, el, /sOmE-cLaSs/i) - expect(result.pass).toBe(true) - }) - - test('success if array matches with class', async () => { - const result = await toHaveElementClass.call({}, el, ['some-class', 'yet-another-class']) - expect(result.pass).toBe(true) - }) + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getAttribute).mockImplementation(async (attribute: string) => { + if (attribute === 'class') { + return 'some-class another-class yet-another-class' + } + return null + }) + }) - test('failure if the classes do not match', async () => { - const result = await toHaveElementClass.call({}, el, 'someclass', { message: 'Not found!' }) - expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('Not found!') - }) + test('success when class name is present', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - test('failure if array does not match with class', async () => { - const result = await toHaveElementClass.call({}, el, ['someclass', 'anotherclass']) - expect(result.pass).toBe(false) - }) + const result = await thisContext.toHaveElementClass(el, 'some-class', { wait: 0, beforeAssertion, afterAssertion }) - describe('options', () => { - test('should fail when class is not a string', async () => { - el.getAttribute = vi.fn().mockImplementation(() => { - return null + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveElementClass', + expectedValue: 'some-class', + options: { beforeAssertion, afterAssertion, wait: 0 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveElementClass', + expectedValue: 'some-class', + options: { beforeAssertion, afterAssertion, wait: 0 }, + result }) - const result = await toHaveElementClass.call({}, el, 'some-class') - expect(result.pass).toBe(false) }) - test('should pass when trimming the attribute', async () => { - el.getAttribute = vi.fn().mockImplementation(() => { - return ' some-class ' - }) - const result = await toHaveElementClass.call({}, el, 'some-class', { trim: true }) + test('success when including surrounding spaces and asymmetric matcher', async () => { + const result = await thisContext.toHaveElementClass(el, expect.stringContaining('some-class ')) expect(result.pass).toBe(true) + + const result2 = await thisContext.toHaveElementClass(el, expect.stringContaining(' another-class ')) + expect(result2.pass).toBe(true) }) - test('should pass when ignore the case', async () => { - const result = await toHaveElementClass.call({}, el, 'sOme-ClAsS', { ignoreCase: true }) + test('success with multiple asymmetric matcher', async () => { + const result = await thisContext.toHaveElementClass(el, [expect.stringContaining('some-class'), expect.stringContaining('another-class')]) + expect(result.pass).toBe(true) }) - test('should pass if containing', async () => { - const result = await toHaveElementClass.call({}, el, 'some', { containing: true }) + test('failure with multiple asymmetric matcher', async () => { + const result = await thisContext.toHaveElementClass(el, [expect.stringContaining('notsome-class'), expect.stringContaining('notanother-class')]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have class + +Expected: [StringContaining "notsome-class", StringContaining "notanother-class"] +Received: "some-class another-class yet-another-class"` + ) + }) + + test('success with RegExp when class name is present', async () => { + const result = await thisContext.toHaveElementClass(el, /sOmE-cLaSs/i) + expect(result.pass).toBe(true) }) - test('should pass if array ignores the case', async () => { - const result = await toHaveElementClass.call({}, el, ['sOme-ClAsS', 'anOther-ClAsS'], { ignoreCase: true }) + test('success if array matches with class', async () => { + const result = await thisContext.toHaveElementClass(el, ['some-class', 'yet-another-class']) + expect(result.pass).toBe(true) }) - }) - describe('failure when class name is not present', () => { - let result: AssertionResult + test('failure if the classes do not match', async () => { + const result = await thisContext.toHaveElementClass(el, 'someclass', { wait: 0, message: 'Not found!' }) - beforeEach(async () => { - result = await toHaveElementClass.call({}, el, 'test') + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Not found! +Expect $(\`sel\`) to have class + +Expected: "someclass" +Received: "some-class another-class yet-another-class"`) }) - test('failure', () => { + test('failure if array does not match with class', async () => { + const result = await thisContext.toHaveElementClass(el, ['someclass', 'anotherclass']) + expect(result.pass).toBe(false) }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have class') + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveElementClass(el, ['not-class', 'not-another-class']) + + expect(result.pass).toBe(false) // success, boolean is inverted later + }) + + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveElementClass(el, ['some-class', 'not-another-class']) + + expect(result.pass).toBe(true) // failure, boolean is inverted later + }) + + describe('options', () => { + test('should fail when class is not a string', async () => { + vi.mocked(el.getAttribute).mockResolvedValue(null) + + const result = await thisContext.toHaveElementClass(el, 'some-class') + + expect(result.pass).toBe(false) + }) + + test('should pass when trimming the attribute', async () => { + vi.mocked(el.getAttribute).mockResolvedValue(' some-class ') + + const result = await thisContext.toHaveElementClass(el, 'some-class', { wait: 0, trim: true }) + + expect(result.pass).toBe(true) + }) + + test('should pass when ignore the case', async () => { + const result = await thisContext.toHaveElementClass(el, 'sOme-ClAsS', { wait: 0, ignoreCase: true }) + expect(result.pass).toBe(true) + }) + + test('should pass if containing', async () => { + const result = await thisContext.toHaveElementClass(el, 'some', { wait: 0, containing: true }) + expect(result.pass).toBe(true) + }) + + test('should pass if array ignores the case', async () => { + const result = await thisContext.toHaveElementClass(el, ['sOme-ClAsS', 'anOther-ClAsS'], { wait: 0, ignoreCase: true }) + expect(result.pass).toBe(true) + }) + }) + + describe('failure when class name is not present', () => { + let result: AssertionResult + + beforeEach(async () => { + result = await thisContext.toHaveElementClass(el, 'test') }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('test') + + test('failure', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have class + +Expected: "test" +Received: "some-class another-class yet-another-class"` ) }) - test('received message', () => { - expect(getReceived(result.message())).toContain('some-class another-class') + }) + + describe('failure with RegExp when class name is not present', () => { + let result: AssertionResult + + beforeEach(async () => { + result = await thisContext.toHaveElementClass(el, /WDIO/) + }) + + test('failure', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have class + +Expected: /WDIO/ +Received: "some-class another-class yet-another-class"` ) }) }) }) - describe('failure with RegExp when class name is not present', () => { - let result: AssertionResult + describe('given multiple elements', () => { + let elements: ChainablePromiseArray + const selectorName = '$$(`sel`)' beforeEach(async () => { - result = await toHaveElementClass.call({}, el, /WDIO/) + elements = await $$('sel') + + expect(elements).toHaveLength(2) + elements.forEach((el) => { + vi.mocked(el.getAttribute).mockImplementation(async (attribute: string) => { + if (attribute === 'class') { + return 'some-class another-class yet-another-class' + } + return null + }) + }) + }) + + test('success when class name is present', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveElementClass(elements, 'some-class', { wait: 0, beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveElementClass', + expectedValue: 'some-class', + options: { beforeAssertion, afterAssertion, wait: 0 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveElementClass', + expectedValue: 'some-class', + options: { beforeAssertion, afterAssertion, wait: 0 }, + result + }) + }) + + test('success when including surrounding spaces and asymmetric matcher', async () => { + const result = await thisContext.toHaveElementClass(elements, expect.stringContaining('some-class ')) + expect(result.pass).toBe(true) + + const result2 = await thisContext.toHaveElementClass(elements, expect.stringContaining(' another-class ')) + expect(result2.pass).toBe(true) + }) + + test('success with multiple asymmetric matcher', async () => { + const result = await thisContext.toHaveElementClass(elements, [expect.stringContaining('some-class'), expect.stringContaining('another-class')]) + + expect(result.pass).toBe(true) + }) + + test('failure with multiple asymmetric matcher', async () => { + const result = await thisContext.toHaveElementClass(elements, [expect.stringContaining('notsome-class'), expect.stringContaining('notanother-class')]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have class + +- Expected - 2 ++ Received + 2 + + Array [ +- StringContaining "notsome-class", +- StringContaining "notanother-class", ++ "some-class another-class yet-another-class", ++ "some-class another-class yet-another-class", + ]` + ) + }) + + test('not - failure with multiple asymmetric matcher - pass should be true', async () => { + const result = await thisNotContext.toHaveElementClass(elements, [expect.stringContaining('some-class'), expect.stringContaining('another-class')]) + + expect(result.pass).toBe(true) // failure, boolean is inverted later + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have class + +Expected [not]: [StringContaining "some-class", StringContaining "another-class"] +Received : ["some-class another-class yet-another-class", "some-class another-class yet-another-class"]` + ) + }) + + test('success with RegExp when class name is present', async () => { + const result = await thisContext.toHaveElementClass(elements, /sOmE-cLaSs/i) + + expect(result.pass).toBe(true) + }) + + test('success if array matches with class', async () => { + const result = await thisContext.toHaveElementClass(elements, ['some-class', 'yet-another-class']) + + expect(result.pass).toBe(true) + }) + + test('failure if the classes do not match', async () => { + const result = await thisContext.toHaveElementClass(elements, 'someclass', { wait: 0, message: 'Not found!' }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Not found! +Expect ${selectorName} to have class + +- Expected - 2 ++ Received + 2 + + Array [ +- "someclass", +- "someclass", ++ "some-class another-class yet-another-class", ++ "some-class another-class yet-another-class", + ]`) }) - test('failure', () => { + test('failure if array does not match with class', async () => { + const result = await thisContext.toHaveElementClass(elements, ['someclass', 'anotherclass']) + expect(result.pass).toBe(false) }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have class') + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveElementClass(elements, ['not-class', 'not-another-class']) + + expect(result.pass).toBe(false) // success, boolean is inverted later + }) + + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveElementClass(elements, ['some-class', 'not-another-class']) + + expect(result.pass).toBe(true) // failure, boolean is inverted later + }) + + describe('options', () => { + test('should fail when class is not a string', async () => { + elements.forEach((el) => { + vi.mocked(el.getAttribute).mockResolvedValue(null) + }) + + const result = await thisContext.toHaveElementClass(elements, 'some-class') + + expect(result.pass).toBe(false) + }) + + test('should pass when trimming the attribute', async () => { + elements.forEach((el) => { + vi.mocked(el.getAttribute).mockResolvedValue(' some-class ') + }) + + const result = await thisContext.toHaveElementClass(elements, 'some-class', { wait: 0, trim: true }) + + expect(result.pass).toBe(true) + }) + + test('should pass when ignore the case', async () => { + const result = await thisContext.toHaveElementClass(elements, 'sOme-ClAsS', { wait: 0, ignoreCase: true }) + expect(result.pass).toBe(true) + }) + + test('should pass if containing', async () => { + const result = await thisContext.toHaveElementClass(elements, 'some', { wait: 0, containing: true }) + expect(result.pass).toBe(true) + }) + + test('should pass if array ignores the case', async () => { + const result = await thisContext.toHaveElementClass(elements, ['sOme-ClAsS', 'anOther-ClAsS'], { wait: 0, ignoreCase: true }) + expect(result.pass).toBe(true) + }) + }) + + describe('failure when class name is not present', () => { + let result: AssertionResult + + beforeEach(async () => { + result = await thisContext.toHaveElementClass(elements, 'test') }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('/WDIO/') + + test('failure', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have class + +- Expected - 2 ++ Received + 2 + + Array [ +- "test", +- "test", ++ "some-class another-class yet-another-class", ++ "some-class another-class yet-another-class", + ]` ) + }) + }) + + describe('failure with RegExp when class name is not present', () => { + let result: AssertionResult + + beforeEach(async () => { + result = await thisContext.toHaveElementClass(elements, /WDIO/) }) - test('received message', () => { - expect(getReceived(result.message())).toContain('some-class another-class') + + test('failure', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have class + +- Expected - 2 ++ Received + 2 + + Array [ +- /WDIO/, +- /WDIO/, ++ "some-class another-class yet-another-class", ++ "some-class another-class yet-another-class", + ]` ) }) }) }) diff --git a/test/matchers/element/toHaveElementProperty.test.ts b/test/matchers/element/toHaveElementProperty.test.ts index affb68bf3..445c5f40c 100644 --- a/test/matchers/element/toHaveElementProperty.test.ts +++ b/test/matchers/element/toHaveElementProperty.test.ts @@ -1,227 +1,429 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' -import { $ } from '@wdio/globals' +import { $, $$ } from '@wdio/globals' -import { getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' import { toHaveElementProperty } from '../../../src/matchers/element/toHaveElementProperty.js' -import type { AssertionResult } from 'expect-webdriverio' vi.mock('@wdio/globals') -describe('toHaveElementProperty', () => { +describe(toHaveElementProperty, () => { + const thisContext = { toHaveElementProperty } + const thisIsNotContext = { isNot: true, toHaveElementProperty } - test('ignore case of stringified value', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('iphone') - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() + describe('given a single element', () => { + let el: ChainablePromiseElement - const result = await toHaveElementProperty.call({}, el, 'property', 'iPhone', { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }) - - expect(result.pass).toBe(true) - expect(el.getProperty).toHaveBeenCalledTimes(1) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveElementProperty', - expectedValue: ['property', 'iPhone'], - options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion } + beforeEach(() => { + el = $('sel') + vi.mocked(el.getProperty).mockResolvedValue('iphone') }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveElementProperty', - expectedValue: ['property', 'iPhone'], - options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }, - result + + test('ignore case of stringified value', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveElementProperty(el, 'property', 'iPhone', { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + expect(el.getProperty).toHaveBeenCalledTimes(1) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveElementProperty', + expectedValue: ['property', 'iPhone'], + options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveElementProperty', + expectedValue: ['property', 'iPhone'], + options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }, + result + }) }) - }) - test('assymeric match', async () => { - const el = await $('sel') + test('success with when property value is number', async () => { + vi.mocked(el.getProperty).mockResolvedValue(5) - el.getProperty = vi.fn().mockResolvedValue('iphone') + const result = await thisContext.toHaveElementProperty(el, 'property', 5) - const result = await toHaveElementProperty.call({}, el, 'property', expect.stringContaining('phone')) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + }) - test('should return false if values dont match', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('iphone') + // TODO Need deep equality to support array and object properly + test('success with when property value is an array, bug?', async () => { + vi.mocked(el.getProperty).mockResolvedValue([5]) - const result = await toHaveElementProperty.bind({})(el, 'property', 'foobar', { wait: 1 }) + const result = await thisContext.toHaveElementProperty(el, 'property', [5]) - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have property property - test('should return success (false) if values dont match when isNot is true', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('iphone') +Expected: [5] +Received: [5]` + ) + }) - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property', 'foobar', { wait: 1 }) + // TODO Need deep equality to support array and object properly + test('success with when property value an object, bug?', async () => { + vi.mocked(el.getProperty).mockResolvedValue( { foo: 'bar' } ) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + // @ts-expect-error -- object not working for now, to support later + const result = await thisContext.toHaveElementProperty(el, 'property', { foo: 'bar' } ) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have property property - test('should return true if values match', async () => { - const el = await $('sel') +Expected: {"foo": "bar"} +Received: {"foo": "bar"}` + ) + }) - el.getProperty = vi.fn().mockResolvedValue('iphone') - const result = await toHaveElementProperty.bind({})(el, 'property', 'iphone', { wait: 1 }) - expect(result.pass).toBe(true) - }) + test('assymeric match', async () => { + const result = await thisContext.toHaveElementProperty(el, 'property', expect.stringContaining('phone')) + expect(result.pass).toBe(true) + }) - test('should return failure (true) if values match when isNot is true', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('iphone') + test('not - success - should return pass=false if values dont match', async () => { + const result = await thisIsNotContext.toHaveElementProperty(el, 'property', 'foobar') - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property', 'iphone', { wait: 1 }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure - should return true if values match', async () => { + const result = await thisIsNotContext.toHaveElementProperty(el, 'property', 'iphone') - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to have property property Expected [not]: "iphone" -Received : "iphone"` - ) - }) +Received : "iphone"`) + }) - test('with RegExp should return true if values match', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('iphone') + test('with RegExp should return true if values match', async () => { + const result = await thisContext.toHaveElementProperty(el, 'property', /iPhOnE/i) - const result = await toHaveElementProperty.call({}, el, 'property', /iPhOnE/i) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) - }) + test('should return false for undefined input', async () => { + vi.mocked(el.getProperty).mockResolvedValue(undefined) - test.for([ - { propertyActualValue: null }, - { propertyActualValue: undefined }] - )('return false for not defined actual if expected is defined since property does not exist', async ( { propertyActualValue }) => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue(propertyActualValue) + const result = await thisContext.toHaveElementProperty(el, 'property', 'iphone') - const result = await toHaveElementProperty.bind({})(el, 'property', 'iphone', { wait: 1 }) - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(false) + }) - test.for([ - { propertyActualValue: null }, - { propertyActualValue: undefined }] - )('return success (false) for not defined actual and defined expected when isNot is true since property does not exist', async ({ propertyActualValue }) => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue(propertyActualValue) + test('should return false for null input', async () => { + vi.mocked(el.getProperty).mockResolvedValue(null) - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property', 'iphone', { wait: 1 }) + const result = await thisContext.toHaveElementProperty(el, 'property', 'iphone') - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(false) + }) - test.for([ - { expectedValue: null }, - // { expectedValue: undefined } // fails a bug? - ] - )('should return true when property does exist by passing an not defined expected value', async ( { expectedValue }) => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('Test Value') + //TODO: False when expecting null and value is null, sounds like a bug? + test('should return true? if value is null', async () => { + vi.mocked(el.getProperty).mockResolvedValue(null) - const result = await toHaveElementProperty.bind({})(el, 'property', expectedValue) + const result = await thisContext.toHaveElementProperty(el, 'property', null) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(false) + }) - test.for([ - { expectedValue: null }, - //{ expectedValue: undefined } // fails a bug? - ] - )('should return failure (true) if property exists by passing not defined expected value when isNot is true', async ( { expectedValue }) => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('Test Value') + test('should return false if value is non-string', async () => { + vi.mocked(el.getProperty).mockResolvedValue(5) - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property', expectedValue) + const result = await thisContext.toHaveElementProperty(el, 'property', 'Test Value') - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(false) + }) - // Bug? When requesting to have element property and it does exist should we return true here? - test.skip('return true if property is present', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('Test Value') + test('not - success - should return pass=false if value is non-string', async () => { + vi.mocked(el.getProperty).mockResolvedValue(5) - const result = await toHaveElementProperty.bind({})(el, 'property') - expect(result.pass).toBe(true) - }) + const result = await thisIsNotContext.toHaveElementProperty(el, 'property', 'Test Value') - // Bug? When requesting to not have element property and it does exist should we have a failure (pass=true? - test.skip('return failure (true) if property is present when isNot is true', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('Test Value') + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property') - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - }) + describe('failure with RegExp when value does not match', () => { + test('failure', async () => { + const result = await thisContext.toHaveElementProperty(el, 'property', /WDIO/) - test.for([ - { expectedValue: null }, - { expectedValue: undefined } - ] - )('return false if property is not present', async ({ expectedValue }) => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue(expectedValue) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have property property - const result = await toHaveElementProperty.bind({})(el, 'property') - expect(result.pass).toBe(false) +Expected: /WDIO/ +Received: "iphone"`) + }) + }) }) - test.for([ - { expectedValue: null }, - { expectedValue: undefined } - ] - )('return success (false) if value is not present when isNot is true', async ({ expectedValue }) => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue(expectedValue) - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property') + describe('given a multiple element', () => { + let els: ChainablePromiseArray - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + beforeEach(async () => { + els = await $$('sel') + els.forEach(element => + vi.mocked(element.getProperty).mockResolvedValue('iphone') + ) + expect(els).toHaveLength(2) + }) - test('should return false if value is non-string', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue(5) + describe('given a single expected value', () => { + test('ignore case of stringified value', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveElementProperty(els, 'property', 'iPhone', { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + els.forEach(el => + expect(el.getProperty).toHaveBeenCalledTimes(1) + ) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveElementProperty', + expectedValue: ['property', 'iPhone'], + options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveElementProperty', + expectedValue: ['property', 'iPhone'], + options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }, + result + }) + }) - const result = await toHaveElementProperty.bind({})(el, 'property', 'Test Value') - expect(result.pass).toBe(false) - }) + test('asymeric match', async () => { + const result = await thisContext.toHaveElementProperty(els, 'property', expect.stringContaining('phone')) + expect(result.pass).toBe(true) + }) - test('should return success (false) if value is non-string when isNot is true', async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue(5) + test('not - success - should return pass=false if values dont match', async () => { + const result = await thisIsNotContext.toHaveElementProperty(els, 'property', 'foobar') - const result = await toHaveElementProperty.bind({ isNot: true })(el, 'property', 'Test Value') + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + test('not - failure - should return pass=true if values match', async () => { + const result = await thisIsNotContext.toHaveElementProperty(els, 'property', 'iphone') - describe('failure with RegExp when value does not match', () => { - let result: AssertionResult + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have property property - beforeEach(async () => { - const el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('iphone') +Expected [not]: ["iphone", "iphone"] +Received : ["iphone", "iphone"]` + ) + }) - result = await toHaveElementProperty.call({}, el, 'property', /WDIO/, { wait: 1 }) - }) + test('with RegExp should return true if values match', async () => { + const result = await thisContext.toHaveElementProperty(els, 'property', /iPhOnE/i) - test('failure', () => { - expect(result.pass).toBe(false) + expect(result.pass).toBe(true) + }) + + test('should return false for null input', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(undefined) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', 'iphone') + + expect(result.pass).toBe(false) + }) + + // True when return non null value but passing null as expected? Sounds like a bug + test('should return true if value is null', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue('Test Value') + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', null) + + expect(result.pass).toBe(true) + }) + + test('should return false if expected is string and actual is non-string', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(5) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', 'Test Value') + + expect(result.pass).toBe(false) + }) + + test('should return true if equal values but with type number', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(5) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', 5) + + expect(result.pass).toBe(true) + }) + + describe('failure with RegExp when value does not match', () => { + test('failure', async () => { + const result = await thisContext.toHaveElementProperty(els, 'property', /WDIO/) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have property property + +- Expected - 2 ++ Received + 2 + + Array [ +- /WDIO/, +- /WDIO/, ++ "iphone", ++ "iphone", + ]`) + }) + }) }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have property') + describe('given a multiple expected values', () => { + test('ignore case of stringified value', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveElementProperty(els, 'property', ['iPhone', 'iPhone'], { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + els.forEach(el => + expect(el.getProperty).toHaveBeenCalledTimes(1) + ) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveElementProperty', + expectedValue: ['property', ['iPhone', 'iPhone']], + options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveElementProperty', + expectedValue: ['property', ['iPhone', 'iPhone']], + options: { wait: 0, ignoreCase: true, beforeAssertion, afterAssertion }, + result + }) + }) + + test('assymeric match', async () => { + const result = await thisContext.toHaveElementProperty(els, 'property', [expect.stringContaining('phone'), expect.stringContaining('phone')]) + expect(result.pass).toBe(true) + }) + + test('not - success - should return false if values dont match', async () => { + const result = await thisIsNotContext.toHaveElementProperty(els, 'property', ['foobar', 'foobar']) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure - should return true if values match', async () => { + const result = await thisIsNotContext.toHaveElementProperty(els, 'property', ['iphone', 'iphone']) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have property property + +Expected [not]: ["iphone", "iphone"] +Received : ["iphone", "iphone"]` + ) }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('/WDIO/') + + test('with RegExp should return true if values match', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue('iPhone') + ) + const result = await thisContext.toHaveElementProperty(els, 'property', [/iPhOnE/i, /iPhOnE/i]) + + expect(result.pass).toBe(true) + }) + + test('should return false for null input and expected value not null', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(null) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', ['iphone', 'iphone']) + + expect(result.pass).toBe(false) + expect(result.message()).toContain(`\ +Expect $$(\`sel\`) to have property property + +- Expected - 2 ++ Received + 2 + + Array [ +- "iphone", +- "iphone", ++ null, ++ null, + ]` + ) + }) + + // TODO: This should pass, sounds like a bug? + test.skip('should return true if value is null and expected are null', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(null) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', [null, null]) + + expect(result.pass).toBe(true) + }) + + test('not - success - should return false if actual value is null and expected is not null', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(null) + ) + + const result = await thisIsNotContext.toHaveElementProperty(els, 'property', ['yo', 'yo']) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('should return false if actual value is non-string and expected is string', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(5) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', ['Test Value', 'Test Value']) + + expect(result.pass).toBe(false) + }) + + test('should return true if all are equal number types', async () => { + els.forEach(el => + vi.mocked(el.getProperty).mockResolvedValue(5) + ) + + const result = await thisContext.toHaveElementProperty(els, 'property', [5, 5]) + + expect(result.pass).toBe(true) }) - test('received message', () => { - expect(getReceived(result.message())).toContain('iphone') + + describe('failure with RegExp when value does not match', () => { + test('failure', async () => { + const result = await thisContext.toHaveElementProperty(els, 'property', /WDIO/) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have property property + +- Expected - 2 ++ Received + 2 + + Array [ +- /WDIO/, +- /WDIO/, ++ "iphone", ++ "iphone", + ]`) + }) }) }) }) diff --git a/test/matchers/element/toHaveHTML.test.ts b/test/matchers/element/toHaveHTML.test.ts index f8dbbd2a2..a0c8472ee 100755 --- a/test/matchers/element/toHaveHTML.test.ts +++ b/test/matchers/element/toHaveHTML.test.ts @@ -1,315 +1,587 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' -import { $ } from '@wdio/globals' - -import { getExpectMessage, getReceived, getExpected } from '../../__fixtures__/utils.js' +import { $, $$ } from '@wdio/globals' import { toHaveHTML } from '../../../src/matchers/element/toHaveHTML.js' vi.mock('@wdio/globals') -describe('toHaveHTML', () => { +describe(toHaveHTML, () => { - test('wait for success', async () => { - const element = await $('sel') - element.getHTML = vi.fn() - .mockResolvedValueOnce('') - .mockResolvedValueOnce('') - .mockResolvedValue('
foo
') + let thisContext: { 'toHaveHTML': typeof toHaveHTML } + let thisNotContext: { 'toHaveHTML': typeof toHaveHTML, isNot: boolean } - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() + beforeEach(() => { + thisContext = { 'toHaveHTML': toHaveHTML } + thisNotContext = { 'toHaveHTML': toHaveHTML, isNot: true } + }) - const result = await toHaveHTML.call({}, element, '
foo
', { ignoreCase: true, beforeAssertion, afterAssertion }) + describe('given single element', () => { + let element: ChainablePromiseElement - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(3) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveHTML', - expectedValue: '
foo
', - options: { ignoreCase: true, beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveHTML', - expectedValue: '
foo
', - options: { ignoreCase: true, beforeAssertion, afterAssertion }, - result + beforeEach(async () => { + element = await $('sel') + + vi.mocked(element.getHTML).mockResolvedValue('
foo
') }) - }) - test('wait but failure', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockRejectedValue(new Error('some error')) + test('wait for success', async () => { + vi.mocked(element.getHTML) + .mockResolvedValueOnce('') + .mockResolvedValueOnce('') + .mockResolvedValue('
foo
') - await expect(() => toHaveHTML.call({}, element, '
foo
', { ignoreCase: true })) - .rejects.toThrow('some error') - }) + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - test('success on the first attempt', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + const result = await thisContext.toHaveHTML(element, '
foo
', { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }) - const result = await toHaveHTML.call({}, element, '
foo
', { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(3) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveHTML', + expectedValue: '
foo
', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveHTML', + expectedValue: '
foo
', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - test('no wait - failure', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + test('wait but error', async () => { + vi.mocked(element.getHTML).mockRejectedValue(new Error('some error')) - const result = await toHaveHTML.call({}, element, 'foo', { wait: 0 }) - expect(result.pass).toBe(false) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) + await expect(() => thisContext.toHaveHTML(element, '
foo
', { ignoreCase: true, wait: 1 })) + .rejects.toThrow('some error') + }) - test('no wait - success', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + test('success on the first attempt', async () => { + const result = await thisContext.toHaveHTML(element, '
foo
', { wait: 1, ignoreCase: true }) - const result = await toHaveHTML.call({}, element, '
foo
', { wait: 0 }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) + }) - test('not - failure - pass should be true', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + test('no wait - failure', async () => { + const result = await thisContext.toHaveHTML(element, 'foo', { wait: 0 }) - const result = await toHaveHTML.call({ isNot: true }, element, '
foo
', { wait: 0 }) + expect(result.pass).toBe(false) + expect(element.getHTML).toHaveBeenCalledTimes(1) + }) + + test('no wait - success', async () => { + const result = await thisContext.toHaveHTML(element, '
foo
', { wait: 0 }) + + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveHTML(element, '
foo
') + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to have HTML Expected [not]: "
foo
" Received : "
foo
"` - ) - }) + ) + }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getHTML = vi.fn().mockResolvedValue('
foo
') + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveHTML(element, 'foobar') - const result = await toHaveHTML.call({ isNot: true }, el, '
Notfoo
', { wait: 0 }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + test('should return true if actual html + single replacer matches the expected html', async () => { + const result = await thisContext.toHaveHTML(element, '
bar
', { wait: 1, replace: ['foo', 'bar'] }) - test("should return false if htmls don't match", async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({})(element, 'foobar', { wait: 1 }) - expect(result.pass).toBe(false) - }) + test('should return true if actual html + replace (string) matches the expected html', async () => { + const result = await thisContext.toHaveHTML(element, '
bar
', { wait: 1, replace: [['foo', 'bar']] }) - test("should suceeds (false) if htmls don't match when isNot is true", async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockReturnValue('
foo
') + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({ isNot: true })(element, 'foobar', { wait: 1 }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + test('should return true if actual html + replace (regex) matches the expected html', async () => { + const result = await thisContext.toHaveHTML(element, '
bar
', { wait: 1, replace: [[/foo/, 'bar']] }) - test('should fails (pass=true) if htmls match when isNot is true', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockReturnValue('
foo
') + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({ isNot: true })(element, '
foo
', { wait: 1 }) - expect(result.pass).toBe(true) // success, boolean is inverted later because of `.not` - }) + test('should return true if actual html starts with expected html', async () => { + const result = await thisContext.toHaveHTML(element, '
', { wait: 1, atStart: true }) - test('should return true if htmls match', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({})(element, '
foo
', { wait: 1 }) - expect(result.pass).toBe(true) - }) + test('should return true if actual html ends with expected html', async () => { + const result = await thisContext.toHaveHTML(element, '
', { wait: 1, atEnd: true }) - test('should return true if actual html + single replacer matches the expected html', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({})(element, '
bar
', { replace: ['foo', 'bar'] }) - expect(result.pass).toBe(true) - }) + test('should return true if actual html contains the expected html at the given index', async () => { + const result = await thisContext.toHaveHTML(element, 'iv>foo', { wait: 1, atIndex: 2 }) - test('should return true if actual html + replace (string) matches the expected html', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({})(element, '
bar
', { replace: [['foo', 'bar']] }) - expect(result.pass).toBe(true) - }) + test('should return true if actual html equals the expected html with includeSelectorTag set to false', async () => { + vi.mocked(element.getHTML).mockImplementation(async ({ includeSelectorTag}: { includeSelectorTag: boolean }) => { + return includeSelectorTag ? '
foo
' : '
foo
' + }) - test('should return true if actual html + replace (regex) matches the expected html', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + const result = await thisContext.toHaveHTML(element, '
foo
', { wait: 1, includeSelectorTag: false }) + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({})(element, '
bar
', { replace: [[/foo/, 'bar']] }) - expect(result.pass).toBe(true) - }) + test('should return true if actual html equals the expected html with includeSelectorTag set to true', async () => { + vi.mocked(element.getHTML).mockImplementation(async ({ includeSelectorTag}: { includeSelectorTag: boolean }) => { + return includeSelectorTag ? '
foo
' : '
foo
' + }) - test('should return true if actual html starts with expected html', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + const result = await thisContext.toHaveHTML(element, '
foo
', { + wait: 1, + includeSelectorTag: true, + }) + expect(result.pass).toBe(true) + }) - const result = await toHaveHTML.bind({})(element, '
', { atStart: true }) - expect(result.pass).toBe(true) - }) + test('message', async () => { + vi.mocked(element.getHTML).mockResolvedValue('') + const result = await thisContext.toHaveHTML(element, '
foo
') - test('should return true if actual html ends with expected html', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have HTML - const result = await toHaveHTML.bind({})(element, '
', { atEnd: true }) - expect(result.pass).toBe(true) - }) +Expected: "
foo
" +Received: ""`) + }) - test('should return true if actual html contains the expected html at the given index', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + test('success if array matches with html and ignoreCase', async () => { + const result = await thisContext.toHaveHTML(element, ['div', '
foo
'], { wait: 1, ignoreCase: true }) - const result = await toHaveHTML.bind({})(element, 'iv>foo', { atIndex: 2 }) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) + }) + + test('success if array matches with html and trim', async () => { + vi.mocked(element.getHTML).mockResolvedValue('
foo
') - test('should return true if actual html equals the expected html with includeSelectorTag set to false', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockImplementation(async ({ includeSelectorTag}: { includeSelectorTag: boolean }) => { - return includeSelectorTag ? '
foo
' : '
foo
' + const result = await thisContext.toHaveHTML(element, ['div', '
foo
', 'toto'], { wait: 1, trim: true }) + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) }) - const result = await toHaveHTML.bind({})(element, '
foo
', { includeSelectorTag: false }) - expect(result.pass).toBe(true) - }) + test('success if array matches with html and replace (string)', async () => { + const result = await thisContext.toHaveHTML(element, ['div', '
foo
', 'toto'], { + wait: 1, + replace: [['Web', 'Browser']], + }) - test('should return true if actual html equals the expected html with includeSelectorTag set to true', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockImplementation(async ({ includeSelectorTag}: { includeSelectorTag: boolean }) => { - return includeSelectorTag ? '
foo
' : '
foo
' + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) }) - const result = await toHaveHTML.bind({})(element, '
foo
', { - includeSelectorTag: true, + test('success if array matches with html and replace (regex)', async () => { + const result = await thisContext.toHaveHTML(element, ['div', '
foo
', 'toto'], { + wait: 1, + replace: [[/Web/g, 'Browser']], + }) + + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) }) - expect(result.pass).toBe(true) - }) - test('message', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('') - const result = await toHaveHTML.call({}, element, '
foo
') - expect(getExpectMessage(result.message())).toContain('to have HTML') - }) + test('success if array matches with html and multiple replacers and one of the replacers is a function', async () => { + const result = await thisContext.toHaveHTML(element, ['div', '

foo

', 'toto'], { + wait: 1, + replace: [ + [/div/g, 'p'], + [/[A-Z]/g, (match: string) => match.toLowerCase()], + ], + }) - test('success if array matches with html and ignoreCase', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + expect(result.pass).toBe(true) + expect(element.getHTML).toHaveBeenCalledTimes(1) + }) - const result = await toHaveHTML.call({}, element, ['div', '
foo
'], { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) + test('failure if array does not match with html', async () => { + const result = await thisContext.toHaveHTML(element, ['div', 'foo']) + + expect(result.pass).toBe(false) + expect(element.getHTML).toHaveBeenCalledTimes(1) + }) - test('success if array matches with html and trim', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + describe('with RegExp', () => { + beforeEach(async () => { + vi.mocked(element.getHTML).mockResolvedValue('This is example HTML') + }) - const result = await toHaveHTML.call({}, element, ['div', '
foo
', 'toto'], { trim: true }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) + test('success if match', async () => { + const result = await thisContext.toHaveHTML(element, /ExAmplE/i) + expect(result.pass).toBe(true) + }) + + test('success if array matches with RegExp', async () => { + const result = await thisContext.toHaveHTML(element, ['div', /ExAmPlE/i]) + expect(result.pass).toBe(true) + }) - test('success if array matches with html and replace (string)', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + test('success if array matches with html', async () => { + const result = await thisContext.toHaveHTML(element, ['This is example HTML', /Webdriver/i]) + expect(result.pass).toBe(true) + }) + + test('success if array matches with html and ignoreCase', async () => { + const result = await thisContext.toHaveHTML(element, ['ThIs Is ExAmPlE HTML', /Webdriver/i], { + wait: 1, + ignoreCase: true, + }) + expect(result.pass).toBe(true) + }) + + test('failure if no match', async () => { + const result = await thisContext.toHaveHTML(element, /Webdriver/i) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have HTML + +Expected: /Webdriver/i +Received: "This is example HTML"` + ) + }) - const result = await toHaveHTML.call({}, element, ['div', '
foo
', 'toto'], { - replace: [['Web', 'Browser']], + test('failure if array does not match with html', async () => { + const result = await thisContext.toHaveHTML(element, ['div', /Webdriver/i]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have HTML + +Expected: ["div", /Webdriver/i] +Received: "This is example HTML"` + ) + }) }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) }) - test('success if array matches with html and replace (regex)', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + describe('given multiple elements', () => { + let elements: ChainablePromiseArray + + beforeEach(async () => { + elements = await $$('sel') + + expect(elements).toHaveLength(2) + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) - const result = await toHaveHTML.call({}, element, ['div', '
foo
', 'toto'], { - replace: [[/Web/g, 'Browser']], }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) - test('success if array matches with html and multiple replacers and one of the replacers is a function', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
FOO
') + test('wait for success', async () => { + elements.forEach(el => vi.mocked(el.getHTML) + .mockResolvedValueOnce('') + .mockResolvedValueOnce('') + .mockResolvedValue('
foo
') + ) + + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() - const result = await toHaveHTML.call({}, element, ['div', '

foo

', 'toto'], { - replace: [ - [/div/g, 'p'], - [/[A-Z]/g, (match: string) => match.toLowerCase()], - ], + const result = await thisContext.toHaveHTML(elements, '
foo
', { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }) + + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(3)) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveHTML', + expectedValue: '
foo
', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveHTML', + expectedValue: '
foo
', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }, + result + }) }) - expect(result.pass).toBe(true) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) - test('failure if array does not match with html', async () => { - const element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('
foo
') + test('wait but failure', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockRejectedValue(new Error('some error'))) - const result = await toHaveHTML.call({}, element, ['div', 'foo'], { wait: 1 }) - expect(result.pass).toBe(false) - expect(element.getHTML).toHaveBeenCalledTimes(1) - }) + await expect(() => thisContext.toHaveHTML(elements, '
foo
', { ignoreCase: true, wait: 1 })) + .rejects.toThrow('some error') + }) - describe('with RegExp', () => { - let element: ChainablePromiseElement + test('success on the first attempt', async () => { + const result = await thisContext.toHaveHTML(elements, '
foo
', { ignoreCase: true }) - beforeEach(async () => { - element = await $('sel') - element.getHTML = vi.fn().mockResolvedValue('This is example HTML') + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(1)) }) - test('success if match', async () => { - const result = await toHaveHTML.call({}, element, /ExAmplE/i) + test('no wait - failure', async () => { + const result = await thisContext.toHaveHTML(elements, 'foo', { wait: 0 }) + + expect(result.pass).toBe(false) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(1)) + }) + + test('no wait - success', async () => { + const result = await thisContext.toHaveHTML(elements, '
foo
', { wait: 0 }) + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(1)) + }) + + test('not - failure on all elements - pass should be true', async () => { + const result = await thisNotContext.toHaveHTML(elements, '
foo
') + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have HTML + +Expected [not]: ["
foo
", "
foo
"] +Received : ["
foo
", "
foo
"]` + ) }) - test('success if array matches with RegExp', async () => { - const result = await toHaveHTML.call({}, element, ['div', /ExAmPlE/i]) + test('not - failure on first element - pass should be true', async () => { + vi.mocked(elements[0].getHTML).mockResolvedValue('
foo
') + vi.mocked(elements[1].getHTML).mockResolvedValue('
fii
') + + const result = await thisNotContext.toHaveHTML(elements, '
foo
') + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have HTML + +Expected [not]: ["
foo
", "
foo
"] +Received : ["
foo
", "
fii
"]` + ) + }) + + test('not - succcess - pass should be false', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
')) + + const result = await thisNotContext.toHaveHTML(elements, '
foo
') + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('should return true if actual html + single replacer matches the expected html', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, '
bar
', { replace: ['foo', 'bar'] }) + expect(result.pass).toBe(true) }) - test('success if array matches with html', async () => { - const result = await toHaveHTML.call({}, element, ['This is example HTML', /Webdriver/i]) + test('should return true if actual html + replace (string) matches the expected html', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, '
bar
', { replace: [['foo', 'bar']] }) expect(result.pass).toBe(true) }) - test('success if array matches with html and ignoreCase', async () => { - const result = await toHaveHTML.call({}, element, ['ThIs Is ExAmPlE HTML', /Webdriver/i], { - ignoreCase: true, + test('should return true if actual html + replace (regex) matches the expected html', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, '
bar
', { replace: [[/foo/, 'bar']] }) + expect(result.pass).toBe(true) + }) + + test('should return true if actual html starts with expected html', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, '
', { atStart: true }) + expect(result.pass).toBe(true) + }) + + test('should return true if actual html ends with expected html', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, '
', { atEnd: true }) + expect(result.pass).toBe(true) + }) + + test('should return true if actual html contains the expected html at the given index', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, 'iv>foo', { atIndex: 2 }) + expect(result.pass).toBe(true) + }) + + test('should return true if actual html equals the expected html with includeSelectorTag set to false', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockImplementation(async ({ includeSelectorTag}: { includeSelectorTag: boolean }) => { + return includeSelectorTag ? '
foo
' : '
foo
' + })) + + const result = await thisContext.toHaveHTML(elements, '
foo
', { includeSelectorTag: false }) + expect(result.pass).toBe(true) + }) + + test('should return true if actual html equals the expected html with includeSelectorTag set to true', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockImplementation(async ({ includeSelectorTag}: { includeSelectorTag: boolean }) => { + return includeSelectorTag ? '
foo
' : '
foo
' + })) + + const result = await thisContext.toHaveHTML(elements, '
foo
', { + includeSelectorTag: true, }) expect(result.pass).toBe(true) }) - test('failure if no match', async () => { - const result = await toHaveHTML.call({}, element, /Webdriver/i) + test('message', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('')) + + const result = await thisContext.toHaveHTML(elements, '
foo
') + + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have HTML + +- Expected - 2 ++ Received + 2 + + Array [ +- "
foo
", +- "
foo
", ++ "", ++ "", + ]` + ) + }) + + test('fails if not an array exact match even if one element matches - not supporting any array value match', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, ['div', '
foo
']) expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have HTML') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getReceived(result.message())).toContain('This is example HTML') + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have HTML + +- Expected - 1 ++ Received + 1 + + Array [ +- "div", ++ "
foo
", + "
foo
", + ]` + ) }) - test('failure if array does not match with html', async () => { - const result = await toHaveHTML.call({}, element, ['div', /Webdriver/i]) + test('fails if expect and actual array length do not match', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, ['div', '
foo
', 'toto'], { trim: true, wait: 0 }) expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have HTML') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getExpected(result.message())).toContain('div') + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have HTML + +- Expected - 3 ++ Received + 1 + + Array [ +- "div", +- "
foo
", +- "toto", ++ "Received array length 2, expected 3", + ]` + ) + }) + + test('success if array matches with html and ignoreCase', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
FOO
')) + + const result = await thisContext.toHaveHTML(elements, ['
foo
', '
foo
'], { ignoreCase: true }) + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(1)) + }) + + test('success if array matches with html and trim', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
foo
')) + + const result = await thisContext.toHaveHTML(elements, ['
foo
', '
foo
'], { trim: true }) + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(1)) + }) + + test('success if array matches with html and multiple replacers and one of the replacers is a function', async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('
FOO
')) + + const result = await thisContext.toHaveHTML(elements, ['

foo

', '

foo

'], { + replace: [ + [/div/g, 'p'], + [/[A-Z]/g, (match: string) => match.toLowerCase()], + ], + }) + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getHTML).toHaveBeenCalledTimes(1)) + }) + + describe('with RegExp', () => { + beforeEach(async () => { + elements.forEach(el => vi.mocked(el.getHTML).mockResolvedValue('This is example HTML')) + }) + + test('success if match', async () => { + const result = await thisContext.toHaveHTML(elements, /ExAmplE/i) + expect(result.pass).toBe(true) + }) + + test('success if array matches with RegExp', async () => { + const result = await thisContext.toHaveHTML(elements, ['This is example HTML', /ExAmPlE/i]) + expect(result.pass).toBe(true) + }) + + test('success if array matches with html and ignoreCase', async () => { + const result = await thisContext.toHaveHTML(elements, ['ThIs Is ExAmPlE HTML', /ExAmPlE/i], { + ignoreCase: true, + }) + expect(result.pass).toBe(true) + }) + + test('failure if no match', async () => { + const result = await thisContext.toHaveHTML(elements, /Webdriver/i) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have HTML + +- Expected - 2 ++ Received + 2 + + Array [ +- /Webdriver/i, +- /Webdriver/i, ++ "This is example HTML", ++ "This is example HTML", + ]` + ) + }) + + test('failure if array does not match with html', async () => { + const result = await thisContext.toHaveHTML(elements, ['div', /Webdriver/i]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have HTML + +- Expected - 2 ++ Received + 2 + + Array [ +- "div", +- /Webdriver/i, ++ "This is example HTML", ++ "This is example HTML", + ]` + ) + }) }) }) }) diff --git a/test/matchers/element/toHaveHeight.test.ts b/test/matchers/element/toHaveHeight.test.ts index 8f6134cf7..618cef036 100755 --- a/test/matchers/element/toHaveHeight.test.ts +++ b/test/matchers/element/toHaveHeight.test.ts @@ -1,135 +1,123 @@ -import { vi, test, describe, expect } from 'vitest' +import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' - -import { getExpectMessage } from '../../__fixtures__/utils.js' import { toHaveHeight } from '../../../src/matchers/element/toHaveHeight.js' vi.mock('@wdio/globals') -describe('toHaveHeight', () => { - test('wait for success', async () => { - const el = await $('sel') - - el.getSize = vi.fn() - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce(32) - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - - const result = await toHaveHeight.call({}, el, 32, { beforeAssertion, afterAssertion }) - - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(3) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveHeight', - expectedValue: 32, - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveHeight', - expectedValue: 32, - options: { beforeAssertion, afterAssertion }, - result - }) - }) +describe(toHaveHeight, () => { - test('wait but failure', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockRejectedValue(new Error('some error')) + let thisContext: { 'toHaveHeight': typeof toHaveHeight } + let thisNotContext: { 'toHaveHeight': typeof toHaveHeight, isNot: boolean } - await expect(() => toHaveHeight.call({}, el, 10, {})) - .rejects.toThrow('some error') + beforeEach(() => { + thisContext = { 'toHaveHeight': toHaveHeight } + thisNotContext = { 'toHaveHeight': toHaveHeight, isNot: true } }) - test('success on the first attempt', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) + describe('given a single element', () => { + let el: ChainablePromiseElement - const result = await toHaveHeight.call({}, el, 32, {}) + beforeEach(async () => { + el = await $('sel') - expect(result.message()).toEqual('Expect $(`sel`) to have height\n\nExpected: 32\nReceived: serializes to the same string') - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) - - test('no wait - failure', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) + el.getSize = vi.fn().mockResolvedValue(32) + }) - const result = await toHaveHeight.call({}, el, 10, { wait: 0 }) - expect(result.message()).toEqual('Expect $(`sel`) to have height\n\nExpected: 10\nReceived: 32') - expect(result.pass).toBe(false) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + test('wait for success', async () => { + el.getSize = vi.fn() + .mockResolvedValueOnce(50) + .mockResolvedValueOnce(32) + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveHeight(el, 32, { beforeAssertion, afterAssertion, wait: 500 }) + + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(2) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveHeight', + expectedValue: 32, + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveHeight', + expectedValue: 32, + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - test('no wait - success', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) + test('wait but failure', async () => { + vi.mocked(el.getSize).mockRejectedValue(new Error('some error')) - const result = await toHaveHeight.call({}, el, 32, { wait: 0 }) + await expect(() => thisContext.toHaveHeight(el, 10)) + .rejects.toThrow('some error') + }) - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + test('success on the first attempt', async () => { + const result = await thisContext.toHaveHeight(el, 32) - test('gte and lte', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - const result = await toHaveHeight.call({}, el, { gte: 31, lte: 33 }, { wait: 0 }) + test('no wait - failure', async () => { + const result = await thisContext.toHaveHeight(el, 10, { wait: 0 }) - expect(result.message()).toEqual('Expect $(`sel`) to have height\n\nExpected: ">= 31 && <= 33"\nReceived: 32') - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have height - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) - const result = await toHaveHeight.call({ isNot: true }, el, 32, { wait: 0 }) +Expected: 10 +Received: 32` + ) + expect(result.pass).toBe(false) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to have height + test('no wait - success', async () => { + const result = await thisContext.toHaveHeight(el, 32, { wait: 0 }) -Expected [not]: 32 -Received : 32` - ) - }) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(31) + test('gte and lte', async () => { + const result = await thisContext.toHaveHeight(el, { gte: 31, lte: 33 }) - const result = await toHaveHeight.call({ isNot: true }, el, 32, { wait: 0 }) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveHeight(el, 32) - test("should return false if sizes don't match", async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to have height - const result = await toHaveHeight.bind({})(el, 10, { wait: 1 }) - expect(result.pass).toBe(false) - }) +Expected [not]: 32 +Received : 32` + ) + }) - test('should return true if sizes match', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(32) + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveHeight(el, 10) - const result = await toHaveHeight.bind({})(el, 32, { wait: 1 }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - expect(result.pass).toBe(true) - }) + test('message', async () => { + el.getSize = vi.fn().mockResolvedValue(1) - test('message', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(null) + const result = await thisContext.toHaveHeight(el, 50) - const result = await toHaveHeight.call({}, el, 50) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have height - expect(getExpectMessage(result.message())).toContain('to have height') +Expected: 50 +Received: 1` + ) + }) }) }) diff --git a/test/matchers/element/toHaveHref.test.ts b/test/matchers/element/toHaveHref.test.ts index f2c24ce07..478935f8d 100644 --- a/test/matchers/element/toHaveHref.test.ts +++ b/test/matchers/element/toHaveHref.test.ts @@ -1,63 +1,68 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' -import { getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' import { toHaveHref } from '../../../src/matchers/element/toHaveHref.js' import type { AssertionResult } from 'expect-webdriverio' vi.mock('@wdio/globals') -describe('toHaveHref', () => { - let el: ChainablePromiseElement +describe(toHaveHref, () => { - beforeEach(async () => { - el = await $('sel') - el.getAttribute = vi.fn().mockImplementation((attribute: string) => { - if (attribute === 'href') { - return 'https://www.example.com' - } - return null - }) - }) + let thisContext: { 'toHaveHref': typeof toHaveHref } - test('success when contains', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const result = await toHaveHref.call({}, el, 'https://www.example.com', { beforeAssertion, afterAssertion }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveHref', - expectedValue: 'https://www.example.com', - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveHref', - expectedValue: 'https://www.example.com', - options: { beforeAssertion, afterAssertion }, - result - }) + beforeEach(() => { + thisContext = { 'toHaveHref': toHaveHref } }) - describe('failure when doesnt contain', () => { - let result: AssertionResult + describe('given a single element', () => { + let el: ChainablePromiseElement beforeEach(async () => { - result = await toHaveHref.call({}, el, 'an href') + el = await $('sel') + vi.mocked(el.getAttribute) + .mockImplementation(async (attribute: string) => { + if (attribute === 'href') { + return 'https://www.example.com' + } + return null + }) }) - test('failure', () => { - expect(result.pass).toBe(false) - }) + test('success when contains', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveHref(el, 'https://www.example.com', { wait: 0, beforeAssertion, afterAssertion }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have attribute href') + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveHref', + expectedValue: 'https://www.example.com', + options: { beforeAssertion, afterAssertion, wait: 0 } }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('an href') + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveHref', + expectedValue: 'https://www.example.com', + options: { beforeAssertion, afterAssertion, wait: 0 }, + result }) - test('received message', () => { - expect(getReceived(result.message())).toContain('https://www.example.com') + }) + + describe('failure when doesnt contain', () => { + let result: AssertionResult + + beforeEach(async () => { + result = await thisContext.toHaveHref(el, 'an href') + }) + + test('failure with proper failure message', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have attribute href + +Expected: "an href" +Received: "https://www.example.com"` + ) }) }) }) diff --git a/test/matchers/element/toHaveId.test.ts b/test/matchers/element/toHaveId.test.ts index 3c67a939c..09205c112 100644 --- a/test/matchers/element/toHaveId.test.ts +++ b/test/matchers/element/toHaveId.test.ts @@ -1,65 +1,67 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' -import { getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' import { toHaveId } from '../../../src/matchers/element/toHaveId.js' import type { AssertionResult } from 'expect-webdriverio' vi.mock('@wdio/globals') -describe('toHaveId', () => { - let el: ChainablePromiseElement +describe(toHaveId, () => { - beforeEach(async () => { - el = await $('sel') - el.getAttribute = vi.fn().mockImplementation((attribute: string) => { - if (attribute === 'id') { - return 'test id' - } - return null - }) - }) + let thisContext: { toHaveId: typeof toHaveId } - test('success', async () => { - const result = await toHaveId.call({}, el, 'test id') - expect(result.pass).toBe(true) + beforeEach(() => { + thisContext = { toHaveId } }) - describe('failure', () => { - let result: AssertionResult - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() + describe('given a single element', () => { + let el: ChainablePromiseElement beforeEach(async () => { - result = await toHaveId.call({}, el, 'an attribute', { beforeAssertion, afterAssertion, wait: 0 }) + el = await $('sel') + vi.mocked(el.getAttribute).mockImplementation(async (attribute: string) => { + if (attribute === 'id') { + return 'test id' + } + return null + }) }) - test('failure', () => { - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveId', - expectedValue: 'an attribute', - options: { beforeAssertion, afterAssertion, wait: 0 } - }) - expect(result.pass).toBe(false) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveId', - expectedValue: 'an attribute', - options: { beforeAssertion, afterAssertion, wait: 0 }, - result - }) + test('success', async () => { + const result = await thisContext.toHaveId(el, 'test id') + expect(result.pass).toBe(true) }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have attribute id') - }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('an attribute') + describe('failure', () => { + let result: AssertionResult + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + beforeEach(async () => { + result = await thisContext.toHaveId(el, 'an attribute', { wait: 1, beforeAssertion, afterAssertion }) }) - test('received message', () => { - expect(getReceived(result.message())).toContain('test id') + + test('failure with proper failure callbacks and message', () => { + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveId', + expectedValue: 'an attribute', + options: { beforeAssertion, afterAssertion, wait: 1 } + }) + expect(result.pass).toBe(false) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveId', + expectedValue: 'an attribute', + options: { beforeAssertion, afterAssertion, wait: 1 }, + result + }) + + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have attribute id + +Expected: "an attribute" +Received: "test id"` + ) }) }) }) - }) diff --git a/test/matchers/element/toHaveSize.test.ts b/test/matchers/element/toHaveSize.test.ts old mode 100755 new mode 100644 index 74665b118..ea691bcd5 --- a/test/matchers/element/toHaveSize.test.ts +++ b/test/matchers/element/toHaveSize.test.ts @@ -1,115 +1,484 @@ -import { vi, test, describe, expect } from 'vitest' -import { $ } from '@wdio/globals' +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' +import type { Size } from '../../../src/matchers/element/toHaveSize.js' import { toHaveSize } from '../../../src/matchers/element/toHaveSize.js' vi.mock('@wdio/globals') -describe('toHaveSize', () => { - test('wait for success', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - - const result = await toHaveSize.call({}, el, { width: 32, height: 32 }, { beforeAssertion, afterAssertion }) - - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveSize', - expectedValue: { width: 32, height: 32 }, - options: { beforeAssertion, afterAssertion } +describe(toHaveSize, async () => { + let thisContext: { toHaveSize: typeof toHaveSize } + let thisNotContext: { isNot: true; toHaveSize: typeof toHaveSize } + + const expectedValue: Size = { width: 32, height: 32 } + const wrongValue: Size = { width: 15, height: 32 } + + beforeEach(async () => { + thisContext = { toHaveSize } + thisNotContext = { isNot: true, ...thisContext } + }) + + describe.for([ + { element: await $('sel'), type: 'awaited ChainablePromiseElement' }, + { element: await $('sel').getElement(), type: 'awaited getElement of ChainablePromiseElement (e.g. WebdriverIO.Element)' }, + { element: $('sel'), type: 'non-awaited of ChainablePromiseElement' } + ])('given a single element when $type', ({ element }) => { + let el: ChainablePromiseElement | WebdriverIO.Element + + beforeEach(() => { + el = element + vi.mocked(el.getSize).mockResolvedValue(expectedValue as unknown as Size & number) // vitest does not support overloads function well }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveSize', - expectedValue: { width: 32, height: 32 }, - options: { beforeAssertion, afterAssertion }, - result + + test('wait for success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveSize(el, expectedValue, { beforeAssertion, afterAssertion, wait: 500 }) + + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveSize', + expectedValue: expectedValue, + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveSize', + expectedValue: expectedValue, + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) }) - }) - test('wait but failure', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockRejectedValue(new Error('some error')) + test('wait but error', async () => { + vi.mocked(el.getSize).mockRejectedValue(new Error('some error')) - await expect(() => toHaveSize.call({}, el, { width: 32, height: 32 }, {})) - .rejects.toThrow('some error') - }) + await expect(() => thisContext.toHaveSize(el, expectedValue)) + .rejects.toThrow('some error') + }) - test('success on the first attempt', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) + test('success by default', async () => { + const result = await thisContext.toHaveSize(el, expectedValue) - const result = await toHaveSize.call({}, el, { width: 32, height: 32 }, {}) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + test('no wait - failure with proper error message', async () => { + vi.mocked(el.getSize).mockResolvedValue(wrongValue as unknown as Size & number) - test('no wait - failure', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) + const result = await thisContext.toHaveSize(el, expectedValue, { wait: 0 }) - const result = await toHaveSize.call({}, el, { width: 15, height: 32 }, { wait: 0 }) + expect(result.pass).toBe(false) + expect(el.getSize).toHaveBeenCalledTimes(1) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have size - expect(result.pass).toBe(false) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) +- Expected - 1 ++ Received + 1 - test('no wait - success', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }` + ) + }) - const result = await toHaveSize.call({}, el, { width: 32, height: 32 }, { wait: 0 }) + test('no wait - success', async () => { + const result = await thisContext.toHaveSize(el, expectedValue, { wait: 0 }) - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) - const result = await toHaveSize.call({ isNot: true }, el, { width: 32, height: 32 }, { wait: 0 }) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveSize(el, expectedValue) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to have size Expected [not]: {"height": 32, "width": 32} Received : {"height": 32, "width": 32}` - ) - }) + ) + }) - test("should return false if sizes don't match", async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) + test('should fails with custom failure message', async () => { + vi.mocked(el.getSize).mockResolvedValue(wrongValue as unknown as Size & number) - const result = await toHaveSize.bind({})(el, { width: 15, height: 32 }, { wait: 1 }) + const result = await thisContext.toHaveSize(el, expectedValue, { wait: 1, message: 'Custom error message' }) - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Custom error message +Expect $(\`sel\`) to have size + +- Expected - 1 ++ Received + 1 - test('should return true if sizes match', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue({ width: 32, height: 32 }) + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }` + ) + }) - const result = await toHaveSize.bind({})(el, { width: 32, height: 32 }, { wait: 1 }) + test('should fails when expected is an unsupported array type', async () => { + const result = await thisContext.toHaveSize(el, [expectedValue]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have size + +Expected: [{"height": 32, "width": 32}] +Received: "Expected value cannot be an array"` + ) + }) - expect(result.pass).toBe(true) }) - test('message', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(null) + describe.for([ + { elements: await $$('sel'), title: 'awaited ChainablePromiseArray' }, + { elements: await $$('sel').getElements(), title: 'awaited getElements of ChainablePromiseArray (e.g. WebdriverIO.ElementArray)' }, + { elements: await $$('sel').filter((t) => t.isEnabled()), title: 'awaited filtered ChainablePromiseArray (e.g. WebdriverIO.Element[])' }, + { elements: $$('sel'), title: 'non-awaited of ChainablePromiseArray' } + ])('given multiple elements when $title', ({ elements, title }) => { + let els: ChainablePromiseArray | WebdriverIO.Element[] | WebdriverIO.ElementArray + let awaitedEls: typeof els - const result = await toHaveSize.call({}, el, { width: 32, height: 32 }) + let selectorName = '$$(`sel`)' + if (title.includes('Element[]')) {selectorName = '$(`sel`), $$(`sel`)[1]'} - expect(result.pass).toBe(false) - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) to have size + beforeEach(async () => { + els = elements + + awaitedEls = Array.isArray(els) ? els : await els + awaitedEls.forEach((el) => { + vi.mocked(el.getSize).mockResolvedValue(expectedValue as unknown as Size & number) + }) + expect(awaitedEls.length).toEqual(2) + }) + + describe('given single expected value', async () => { + test('wait success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveSize(els, expectedValue, { beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveSize', + expectedValue: expectedValue, + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveSize', + expectedValue: expectedValue, + options: { beforeAssertion, afterAssertion }, + result + }) + }) + + test('wait but errors', async () => { + awaitedEls.forEach((el) => { + vi.mocked(el.getSize).mockRejectedValue(new Error('some error')) + }) + + await expect(() => thisContext.toHaveSize(els, expectedValue)) + .rejects.toThrow('some error') + }) + + test('no wait - failure', async () => { + awaitedEls.forEach((el) => { + vi.mocked(el.getSize).mockResolvedValue(wrongValue as unknown as Size & number) + }) + + const result = await thisContext.toHaveSize(els, expectedValue, { wait: 0 }) + + expect(result.pass).toBe(false) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have size + +- Expected - 2 ++ Received + 2 + + Array [ + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }, + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }, + ]` + ) + }) + + test('no wait - success', async () => { + const result = await thisContext.toHaveSize(els, expectedValue, { wait: 0 }) + + expect(result.pass).toBe(true) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + }) + + test('not - success - pass should false', async () => { + const result = await thisNotContext.toHaveSize(els, wrongValue) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveSize(els, expectedValue) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have size + +Expected [not]: [{"height": 32, "width": 32}, {"height": 32, "width": 32}] +Received : [{"height": 32, "width": 32}, {"height": 32, "width": 32}]` + ) + }) + }) + + describe('given multiple expected values', async () => { + const expectedSize = expectedValue + const expectedSizes = [expectedSize, expectedSize] + + test('wait - success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveSize(els, expectedSizes, { beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveSize', + expectedValue: expectedSizes, + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveSize', + expectedValue: expectedSizes, + options: { beforeAssertion, afterAssertion }, + result + }) + }) + + test('wait but error', async () => { + awaitedEls.forEach((el) => { + vi.mocked(el.getSize).mockRejectedValue(new Error('some error')) + }) + + await expect(() => thisContext.toHaveSize(els, expectedSizes)) + .rejects.toThrow('some error') + }) + + test('success on the first attempt', async () => { + const result = await thisContext.toHaveSize(els, expectedSizes) + + expect(result.pass).toBe(true) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + }) + + test('no wait - failure - all elements', async () => { + awaitedEls.forEach((el) => { + vi.mocked(el.getSize).mockResolvedValue(wrongValue as unknown as Size & number) + }) + + const result = await thisContext.toHaveSize(els, expectedSizes, { wait: 0 }) + + expect(result.pass).toBe(false) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have size + +- Expected - 2 ++ Received + 2 + + Array [ + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }, + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }, + ]` + ) + }) + + test('no wait - failure - first element', async () => { + vi.mocked(awaitedEls[0].getSize).mockResolvedValue(wrongValue as unknown as Size & number) + + const result = await thisContext.toHaveSize(els, expectedSizes, { wait: 0 }) + + expect(result.pass).toBe(false) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have size + +- Expected - 1 ++ Received + 1 + + Array [ + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }, + Object { + "height": 32, + "width": 32, + }, + ]` + ) + }) + + test('no wait - failure - second element', async () => { + vi.mocked(awaitedEls[1].getSize).mockResolvedValue(wrongValue as unknown as Size & number) + + const result = await thisContext.toHaveSize(els, expectedSizes, { wait: 0 }) + + expect(result.pass).toBe(false) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have size + +- Expected - 1 ++ Received + 1 + + Array [ + Object { + "height": 32, + "width": 32, + }, + Object { + "height": 32, +- "width": 32, ++ "width": 15, + }, + ]` + ) + }) + + test('no wait - success', async () => { + const result = await thisContext.toHaveSize(els, expectedSizes, { wait: 0 }) + + expect(result.pass).toBe(true) + awaitedEls.forEach((el) => + expect(el.getSize).toHaveBeenCalledTimes(1) + ) + }) + + test('not - failure - all elements - pass should be true', async () => { + const result = await thisNotContext.toHaveSize(els, expectedSizes) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have size + +Expected [not]: [{"height": 32, "width": 32}, {"height": 32, "width": 32}] +Received : [{"height": 32, "width": 32}, {"height": 32, "width": 32}]` + ) + }) + + test('not - failure - first element has same size - pass should be true', async () => { + vi.mocked(awaitedEls[0].getSize).mockResolvedValue(expectedSize as unknown as Size & number) + vi.mocked(awaitedEls[1].getSize).mockResolvedValue(wrongValue as unknown as Size & number) + + const result = await thisNotContext.toHaveSize(els, expectedSizes) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have size + +Expected [not]: [{"height": 32, "width": 32}, {"height": 32, "width": 32}] +Received : [{"height": 32, "width": 32}, {"height": 32, "width": 15}]` + ) + }) + + test('not - failure - one element has same size - pass should be true', async () => { + vi.mocked(awaitedEls[0].getSize).mockResolvedValue(wrongValue as unknown as Size & number) + vi.mocked(awaitedEls[1].getSize).mockResolvedValue(expectedSize as unknown as Size & number) + + const result = await thisNotContext.toHaveSize(els, expectedSizes) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have size + +Expected [not]: [{"height": 32, "width": 32}, {"height": 32, "width": 32}] +Received : [{"height": 32, "width": 15}, {"height": 32, "width": 32}]` + ) + }) + + test('should fails when expected is an array with a mismatched length', async () => { + const result = await thisContext.toHaveSize(elements, [expectedValue, expectedValue, expectedValue]) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have size + +- Expected - 12 ++ Received + 1 + + Array [ +- Object { +- "height": 32, +- "width": 32, +- }, +- Object { +- "height": 32, +- "width": 32, +- }, +- Object { +- "height": 32, +- "width": 32, +- }, ++ "Received array length 2, expected 3", + ]` + ) + }) + }) + + test('fails when no elements are provided', async () => { + const result = await thisContext.toHaveSize([], expectedValue) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect [] to have size Expected: {"height": 32, "width": 32} -Received: null`) +Received: undefined`) + }) }) }) diff --git a/test/matchers/element/toHaveStyle.test.ts b/test/matchers/element/toHaveStyle.test.ts index 225e5204e..8f7c766f8 100644 --- a/test/matchers/element/toHaveStyle.test.ts +++ b/test/matchers/element/toHaveStyle.test.ts @@ -1,298 +1,243 @@ -import { vi, test, describe, expect } from 'vitest' +import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' - import { toHaveStyle } from '../../../src/matchers/element/toHaveStyle.js' +import type { ParsedCSSValue } from 'webdriverio' vi.mock('@wdio/globals') -describe('toHaveStyle', () => { - let el: ChainablePromiseElement - const mockStyle: { [key: string]: string; } = { - 'font-family': 'Faktum', - 'font-size': '26px', - 'color': '#000' - } - - test('wait for success', async () => { - el = await $('sel') - el.getCSSProperty = vi.fn().mockResolvedValueOnce({ value: 'Wrong Value' }) - .mockResolvedValueOnce({ value: 'Wrong Value' }) - .mockImplementation((property: string) => { - return { value: mockStyle[property] } - }) - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() +describe(toHaveStyle, () => { - const result = await toHaveStyle.call({}, el, mockStyle, { ignoreCase: true, beforeAssertion, afterAssertion }) + let thisContext: { toHaveStyle: typeof toHaveStyle } + let thisNotContext: { isNot: true; toHaveStyle: typeof toHaveStyle } - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(6) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveStyle', - expectedValue: mockStyle, - options: { ignoreCase: true, beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveStyle', - expectedValue: mockStyle, - options: { ignoreCase: true, beforeAssertion, afterAssertion }, - result - }) + beforeEach(() => { + thisContext = { toHaveStyle } + thisNotContext = { isNot: true, toHaveStyle } }) - test('wait but failure', async () => { - el = await $('sel') - el.getCSSProperty = vi.fn().mockRejectedValue(new Error('some error')) + describe('given a single element', () => { + let el: ChainablePromiseElement - await expect(() => toHaveStyle.call({}, el, mockStyle, { ignoreCase: true })) - .rejects.toThrow('some error') - }) + const mockStyle: { [key: string]: string; } = { + 'font-family': 'Faktum', + 'font-size': '26px', + 'color': '#000' + } - test('success on the first attempt', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getCSSProperty).mockImplementation(async (property: string) => + ({ value: mockStyle[property], parsed: {} } satisfies ParsedCSSValue) + ) }) - const result = await toHaveStyle.call({}, el, mockStyle, { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) - - test('no wait - failure', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockResolvedValue({ value: 'Wrong Value' }) + test('wait for success', async () => { + vi.mocked(el.getCSSProperty).mockResolvedValueOnce({ value: 'Wrong Value', parsed: {} }) + .mockImplementation(async (property: string) => { + return { value: mockStyle[property], parsed: {} } + }) + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveStyle(el, mockStyle, { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }) + + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(6) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveStyle', + expectedValue: mockStyle, + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveStyle', + expectedValue: mockStyle, + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - const result = await toHaveStyle.call({}, el, mockStyle, { wait: 0 }) - expect(result.pass).toBe(false) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) + test('wait but failure', async () => { + vi.mocked(el.getCSSProperty).mockRejectedValue(new Error('some error')) - test('no wait - success', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } + await expect(() => thisContext.toHaveStyle(el, mockStyle, { ignoreCase: true, wait: 1 })) + .rejects.toThrow('some error') }) - const result = await toHaveStyle.call({}, el, mockStyle, { wait: 0 }) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) + test('success on the first attempt', async () => { + const result = await thisContext.toHaveStyle(el, mockStyle, { wait: 1, ignoreCase: true }) - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const result = await toHaveStyle.call({ isNot: true }, el, mockStyle, { wait: 0 }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to have style + test('no wait - failure', async () => { + vi.mocked(el.getCSSProperty).mockResolvedValue({ value: 'Wrong Value', parsed: {} }) -Expected [not]: {"color": "#000", "font-family": "Faktum", "font-size": "26px"} -Received : {"color": "#000", "font-family": "Faktum", "font-size": "26px"}` - ) - }) + const result = await thisContext.toHaveStyle(el, mockStyle, { wait: 0 }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } + expect(result.pass).toBe(false) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const wrongStyle: { [key: string]: string; } = { - 'font-family': 'Incorrect Font', - 'font-size': '100px', - 'color': '#fff' - } - const result = await toHaveStyle.bind({ isNot: true })(el, wrongStyle, { wait: 1 }) + test('no wait - success', async () => { + const result = await thisContext.toHaveStyle(el, mockStyle, { wait: 0 }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) - - test('should return false if styles dont match', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const wrongStyle: { [key: string]: string; } = { - 'font-family': 'Incorrect Font', - 'font-size': '100px', - 'color': '#fff' - } + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveStyle(el, mockStyle) - const result = await toHaveStyle.bind({ })(el, wrongStyle, { wait: 1 }) - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to have style - test('should return true if styles match', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } +Expected [not]: {"color": "#000", "font-family": "Faktum", "font-size": "26px"} +Received : {"color": "#000", "font-family": "Faktum", "font-size": "26px"}` + ) }) - const result = await toHaveStyle.bind({})(el, mockStyle, { wait: 1 }) - expect(result.pass).toBe(true) - }) + test('not - success - pass should be false', async () => { + const wrongStyle: { [key: string]: string; } = { + 'font-family': 'Incorrect Font', + 'font-size': '100px', + 'color': '#fff' + } - test('message shows correctly', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockResolvedValue({ value: 'Wrong Value' }) + const result = await thisNotContext.toHaveStyle(el, wrongStyle) - const result = await toHaveStyle.call({}, el, 'WebdriverIO' as any) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('message shows correctly', async () => { + vi.mocked(el.getCSSProperty).mockResolvedValue({ value: 'Wrong Value', parsed: {} }) - expect(result.pass).toBe(false) - expect(result.message()).toEqual(`\ + const result = await thisContext.toHaveStyle(el, 'WebdriverIO' as any) + + expect(result.message()).toEqual(`\ Expect $(\`sel\`) to have style Expected: "WebdriverIO" Received: {"0": "Wrong Value", "1": "Wrong Value", "10": "Wrong Value", "2": "Wrong Value", "3": "Wrong Value", "4": "Wrong Value", "5": "Wrong Value", "6": "Wrong Value", "7": "Wrong Value", "8": "Wrong Value", "9": "Wrong Value"}` - ) - - }) - - test('success if style matches with ignoreCase', async () => { - const el = await $('sel') - - const actualStyle: { [key: string]: string; } = { - 'font-family': 'Faktum', - 'font-size': '26px', - 'color': '#fff' - } - - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: actualStyle[property] } + ) }) - const alteredCaseStyle: { [key: string]: string; } = { - 'font-family': 'FaKtum', - 'font-size': '26px', - 'color': '#FFF' - } + test('success if style matches with ignoreCase', async () => { + const actualStyle: { [key: string]: string; } = { + 'font-family': 'Faktum', + 'font-size': '26px', + 'color': '#fff' + } - const result = await toHaveStyle.call({}, el, alteredCaseStyle, { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) + vi.mocked(el.getCSSProperty).mockImplementation(async (property: string) =>({ value: actualStyle[property], parsed: {} })) - test('success if style matches with trim', async () => { - const el = await $('sel') + const alteredCaseStyle: { [key: string]: string; } = { + 'font-family': 'FaKtum', + 'font-size': '26px', + 'color': '#FFF' + } - const actualStyle: { [key: string]: string; } = { - 'font-family': ' Faktum ', - 'font-size': ' 26px ', - 'color': ' #fff ' - } + const result = await thisContext.toHaveStyle(el, alteredCaseStyle, { wait: 0, ignoreCase: true }) - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: actualStyle[property] } + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const alteredSpaceStyle: { [key: string]: string; } = { - 'font-family': 'Faktum', - 'font-size': '26px', - 'color': '#fff' - } + test('success if style matches with trim', async () => { + const actualStyle: { [key: string]: string; } = { + 'font-family': ' Faktum ', + 'font-size': ' 26px ', + 'color': ' #fff ' + } - const result = await toHaveStyle.call({}, el, alteredSpaceStyle, { trim: true }) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) + vi.mocked(el.getCSSProperty).mockImplementation(async (property: string) => ({ value: actualStyle[property], parsed: {} })) - test('sucess if style matches with containing', async () => { - const el = await $('sel') - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: mockStyle[property] } - }) - - const result = await toHaveStyle.call( - {}, - el, - { + const alteredSpaceStyle: { [key: string]: string; } = { 'font-family': 'Faktum', - 'font-size': '26', - color: '000', - }, - { containing: true } - ) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) - - test('sucess if style matches with atStart', async () => { - const el = await $('sel') + 'font-size': '26px', + 'color': '#fff' + } - const actualStyle: { [key: string]: string } = { - 'font-family': 'Faktum Lorem ipsum dolor sit amet', - 'text-rendering': 'optimizeLegibility', - 'overflow-wrap': 'break-word', - } - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: actualStyle[property] } + const result = await thisContext.toHaveStyle(el, alteredSpaceStyle, { wait: 0, trim: true }) + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const result = await toHaveStyle.call( - {}, - el, - { - 'font-family': 'Faktum', - 'text-rendering': 'optimize', - 'overflow-wrap': 'break', - }, - { atStart: true } - ) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) - - test('sucess if style matches with atEnd', async () => { - const el = await $('sel') - const actualStyle: { [key: string]: string } = { - 'font-family': 'Faktum Lorem ipsum dolor sit amet', - 'text-rendering': 'optimizeLegibility', - 'overflow-wrap': 'break-word', - } - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: actualStyle[property] } + test('sucess if style matches with containing', async () => { + const result = await thisNotContext.toHaveStyle( + el, + { + 'font-family': 'Faktum', + 'font-size': '26', + color: '000', + }, + { wait: 1, containing: true } + ) + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const result = await toHaveStyle.call( - {}, - el, - { - 'font-family': 'sit amet', - 'text-rendering': 'Legibility', - 'overflow-wrap': '-word', - }, - { atEnd: true } - ) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) - }) - - test('sucess if style matches with atIndex', async () => { - const el = await $('sel') - const actualStyle: { [key: string]: string } = { - 'font-family': 'Faktum Lorem ipsum dolor sit amet', - 'text-rendering': 'optimizeLegibility', - 'overflow-wrap': 'break-word', - } - el.getCSSProperty = vi.fn().mockImplementation((property: string) => { - return { value: actualStyle[property] } + test('sucess if style matches with atStart', async () => { + const actualStyle: { [key: string]: string } = { + 'font-family': 'Faktum Lorem ipsum dolor sit amet', + 'text-rendering': 'optimizeLegibility', + 'overflow-wrap': 'break-word', + } + vi.mocked(el.getCSSProperty).mockImplementation(async (property: string) => ({ value: actualStyle[property], parsed: {} })) + + const result = await thisContext.toHaveStyle( + el, + { + 'font-family': 'Faktum', + 'text-rendering': 'optimize', + 'overflow-wrap': 'break', + }, + { atStart: true } + ) + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) }) - const result = await toHaveStyle.call({}, el, - { - 'font-family': 'tum Lorem ipsum dolor sit amet', - 'text-rendering': 'imizeLegibility', - 'overflow-wrap': 'ak-word', - }, - { atIndex: 3 }) + test('sucess if style matches with atEnd', async () => { + const actualStyle: { [key: string]: string } = { + 'font-family': 'Faktum Lorem ipsum dolor sit amet', + 'text-rendering': 'optimizeLegibility', + 'overflow-wrap': 'break-word', + } + vi.mocked(el.getCSSProperty).mockImplementation(async (property: string) => ({ value: actualStyle[property], parsed: {} })) + + const result = await thisContext.toHaveStyle( + el, + { + 'font-family': 'sit amet', + 'text-rendering': 'Legibility', + 'overflow-wrap': '-word', + }, + { atEnd: true } + ) + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) + }) - expect(result.pass).toBe(true) - expect(el.getCSSProperty).toHaveBeenCalledTimes(3) + test('sucess if style matches with atIndex', async () => { + const actualStyle: { [key: string]: string } = { + 'font-family': 'Faktum Lorem ipsum dolor sit amet', + 'text-rendering': 'optimizeLegibility', + 'overflow-wrap': 'break-word', + } + vi.mocked(el.getCSSProperty).mockImplementation(async (property: string) => ({ value: actualStyle[property], parsed: {} })) + + const result = await thisContext.toHaveStyle(el, + { + 'font-family': 'tum Lorem ipsum dolor sit amet', + 'text-rendering': 'imizeLegibility', + 'overflow-wrap': 'ak-word', + }, + { atIndex: 3 }) + + expect(result.pass).toBe(true) + expect(el.getCSSProperty).toHaveBeenCalledTimes(3) + }) }) - }) diff --git a/test/matchers/element/toHaveText.test.ts b/test/matchers/element/toHaveText.test.ts index 92aa72a29..9fc303cb5 100755 --- a/test/matchers/element/toHaveText.test.ts +++ b/test/matchers/element/toHaveText.test.ts @@ -1,382 +1,677 @@ import { $, $$ } from '@wdio/globals' import { beforeEach, describe, expect, test, vi } from 'vitest' - -import { getExpectMessage, getReceived, getExpected } from '../../__fixtures__/utils.js' import { toHaveText } from '../../../src/matchers/element/toHaveText.js' -import type { ChainablePromiseArray } from 'webdriverio' +import type { ChainablePromiseArray, ChainablePromiseElement } from 'webdriverio' +import { notFoundElementFactory } from '../../__mocks__/@wdio/globals.js' vi.mock('@wdio/globals') -describe('toHaveText', () => { - describe('when receiving an element array', () => { - let els: ChainablePromiseArray +describe(toHaveText, async () => { + + let thisContext: { toHaveText: typeof toHaveText; isNot?: boolean } + let thisNotContext: { toHaveText: typeof toHaveText; isNot: true } + + beforeEach(() => { + thisContext = { toHaveText } + thisNotContext = { toHaveText, isNot: true } + }) + + describe.each([ + { element: await $('sel'), title: 'awaited ChainablePromiseElement' }, + { element: await $('sel').getElement(), title: 'awaited getElement of ChainablePromiseElement (e.g. WebdriverIO.Element)' }, + { element: $('sel'), title: 'non-awaited of ChainablePromiseElement' }, + + // Since Promise Type is not supported the below is not official even if it works, should we support it? TODO delete or remove casting `as unknown as ChainablePromiseArray` + // { element: $('sel').getElement() as unknown as ChainablePromiseElement, selectorName: '', title: 'non-awaited getElements of ChainablePromiseArray' } + ])('given a single element when $title', ({ element }) => { + let el: ChainablePromiseElement | WebdriverIO.Element beforeEach(async () => { - els = await $$('parent') + el = element + }) + + test('wait for success', async () => { + vi.mocked(el.getText).mockResolvedValueOnce('').mockResolvedValueOnce('').mockResolvedValueOnce('webdriverio') + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveText(el, 'WebdriverIO', { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }) - const el1: ChainablePromiseElement = await $('sel') - el1.getText = vi.fn().mockResolvedValue('WebdriverIO') + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(3) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveText', + expectedValue: 'WebdriverIO', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveText', + expectedValue: 'WebdriverIO', + options: { ignoreCase: true, beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - const el2: ChainablePromiseElement = await $('dev') - el2.getText = vi.fn().mockResolvedValue('Get Started') + test('wait but error', async () => { + vi.mocked(el.getText).mockRejectedValue(new Error('some error')) - els[0] = el1 - els[1] = el2 + await expect(() => thisContext.toHaveText(el, 'WebdriverIO', { ignoreCase: true, wait: 500 })) + .rejects.toThrow('some error') }) - test('should return true if the received element array matches the expected text array', async () => { - const result = await toHaveText.bind({})(els, ['WebdriverIO', 'Get Started']) + test('success and trim by default', async () => { + vi.mocked(el.getText).mockResolvedValue(' WebdriverIO ') + + const result = await thisContext.toHaveText(el, 'WebdriverIO') expect(result.pass).toBe(true) }) - test('should return true if the received element array matches the expected text array & ignoreCase', async () => { - const result = await toHaveText.bind({})(els, ['webdriverio', 'get started'], { ignoreCase: true }) + test('success on the first attempt', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') + + const result = await thisContext.toHaveText(el, 'WebdriverIO', { ignoreCase: true }) expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) }) - test('should return false if the received element array does not match the expected text array', async () => { - const result = await toHaveText.bind({})(els, ['webdriverio', 'get started']) + test('no wait - failure', async () => { + vi.mocked(el.getText).mockResolvedValue('webdriverio') + + const result = await thisContext.toHaveText(el, 'WebdriverIO', { wait: 0 }) + expect(result.pass).toBe(false) - }) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have text - test('should return true if the expected message shows correctly', async () => { - const result = await toHaveText.bind({})(els, ['webdriverio', 'get started'], { message: 'Test' }) - expect(getExpectMessage(result.message())).toContain('Test') +Expected: "WebdriverIO" +Received: "webdriverio"` + ) + expect(el.getText).toHaveBeenCalledTimes(1) }) - }) - test('wait for success', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValueOnce('').mockResolvedValueOnce('').mockResolvedValueOnce('webdriverio') - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() + test('no wait - success', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.call({}, el, 'WebdriverIO', { ignoreCase: true, beforeAssertion, afterAssertion }) + const result = await thisContext.toHaveText(el, 'WebdriverIO', { wait: 0 }) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(3) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveText', - expectedValue: 'WebdriverIO', - options: { ignoreCase: true, beforeAssertion, afterAssertion } + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveText', - expectedValue: 'WebdriverIO', - options: { ignoreCase: true, beforeAssertion, afterAssertion }, - result + + test('not - failure - pass should be true', async () => { + + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') + + const result = await thisNotContext.toHaveText(el, 'WebdriverIO') + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) not to have text + +Expected [not]: "WebdriverIO" +Received : "WebdriverIO"`) }) - }) - test('wait but failure', async () => { - const el = await $('sel') - el.getText = vi.fn().mockRejectedValue(new Error('some error')) + test('not - success - pass should be false', async () => { - await expect(() => toHaveText.call({}, el, 'WebdriverIO', { ignoreCase: true })) - .rejects.toThrow('some error') - }) + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - test('success on the first attempt', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + const result = await thisNotContext.toHaveText(el, 'Not Desired') - const result = await toHaveText.call({}, el, 'WebdriverIO', { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - test('no wait - failure', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('webdriverio') + test("should return false if texts don't match when trimming is disabled", async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.call({}, el, 'WebdriverIO', { wait: 0 }) + const result = await thisContext.toHaveText(el, 'foobar', { trim: false, wait: 0 }) + expect(result.pass).toBe(false) + }) - expect(result.pass).toBe(false) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + test('should return true if texts strictly match without trimming', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - test('no wait - success', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + const result = await thisContext.toHaveText(el, 'WebdriverIO', { trim: false }) - const result = await toHaveText.call({}, el, 'WebdriverIO', { wait: 0 }) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + test('should return true if actual text + single replacer matches the expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + const result = await thisContext.toHaveText(el, 'BrowserdriverIO', { replace: ['Web', 'Browser'] }) - const result = await toHaveText.call({ isNot: true }, el, 'WebdriverIO', { wait: 0 }) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to have text + test('should return true if actual text + replace (string) matches the expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') -Expected [not]: "WebdriverIO" -Received : "WebdriverIO"` - ) - }) + const result = await thisContext.toHaveText(el, 'BrowserdriverIO', { replace: [['Web', 'Browser']] }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + expect(result.pass).toBe(true) + }) - const result = await toHaveText.call({ isNot: true }, el, 'not WebdriverIO', { wait: 0 }) + test('should return true if actual text + replace (regex) matches the expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + const result = await thisContext.toHaveText(el, 'BrowserdriverIO', { replace: [[/Web/, 'Browser']] }) - test('not with no trim - failure - pass should be true', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue(' WebdriverIO ') + expect(result.pass).toBe(true) + }) - const result = await toHaveText.call({ isNot: true }, el, ' WebdriverIO ', { trim: false, wait: 0 }) + test('should return true if actual text starts with expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ -Expect $(\`sel\`) not to have text + const result = await thisContext.toHaveText(el, 'Web', { atStart: true }) -Expected [not]: " WebdriverIO " -Received : " WebdriverIO "` - ) - }) + expect(result.pass).toBe(true) + }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual text ends with expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.call({ isNot: true }, el, 'not WebdriverIO', { wait: 0 }) + const result = await thisContext.toHaveText(el, 'IO', { atEnd: true }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + expect(result.pass).toBe(true) + }) - test('should return true if texts match', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('should return true if actual text contains the expected text at the given index', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.bind({})(el, 'WebdriverIO', { wait: 1 }) - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveText(el, 'iverIO', { atIndex: 5 }) - test('should return true if actual text + single replacer matches the expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + expect(result.pass).toBe(true) + }) - const result = await toHaveText.bind({})(el, 'BrowserdriverIO', { replace: ['Web', 'Browser'] }) + test('message', async () => { + vi.mocked(el.getText).mockResolvedValue('') - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveText(el, 'WebdriverIO') - test('should return true if actual text + replace (string) matches the expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have text - const result = await toHaveText.bind({})(el, 'BrowserdriverIO', { replace: [['Web', 'Browser']] }) +Expected: "WebdriverIO" +Received: ""` + ) + }) - expect(result.pass).toBe(true) - }) + test('success if array matches with text and ignoreCase', async () => { - test('should return true if actual text + replace (regex) matches the expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + vi.mocked(el.getText).mockResolvedValue('webdriverio') - const result = await toHaveText.bind({})(el, 'BrowserdriverIO', { replace: [[/Web/, 'Browser']] }) + const result = await thisContext.toHaveText(el, ['WDIO', 'Webdriverio'], { ignoreCase: true }) + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(true) - }) + test('success if array matches with text and trim', async () => { - test('should return true if actual text starts with expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + vi.mocked(el.getText).mockResolvedValue(' WebdriverIO ') - const result = await toHaveText.bind({})(el, 'Web', { atStart: true }) + const result = await thisContext.toHaveText(el, ['WDIO', 'WebdriverIO', 'toto'], { trim: true }) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) + }) - test('should return true if actual text ends with expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('success if array matches with text and replace (string)', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.bind({})(el, 'IO', { atEnd: true }) + const result = await thisContext.toHaveText(el, ['WDIO', 'BrowserdriverIO', 'toto'], { replace: [['Web', 'Browser']] }) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) + }) - test('should return true if actual text contains the expected text at the given index', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('success if array matches with text and replace (regex)', async () => { - const result = await toHaveText.bind({})(el, 'iverIO', { atIndex: 5 }) + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - expect(result.pass).toBe(true) - }) + const result = await thisContext.toHaveText(el, ['WDIO', 'BrowserdriverIO', 'toto'], { replace: [[/Web/g, 'Browser']] }) - test('message', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('') + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) + }) - const result = await toHaveText.call({}, el, 'WebdriverIO') + test('success if array matches with text and multiple replacers and one of the replacers is a function', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - expect(getExpectMessage(result.message())).toContain('to have text') - }) + const result = await thisContext.toHaveText(el, ['WDIO', 'browserdriverio', 'toto'], { + replace: [ + [/Web/g, 'Browser'], + [/[A-Z]/g, (match: string) => match.toLowerCase()], + ], + }) - test('success if array matches with text and ignoreCase', async () => { - const el = await $('sel') + expect(result.pass).toBe(true) + expect(el.getText).toHaveBeenCalledTimes(1) + }) - el.getText = vi.fn().mockResolvedValue('webdriverio') + test('failure if array does not match with text', async () => { - const result = await toHaveText.call({}, el, ['WDIO', 'Webdriverio'], { ignoreCase: true }) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') + const result = await thisContext.toHaveText(el, ['WDIO', 'Webdriverio']) - test('success if array matches with text and trim', async () => { - const el = await $('sel') + expect(result.pass).toBe(false) + expect(el.getText).toHaveBeenCalledTimes(1) + }) - el.getText = vi.fn().mockResolvedValue(' WebdriverIO ') + test('should return true if actual text contains the expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.call({}, el, ['WDIO', 'WebdriverIO', 'toto'], { trim: true }) + const result = await thisContext.toHaveText(el, expect.stringContaining('iverIO'), {}) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + }) - test('success if array matches with text and replace (string)', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('should return false if actual text does not contain the expected text', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - const result = await toHaveText.call({}, el, ['WDIO', 'BrowserdriverIO', 'toto'], { replace: [['Web', 'Browser']] }) + const result = await thisContext.toHaveText(el, expect.stringContaining('WDIO')) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(false) + }) - test('success if array matches with text and replace (regex)', async () => { - const el = await $('sel') + test('should return true if actual text contains one of the expected texts', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + const result = await thisContext.toHaveText(el, [expect.stringContaining('iverIO'), expect.stringContaining('WDIO')], {}) - const result = await toHaveText.call({}, el, ['WDIO', 'BrowserdriverIO', 'toto'], { replace: [[/Web/g, 'Browser']] }) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + test('should return false if actual text does not contain the expected texts', async () => { + vi.mocked(el.getText).mockResolvedValue('WebdriverIO') - test('success if array matches with text and multiple replacers and one of the replacers is a function', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + const result = await thisContext.toHaveText(el, [expect.stringContaining('EXAMPLE'), expect.stringContaining('WDIO')]) - const result = await toHaveText.call({}, el, ['WDIO', 'browserdriverio', 'toto'], { - replace: [ - [/Web/g, 'Browser'], - [/[A-Z]/g, (match: string) => match.toLowerCase()], - ], + expect(result.pass).toBe(false) }) - expect(result.pass).toBe(true) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + describe('with RegExp', () => { + let el: ChainablePromiseElement - test('failure if array does not match with text', async () => { - const el = await $('sel') + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getText).mockResolvedValue('This is example text') + }) - el.getText = vi.fn().mockResolvedValue('WebdriverIO') - const result = await toHaveText.call({}, el, ['WDIO', 'Webdriverio'], { wait: 1 }) + test('success if match', async () => { + const result = await thisContext.toHaveText(el, /ExAmplE/i) - expect(result.pass).toBe(false) - expect(el.getText).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + }) - test('should return true if actual text contains the expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('success if array matches with RegExp', async () => { + const result = await thisContext.toHaveText(el, ['WDIO', /ExAmPlE/i]) - const result = await toHaveText.bind({})(el, expect.stringContaining('iverIO'), {}) + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) - }) + test('success if array matches with text', async () => { + const result = await thisContext.toHaveText(el, ['This is example text', /Webdriver/i]) - test('should return false if actual text does not contain the expected text', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + expect(result.pass).toBe(true) + }) - const result = await toHaveText.bind({})(el, expect.stringContaining('WDIO'), {}) + test('success if array matches with text and ignoreCase', async () => { + const result = await thisContext.toHaveText(el, ['ThIs Is ExAmPlE tExT', /Webdriver/i], { + ignoreCase: true, + }) - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(true) + }) - test('should return true if actual text contains one of the expected texts', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('failure if no match', async () => { + const result = await thisContext.toHaveText(el, /Webdriver/i) - const result = await toHaveText.bind({})(el, [expect.stringContaining('iverIO'), expect.stringContaining('WDIO')], {}) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have text - expect(result.pass).toBe(true) - }) +Expected: /Webdriver/i +Received: "This is example text"` + ) + }) - test('should return false if actual text does not contain the expected texts', async () => { - const el = await $('sel') - el.getText = vi.fn().mockResolvedValue('WebdriverIO') + test('failure if array does not match with text', async () => { + const result = await thisContext.toHaveText(el, ['WDIO', /Webdriver/i]) - const result = await toHaveText.bind({})(el, [expect.stringContaining('EXAMPLE'), expect.stringContaining('WDIO')], {}) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have text - expect(result.pass).toBe(false) +Expected: ["WDIO", /Webdriver/i] +Received: "This is example text"` + ) + }) + }) }) - describe('with RegExp', () => { - let el: ChainablePromiseElement + describe.each([ + { elements: await $$('sel'), title: 'awaited ChainablePromiseArray' }, + { elements: await $$('sel').getElements(), title: 'awaited getElements of ChainablePromiseArray (e.g. WebdriverIO.ElementArray)' }, + { elements: await $$('sel').filter((t) => t.isEnabled()), selectorName: '$(`sel`), $(`dev`)', title: 'awaited filtered ChainablePromiseArray (e.g. WebdriverIO.Element[])' }, + { elements: $$('sel'), title: 'non-awaited of ChainablePromiseArray' }, + + // Since Promise Type is not supported the below is not official even if it works, should we support it? TODO delete or remove casting `as unknown as ChainablePromiseArray` + { elements: $$('sel').filter((t) => t.isEnabled()) as unknown as ChainablePromiseArray, selectorName: '$(`sel`), $(`dev`)', title: 'non-awaited filtered ChainablePromiseArray' }, + { elements: $$('sel').getElements() as unknown as ChainablePromiseArray, title: 'non-awaited getElements of ChainablePromiseArray' } + ])('given multiple elements when $title', ({ elements, selectorName }) => { + let els: ChainablePromiseArray | WebdriverIO.ElementArray | WebdriverIO.Element[] + selectorName = selectorName || '$$(`sel`)' beforeEach(async () => { - el = await $('sel') - el.getText = vi.fn().mockResolvedValue('This is example text') + els = elements + + const awaitedEls = await els + awaitedEls[0] = await $('sel') + awaitedEls[1] = await $('dev') }) - test('success if match', async () => { - const result = await toHaveText.call({}, el, /ExAmplE/i) + describe('given single expected values', () => { + beforeEach(async () => { + els = elements - expect(result.pass).toBe(true) - }) + const awaitedEls = await els + expect(awaitedEls.length).toBe(2) + awaitedEls.forEach(el => vi.mocked(el.getText).mockResolvedValue('WebdriverIO')) + }) - test('success if array matches with RegExp', async () => { - const result = await toHaveText.call({}, el, ['WDIO', /ExAmPlE/i]) + test('should return true if the received element array matches the expected text array', async () => { + const result = await thisContext.toHaveText(els, 'WebdriverIO') + expect(result.pass).toBe(true) + }) - expect(result.pass).toBe(true) - }) + test('should return true if the received element array matches the expected text array & ignoreCase', async () => { + const result = await thisContext.toHaveText(els, 'webdriverio', { ignoreCase: true }) + expect(result.pass).toBe(true) + }) + + test('should return false if the received element array does not match the expected text array', async () => { + const result = await thisContext.toHaveText(els, 'webdriverio') + expect(result.pass).toBe(false) + }) - test('success if array matches with text', async () => { - const result = await toHaveText.call({}, el, ['This is example text', /Webdriver/i]) + test('should return false when first element does not match', async () => { + vi.mocked((await els)[0].getText).mockResolvedValueOnce('Wrong') + vi.mocked((await els)[1].getText).mockResolvedValueOnce('webdriverio') - expect(result.pass).toBe(true) + const result = await thisContext.toHaveText(els, 'webdriverio', { message: 'Test', wait: 0 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Test +Expect ${selectorName} to have text + +- Expected - 1 ++ Received + 1 + + Array [ +- "webdriverio", ++ "Wrong", + "webdriverio", + ]` + ) + }) + + test('should return false when second element does not match', async () => { + vi.mocked((await els)[0].getText).mockResolvedValueOnce('webdriverio') + vi.mocked((await els)[1].getText).mockResolvedValueOnce('Wrong') + + const result = await thisContext.toHaveText(els, 'webdriverio', { message: 'Test', wait: 0 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Test +Expect ${selectorName} to have text + +- Expected - 1 ++ Received + 1 + + Array [ + "webdriverio", +- "webdriverio", ++ "Wrong", + ]` + ) + }) + + test('should shows custom failure message', async () => { + const result = await thisContext.toHaveText(els, 'webdriverio', { message: 'Test', wait: 0 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Test +Expect ${selectorName} to have text + +- Expected - 2 ++ Received + 2 + + Array [ +- "webdriverio", +- "webdriverio", ++ "WebdriverIO", ++ "WebdriverIO", + ]` + ) + }) }) - test('success if array matches with text and ignoreCase', async () => { - const result = await toHaveText.call({}, el, ['ThIs Is ExAmPlE tExT', /Webdriver/i], { - ignoreCase: true, + describe('given multiples expected values', () => { + let awaitedElements: WebdriverIO.Element[] | WebdriverIO.ElementArray| ChainablePromiseArray + beforeEach(async () => { + els = elements + + awaitedElements = await els + vi.mocked(awaitedElements[0].getText).mockResolvedValue('WebdriverIO') + vi.mocked(awaitedElements[1].getText).mockResolvedValue('Get Started') }) - expect(result.pass).toBe(true) + test('should return true if the received elements', async () => { + const result = await thisContext.toHaveText(els, ['WebdriverIO', 'Get Started']) + expect(result.pass).toBe(true) + }) + + test('should return true if the received elements and trim by default', async () => { + const awaitedEls = await els + vi.mocked(awaitedEls[0].getText).mockResolvedValue(' WebdriverIO ') + vi.mocked(awaitedEls[1].getText).mockResolvedValue(' Get Started ') + + const result = await thisContext.toHaveText(els, ['WebdriverIO', 'Get Started']) + + expect(result.pass).toBe(true) + }) + + test('should return true if the received element array matches the expected text array & ignoreCase', async () => { + const result = await thisContext.toHaveText(els, ['webdriverio', 'get started'], { ignoreCase: true }) + expect(result.pass).toBe(true) + }) + + test('should return false if the received element array does not match the expected text array', async () => { + const result = await thisContext.toHaveText(els, ['webdriverio', 'get started']) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have text + +- Expected - 2 ++ Received + 2 + + Array [ +- "webdriverio", +- "get started", ++ "WebdriverIO", ++ "Get Started", + ]` + ) + }) + + test('should return false if the first received element array does not match the first expected text in the array', async () => { + const result = await thisContext.toHaveText(els, ['webdriverIO', 'Get Started']) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have text + +- Expected - 1 ++ Received + 1 + + Array [ +- "webdriverIO", ++ "WebdriverIO", + "Get Started", + ]` + ) + }) + + test('should return false if the second received element array does not match the second expected text in the array', async () => { + const result = await thisContext.toHaveText(els, ['WebdriverIO', 'get started']) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have text + +- Expected - 1 ++ Received + 1 + + Array [ + "WebdriverIO", +- "get started", ++ "Get Started", + ]` + ) + }) + + // TODO legacy behavior to be removed in future major release + test('should return true when trying to match non-indexed + more texts than elements (legacy behavior to deprecated)', async () => { + vi.mocked((await els)[0].getText).mockResolvedValueOnce('webdriverio1') + vi.mocked((await els)[1].getText).mockResolvedValueOnce('webdriverio2') + + const result = await thisContext.toHaveText(els, ['webdriverio2', 'webdriverio1', 'webdriverio']) + + expect(result.pass).toBe(true) + }) + + test('should return false when trying to match non-indexed + more texts than elements (legacy behavior to deprecated) but nothing match', async () => { + vi.mocked((await els)[0].getText).mockResolvedValueOnce('webdriverio1') + vi.mocked((await els)[1].getText).mockResolvedValueOnce('webdriverio2') + + const result = await thisContext.toHaveText(els, ['webdriverio', 'webdriverio', 'webdriverIO']) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have text + +- Expected - 3 ++ Received + 2 + + Array [ +- "webdriverio", +- "webdriverio", +- "webdriverIO", ++ "webdriverio1", ++ "webdriverio2", + ]` + ) + }) + + test('should return false and display proper custom error message', async () => { + const result = await thisContext.toHaveText(els, ['webdriverio', 'get started'], { message: 'Test', wait: 0 }) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Test +Expect ${selectorName} to have text + +- Expected - 2 ++ Received + 2 + + Array [ +- "webdriverio", +- "get started", ++ "WebdriverIO", ++ "Get Started", + ]` + ) + }) + + test('not - failure on both elements - pass should be true', async () => { + const result = await thisNotContext.toHaveText(elements, ['WebdriverIO', 'Get Started']) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have text + +Expected [not]: ["WebdriverIO", "Get Started"] +Received : ["WebdriverIO", "Get Started"]`) + }) + + test('not - failure on first element only - pass should be true', async () => { + const result = await thisNotContext.toHaveText(elements, ['WebdriverIO', 'OK Get Started']) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to have text + +Expected [not]: ["WebdriverIO", "OK Get Started"] +Received : ["WebdriverIO", "Get Started"]`) + }) + + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveText(elements, ['NOT WebdriverIO', 'NOT Get Started']) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) }) + }) - test('failure if no match', async () => { - const result = await toHaveText.call({}, el, /Webdriver/i) + describe('Edge cases', () => { + test('should have pass false with proper error message when actual is an empty array of elements', async () => { + const result = await thisContext.toHaveText([], 'webdriverio') expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have text') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getReceived(result.message())).toContain('This is example text') + expect(result.message()).toEqual(`\ +Expect [] to have text + +Expected: "webdriverio" +Received: undefined`) }) - test('failure if array does not match with text', async () => { - const result = await toHaveText.call({}, el, ['WDIO', /Webdriver/i]) + // TODO view later to handle this case more gracefully + test('given element is not found then it throws error when an element does not exists', async () => { + const element: WebdriverIO.Element = notFoundElementFactory('sel') + + await expect(thisContext.toHaveText(element, 'webdriverio')).rejects.toThrow("Can't call getText on element with selector sel because element wasn't found") + }) + + // TODO view later to handle this case more gracefully + test('given element from out of bound ChainableArray, then it throws error when an element does not exists', async () => { + const element: ChainablePromiseElement = $$('elements')[3] + + await expect(thisContext.toHaveText(element, 'webdriverio')).rejects.toThrow('Index out of bounds! $$(elements) returned only 2 elements.') + }) + + test.for([ + { actual: undefined, selectorName: 'undefined' }, + { actual: null, selectorName: 'null' }, + { actual: true, selectorName: 'true' }, + { actual: 5, selectorName: '5' }, + { actual: 'test', selectorName: 'test' }, + { actual: Promise.resolve(true), selectorName: 'true' }, + { actual: {}, selectorName: '{}' }, + { actual: ['1', '2'], selectorName: '["1","2"]' }, + ])('should have pass false with proper error message when actual is unsupported type of $actual', async ({ actual, selectorName }) => { + const result = await thisContext.toHaveText(actual as any, 'webdriverio') expect(result.pass).toBe(false) - expect(getExpectMessage(result.message())).toContain('to have text') - expect(getExpected(result.message())).toContain('/Webdriver/i') - expect(getExpected(result.message())).toContain('WDIO') + expect(result.message()).toEqual(`\ +Expect ${selectorName} to have text + +Expected: "webdriverio" +Received: undefined`) }) }) }) diff --git a/test/matchers/element/toHaveValue.test.ts b/test/matchers/element/toHaveValue.test.ts index f5b49e01d..87c9b2f66 100755 --- a/test/matchers/element/toHaveValue.test.ts +++ b/test/matchers/element/toHaveValue.test.ts @@ -1,98 +1,94 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' import { $ } from '@wdio/globals' -import { getExpectMessage, getReceived, getExpected } from '../../__fixtures__/utils.js' import { toHaveValue } from '../../../src/matchers/element/toHaveValue.js' import type { AssertionResult } from 'expect-webdriverio' vi.mock('@wdio/globals') -describe('toHaveValue', () => { - let el: ChainablePromiseElement +describe(toHaveValue, () => { - beforeEach(async () => { - el = await $('sel') - el.getProperty = vi.fn().mockResolvedValue('This is an example value') + let thisContext: { toHaveValue: typeof toHaveValue } + + beforeEach(() => { + thisContext = { toHaveValue } }) - describe('success', () => { - test('exact passes', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() + describe('given single element', () => { + let el: ChainablePromiseElement - const result = await toHaveValue.call({}, el, 'This is an example value', { beforeAssertion, afterAssertion }) + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getProperty).mockResolvedValue('This is an example value') + }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveElementProperty', - expectedValue: ['value', 'This is an example value'], - options: { beforeAssertion, afterAssertion } + describe('success', () => { + test('exact passes', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveValue(el, 'This is an example value', { wait: 0, beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveValue', + expectedValue: ['value', 'This is an example value'], + options: { beforeAssertion, afterAssertion, wait: 0 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveValue', + expectedValue: ['value', 'This is an example value'], + options: { beforeAssertion, afterAssertion, wait: 0 }, + result + }) }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveElementProperty', - expectedValue: ['value', 'This is an example value'], - options: { beforeAssertion, afterAssertion }, - result - }) - }) - test('assymetric passes', async () => { - const result = await toHaveValue.call({}, el, expect.stringContaining('example value')) + test('assymetric passes', async () => { + const result = await thisContext.toHaveValue(el, expect.stringContaining('example value')) - expect(result.pass).toBe(true) - }) + expect(result.pass).toBe(true) + }) - test('RegExp passes', async () => { - const result = await toHaveValue.call({}, el, /ExAmPlE/i) + test('RegExp passes', async () => { + const result = await thisContext.toHaveValue(el, /ExAmPlE/i) - expect(result.pass).toBe(true) + expect(result.pass).toBe(true) + }) }) - }) - describe('failure', () => { - let result: AssertionResult + describe('failure', () => { + let result: AssertionResult - beforeEach(async () => { - result = await toHaveValue.call({}, el, 'webdriver') - }) + beforeEach(async () => { + result = await thisContext.toHaveValue(el, 'webdriver') + }) - test('does not pass', () => { - expect(result.pass).toBe(false) - }) + test('does not pass with proper failure message', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have property value - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have property value') - }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('webdriver') - }) - test('received message', () => { - expect(getReceived(result.message())).toContain('This is an example value') +Expected: "webdriver" +Received: "This is an example value"` + ) }) }) - }) - describe('failure with RegExp', () => { - let result: AssertionResult + describe('failure with RegExp', () => { + let result: AssertionResult - beforeEach(async () => { - result = await toHaveValue.call({}, el, /WDIO/) - }) + beforeEach(async () => { + result = await thisContext.toHaveValue(el, /WDIO/) + }) - test('does not pass', () => { - expect(result.pass).toBe(false) - }) + test('does not pass with proper failure message', () => { + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have property value - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to have property value') - }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('/WDIO/') - }) - test('received message', () => { - expect(getReceived(result.message())).toContain('This is an example value') +Expected: /WDIO/ +Received: "This is an example value"` + ) }) }) }) diff --git a/test/matchers/element/toHaveWidth.test.ts b/test/matchers/element/toHaveWidth.test.ts index 8a13f0700..39900e9b6 100755 --- a/test/matchers/element/toHaveWidth.test.ts +++ b/test/matchers/element/toHaveWidth.test.ts @@ -1,133 +1,284 @@ -import { vi, test, describe, expect } from 'vitest' -import { $ } from '@wdio/globals' - -import { getExpectMessage } from '../../__fixtures__/utils.js' +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' import { toHaveWidth } from '../../../src/matchers/element/toHaveWidth.js' +import type { Size } from '../../../src/matchers/element/toHaveSize.js' vi.mock('@wdio/globals') -describe('toHaveWidth', () => { - test('wait for success', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() +describe(toHaveWidth, () => { - const result = await toHaveWidth.call({}, el, 50, { beforeAssertion, afterAssertion }) + let thisContext: { toHaveWidth: typeof toHaveWidth } + let thisNotContext: { toHaveWidth: typeof toHaveWidth, isNot: boolean } - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toHaveWidth', - expectedValue: 50, - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toHaveWidth', - expectedValue: 50, - options: { beforeAssertion, afterAssertion }, - result - }) + beforeEach(() => { + thisContext = { toHaveWidth } + thisNotContext = { toHaveWidth, isNot: true } }) - test('wait but failure', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockRejectedValue(new Error('some error')) + describe('given single element', () => { + let el: ChainablePromiseElement - await expect(() => toHaveWidth.call({}, el, 10, {})) - .rejects.toThrow('some error') - }) + beforeEach(async () => { + el = await $('sel') + vi.mocked(el.getSize).mockResolvedValue(50 as unknown as Size & number) // vitest does not support overloads function well + }) - test('success on the first attempt', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + test('success', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveWidth(el, 50, { beforeAssertion, afterAssertion }) + + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveWidth', + expectedValue: 50, + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveWidth', + expectedValue: 50, + options: { beforeAssertion, afterAssertion }, + result + }) + }) - const result = await toHaveWidth.call({}, el, 50, {}) + test('error', async () => { + vi.mocked(el.getSize).mockRejectedValue(new Error('some error')) - expect(result.message()).toEqual('Expect $(`sel`) to have width\n\nExpected: 50\nReceived: serializes to the same string') - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + await expect(() => thisContext.toHaveWidth(el, 10)) + .rejects.toThrow('some error') + }) - test('no wait - failure', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + test('success on the first attempt', async () => { + const result = await thisContext.toHaveWidth(el, 50) - const result = await toHaveWidth.call({}, el, 10, { wait: 0 }) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - expect(result.message()).toEqual('Expect $(`sel`) to have width\n\nExpected: 10\nReceived: 50') - expect(result.pass).toBe(false) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + test('no wait - failure', async () => { + const result = await thisContext.toHaveWidth(el, 10, { wait: 0 }) - test('no wait - success', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have width - const result = await toHaveWidth.call({}, el, 50, { wait: 0 }) +Expected: 10 +Received: 50`) + expect(result.pass).toBe(false) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + test('no wait - success', async () => { + const result = await thisContext.toHaveWidth(el, 50, { wait: 0 }) - test('gte and lte', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - const result = await toHaveWidth.call({}, el, { gte: 49, lte: 51 }, { wait: 0 }) + test('gte and lte', async () => { + const result = await thisContext.toHaveWidth(el, { gte: 49, lte: 51 }) - expect(result.message()).toEqual('Expect $(`sel`) to have width\n\nExpected: ">= 49 && <= 51"\nReceived: 50') - expect(result.pass).toBe(true) - expect(el.getSize).toHaveBeenCalledTimes(1) - }) + expect(result.pass).toBe(true) + expect(el.getSize).toHaveBeenCalledTimes(1) + }) - test('not - failure - pass should be true', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) - const result = await toHaveWidth.call({ isNot: true }, el, 50, { wait: 0 }) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toHaveWidth(el, 50) - expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` - expect(result.message()).toEqual(`\ + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ Expect $(\`sel\`) not to have width Expected [not]: 50 Received : 50` - ) - }) + ) + }) - test('not - success - pass should be false', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveWidth(el, 100) - const result = await toHaveWidth.call({ isNot: true }, el, 40, { wait: 0 }) + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) - expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` - }) + test('message', async () => { + el.getSize = vi.fn().mockResolvedValue(0) - test("should return false if sizes don't match", async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + const result = await thisContext.toHaveWidth(el, 50) - const result = await toHaveWidth.bind({})(el, 10, { wait: 1 }) + expect(result.message()).toEqual(`\ +Expect $(\`sel\`) to have width - expect(result.pass).toBe(false) +Expected: 50 +Received: 0` + ) + }) }) - test('should return true if sizes match', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(50) + describe('given multiple elements', () => { + let elements: ChainablePromiseArray + beforeEach(async () => { + elements = await $$('sel') + }) - const result = await toHaveWidth.bind({})(el, 50, { wait: 1 }) + test('wait for success', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + + const result = await thisContext.toHaveWidth(elements, 50, { beforeAssertion, afterAssertion, wait: 500 } ) + + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getSize).toHaveBeenCalledTimes(1)) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toHaveWidth', + expectedValue: 50, + options: { beforeAssertion, afterAssertion, wait: 500 } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toHaveWidth', + expectedValue: 50, + options: { beforeAssertion, afterAssertion, wait: 500 }, + result + }) + }) - expect(result.pass).toBe(true) - }) + test('wait but failure', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockRejectedValue(new Error('some error'))) + + await expect(() => thisContext.toHaveWidth(elements, 10)) + .rejects.toThrow('some error') + }) + + test('success on the first attempt', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisContext.toHaveWidth(elements, 50) + + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getSize).toHaveBeenCalledTimes(1)) + }) + + test('no wait - failure', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisContext.toHaveWidth(elements, 10, { wait: 0 }) + + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have width + +- Expected - 2 ++ Received + 2 + + Array [ +- 10, +- 10, ++ 50, ++ 50, + ]` + ) + expect(result.pass).toBe(false) + elements.forEach(el => expect(el.getSize).toHaveBeenCalledTimes(1)) + }) - test('message', async () => { - const el = await $('sel') - el.getSize = vi.fn().mockResolvedValue(null) + test('no wait - success', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) - const result = await toHaveWidth.call({}, el, 50) + const result = await thisContext.toHaveWidth(elements, 50, { wait: 0 }) - expect(getExpectMessage(result.message())).toContain('to have width') + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getSize).toHaveBeenCalledTimes(1)) + }) + + test('gte and lte', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisContext.toHaveWidth(elements, { gte: 49, lte: 51 }) + + expect(result.pass).toBe(true) + elements.forEach(el => expect(el.getSize).toHaveBeenCalledTimes(1)) + }) + + test('not - failure - pass should be true', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisNotContext.toHaveWidth(elements, 50) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have width + +Expected [not]: [50, 50] +Received : [50, 50]` + ) + }) + + test('not - failure lte - pass should be true', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisNotContext.toHaveWidth(elements, { lte: 51 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have width + +Expected [not]: ["<= 51", "<= 51"] +Received : [50, 50]` + ) + }) + + test('not - failure lte only first element - pass should be true', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisNotContext.toHaveWidth(elements, [{ lte: 51 }, 51]) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have width + +Expected [not]: ["<= 51", 51] +Received : [50, 50]` + ) + }) + + test('not - failure gte - pass should be true', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(50)) + + const result = await thisNotContext.toHaveWidth(elements, { gte: 49 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later because of `.not` + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) not to have width + +Expected [not]: [">= 49", ">= 49"] +Received : [50, 50]` + ) + }) + + test('not - success - pass should be false', async () => { + const result = await thisNotContext.toHaveWidth(elements, 10) + + expect(result.pass).toBe(false) // success, boolean is inverted later because of `.not` + }) + + test('message', async () => { + elements.forEach(el => vi.mocked(el.getSize).mockResolvedValue(0)) + + const result = await thisContext.toHaveWidth(elements, 50) + + expect(result.message()).toEqual(`\ +Expect $$(\`sel\`) to have width + +- Expected - 2 ++ Received + 2 + + Array [ +- 50, +- 50, ++ 0, ++ 0, + ]`) + }) }) }) diff --git a/test/matchers/elements/toBeElementsArrayOfSize.test.ts b/test/matchers/elements/toBeElementsArrayOfSize.test.ts index e2ed9b5ef..6013a60a2 100644 --- a/test/matchers/elements/toBeElementsArrayOfSize.test.ts +++ b/test/matchers/elements/toBeElementsArrayOfSize.test.ts @@ -1,52 +1,51 @@ import { vi, test, describe, expect, beforeEach } from 'vitest' -import { $$ } from '@wdio/globals' +import { $$, browser } from '@wdio/globals' -import { getExpectMessage, getReceived, getExpected } from '../../__fixtures__/utils.js' import { toBeElementsArrayOfSize } from '../../../src/matchers/elements/toBeElementsArrayOfSize.js' -import type { AssertionResult } from 'expect-webdriverio' - -const createMockElementArray = (length: number): WebdriverIO.ElementArray => { - const array = Array.from({ length }, () => ({})) - const mockArray = { - selector: 'parent', - get length() { return array.length }, - set length(newLength: number) { array.length = newLength }, - parent: { - $: vi.fn(), - $$: vi.fn().mockReturnValue(array), - }, - foundWith: '$$', - props: [], - [Symbol.iterator]: array[Symbol.iterator].bind(array), - filter: vi.fn().mockReturnThis(), - map: vi.fn().mockReturnThis(), - find: vi.fn().mockReturnThis(), - forEach: vi.fn(), - some: vi.fn(), - every: vi.fn(), - slice: vi.fn().mockReturnThis(), - toArray: vi.fn().mockReturnThis(), - } - return Object.assign(array, mockArray) as unknown as WebdriverIO.ElementArray -} - -vi.mock('@wdio/globals', () => ({ - $$: vi.fn().mockImplementation(() => createMockElementArray(2)) -})) - -describe('toBeElementsArrayOfSize', () => { - describe('given an elements of type WebdriverIO.ElementArray', () => { - let els: WebdriverIO.ElementArray +import { chainableElementArrayFactory, elementArrayFactory, elementFactory } from '../../__mocks__/@wdio/globals.js' +import { waitUntil } from '../../../src/utils.js' +import { refetchElements } from '../../../src/util/refetchElements.js' + +vi.mock('@wdio/globals') + +describe(toBeElementsArrayOfSize, async () => { + let thisContext: { toBeElementsArrayOfSize: typeof toBeElementsArrayOfSize } + let thisNotContext: { toBeElementsArrayOfSize: typeof toBeElementsArrayOfSize, isNot: boolean } + + beforeEach(() => { + thisContext = { toBeElementsArrayOfSize } + thisNotContext = { toBeElementsArrayOfSize, isNot: true } + }) + + describe.each([ + { elements: await $$('elements'), title: 'awaited ChainablePromiseArray' }, + { elements: await $$('elements').getElements(), title: 'awaited getElements of ChainablePromiseArray (e.g. WebdriverIO.ElementArray)' }, + { elements: await $$('elements').filter((t) => t.isEnabled()), selectorName: '$(`elements`), $$(`elements`)[1]', title: 'awaited filtered ChainablePromiseArray (e.g. WebdriverIO.Element[])' }, + { elements: [elementFactory('element'), elementFactory('element')], selectorName: '$(`element`), $(`element`)', title: 'Array of element (e.g. WebdriverIO.Element[])' }, + { elements: $$('elements'), title: 'non-awaited of ChainablePromiseArray' }, + + // TODO to support, since the below return Promise, we do not support it type wise yet, but we could + { elements: $$('elements').getElements() as unknown as ChainablePromiseArray, title: 'non-awaited of ChainablePromiseArray' }, + { elements: $$('elements').filter((t) => t.isEnabled()) as unknown as ChainablePromiseArray, selectorName:'$(`elements`), $$(`elements`)[1]', title: 'awaited filtered ChainablePromiseArray (e.g. WebdriverIO.Element[])' }, + ])('given multiple elements when $title', ({ elements, selectorName = '$$(`elements`)' }) => { + let els: ChainablePromiseArray | WebdriverIO.Element[] | WebdriverIO.ElementArray beforeEach(() => { - els = $$('parent') as unknown as WebdriverIO.ElementArray + els = elements }) describe('success', () => { test('array of size 2', async () => { const beforeAssertion = vi.fn() const afterAssertion = vi.fn() - const result = await toBeElementsArrayOfSize.call({}, els, 2, { beforeAssertion, afterAssertion, wait: 0 }) + + const result = await thisContext.toBeElementsArrayOfSize(els, 2, { beforeAssertion, afterAssertion, wait: 0 }) + + expect(waitUntil).toHaveBeenCalledWith( + expect.any(Function), + undefined, + expect.objectContaining({ wait: 0 }) + ) expect(result.pass).toBe(true) expect(beforeAssertion).toBeCalledWith({ matcherName: 'toBeElementsArrayOfSize', @@ -60,207 +59,266 @@ describe('toBeElementsArrayOfSize', () => { result }) }) - test('array of size 5', async () => { - els = createMockElementArray(5) - const result = await toBeElementsArrayOfSize.call({}, els, 5, { wait : 0 }) - expect(result.pass).toBe(true) + + test.for([ + 0, 1, 3 + ])('not - success - pass should be false', async (expectedNotToBeSizeOf) => { + const result = await thisNotContext.toBeElementsArrayOfSize(els, expectedNotToBeSizeOf) + + expect(result.pass).toBe(false) // success, boolean is inverted later in .not cases }) }) describe('failure', () => { - let result: AssertionResult + test('fails with proper error message', async () => { + const result = await thisContext.toBeElementsArrayOfSize(els, 5) - beforeEach(async () => { - result = await toBeElementsArrayOfSize.call({}, els, 5, { wait: 0 }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be elements array of size + +Expected: 5 +Received: 2` + ) }) - test('fails', () => { - expect(result.pass).toBe(false) + test('not - failure - pass should be true', async () => { + const result = await thisNotContext.toBeElementsArrayOfSize(els, 2) + + expect(result.pass).toBe(true) // failure, boolean is inverted later in .not cases + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be elements array of size + +Expected [not]: 2 +Received : 2` + ) }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to be elements array of size') - }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('5') - }) - test('received message', () => { - expect(getReceived(result.message())).toContain('2') - }) + test('not - failure - lte - pass should be true', async () => { + const result = await thisNotContext.toBeElementsArrayOfSize(els, { lte: 3 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later in .not cases + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be elements array of size + +Expected [not]: "<= 3" +Received : 2` + ) }) + + test('not - failure - gte - pass should be true', async () => { + const result = await thisNotContext.toBeElementsArrayOfSize(els, { gte: 1 }) + + expect(result.pass).toBe(true) // failure, boolean is inverted later in .not cases + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be elements array of size + +Expected [not]: ">= 1" +Received : 2` + ) + }) + }) describe('error catching', () => { test('throws error with incorrect size param', async () => { - await expect(toBeElementsArrayOfSize.call({}, els, '5' as any)).rejects.toThrow('Invalid params passed to toBeElementsArrayOfSize.') + await expect(thisContext.toBeElementsArrayOfSize(els, '5' as any)).rejects.toThrow('Invalid NumberOptions. Received: "5"') }) test('works if size contains options', async () => { - const result = await toBeElementsArrayOfSize.call({}, els, { lte: 5 }, { wait: 0 }) + const result = await thisContext.toBeElementsArrayOfSize(els, { lte: 5 }) expect(result.pass).toBe(true) }) }) describe('number options', () => { test.each([ - ['lte', 10, true], - ['lte', 1, false], - ['gte', 1, true], - ['gte', 10, false], - ['gte and lte', { gte: 1, lte: 10, wait: 0 }, true], - ['not gte but is lte', { gte: 10, lte: 10, wait: 0 }, false], - ['not lte but is gte', { gte: 1, lte: 1, wait: 0 }, false], - ])('should handle %s correctly', async (_, option, expected) => { - const result = await toBeElementsArrayOfSize.call({}, els, typeof option === 'object' ? option : { [_ as string]: option }) - expect(result.pass).toBe(expected) + ['number - equal', 2, true], + ['number - equal - fail 1', 1, false], + ['number - equal - fail 2', 3, false], + ])('should handle %s correctly', async (_title, expectedNumberValue, expectedPass) => { + const result = await thisContext.toBeElementsArrayOfSize(els, expectedNumberValue, { wait: 0 }) + + expect(result.pass).toBe(expectedPass) + }) + + test.each([ + ['gte - equal', { gte: 2 } satisfies ExpectWebdriverIO.NumberOptions, true], + ['gte - fail', { gte: 1 } satisfies ExpectWebdriverIO.NumberOptions, true], + ['gte', { gte: 3 } satisfies ExpectWebdriverIO.NumberOptions, false], + ['lte - equal', { lte: 2 } satisfies ExpectWebdriverIO.NumberOptions, true], + ['lte - fail', { lte: 3 } satisfies ExpectWebdriverIO.NumberOptions, true], + ['lte', { lte: 1 } satisfies ExpectWebdriverIO.NumberOptions, false], + ['gte and lte', { gte: 1, lte: 10 } satisfies ExpectWebdriverIO.NumberOptions, true], + ['not gte but is lte', { gte: 10, lte: 10 } satisfies ExpectWebdriverIO.NumberOptions, false], + ['not lte but is gte', { gte: 1, lte: 1 } satisfies ExpectWebdriverIO.NumberOptions, false], + ])('should handle %s correctly', async (_title, expectedNumberValue: ExpectWebdriverIO.NumberOptions, expectedPass) => { + const result = await thisContext.toBeElementsArrayOfSize(els, expectedNumberValue) + + expect(result.pass).toBe(expectedPass) }) }) + }) - describe('array update', () => { - test('updates the received array when assertion passes', async () => { - const receivedArray = createMockElementArray(2); - (receivedArray.parent as any)._length = 5; - (receivedArray.parent as any).$$ = vi.fn().mockReturnValue(createMockElementArray(5)) + describe('Refresh ElementArray', async () => { + let elementArrayOf2: ChainablePromiseArray + let elementArrayOf5: ChainablePromiseArray - const result = await toBeElementsArrayOfSize.call({}, receivedArray, 5) + beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actuatlRefetchElements = await vi.importActual('../../../src/util/refetchElements.js') + vi.spyOn(actuatlRefetchElements, 'refetchElements') - expect(result.pass).toBe(true) - expect(receivedArray.length).toBe(5) - }) + elementArrayOf2 = await chainableElementArrayFactory('elements', 2) + elementArrayOf5 = await chainableElementArrayFactory('elements', 5) + }) - test('does not update the received array when assertion fails', async () => { - const receivedArray = createMockElementArray(2) + test('refresh once the elements array using parent $$ and update actual element with newly fetched elements', async () => { + vi.fn(browser.$$).mockResolvedValueOnce(elementArrayOf2).mockResolvedValueOnce(elementArrayOf5) + const elements = await $$('elements') - const result = await toBeElementsArrayOfSize.call({}, receivedArray, 10) + const result = await thisContext.toBeElementsArrayOfSize(elements, 5, { wait: 95, interval: 50 }) - expect(result.pass).toBe(false) - expect(receivedArray.length).toBe(2) - }) + expect(result.pass).toBe(true) + expect(elements).toBe(elementArrayOf2) // Original actual elements array but altered + expect(elements.length).toBe(5) // Altered actual elements array + expect(browser.$$).toHaveBeenCalledTimes(2) + expect(refetchElements).toHaveBeenNthCalledWith(1, elementArrayOf2, 95, true) + expect(refetchElements).toHaveBeenCalledTimes(1) + }) - test('does not modify non-array received values', async () => { - const nonArrayEls = { - selector: 'parent', - length: 2, - parent: { - $: vi.fn(), - $$: vi.fn().mockReturnValue(createMockElementArray(5)), - }, - foundWith: '$$', - props: [], - } as unknown as WebdriverIO.ElementArray + test('refresh multiple time actual elements but does not update it since it failed', async () => { + vi.mocked(browser.$$).mockReturnValueOnce(elementArrayOf2).mockReturnValue(elementArrayOf5) + const elements = await $$('elements') - const result = await toBeElementsArrayOfSize.call({}, nonArrayEls, 5) + const result = await thisContext.toBeElementsArrayOfSize(elements, 10, { wait: 100, interval: 20 }) - expect(result.pass).toBe(true) - expect(nonArrayEls.length).toBe(2) - }) + expect(result.pass).toBe(false) + expect(elements.length).toBe(2) + expect(elements).toBe(elementArrayOf2) + expect(browser.$$).toHaveBeenCalledTimes(6) + expect(refetchElements).toHaveBeenNthCalledWith(1, elementArrayOf2, 100, true) + expect(refetchElements).toHaveBeenNthCalledWith(2, elementArrayOf5, 100, true) + }) - test('does not alter the array when checking', async () => { - const receivedArray = createMockElementArray(2) - const result = await toBeElementsArrayOfSize.call({}, receivedArray, 2) + // TODO: By awaiting the promise we could update the actual elements array, so should we support that? + test('refresh once but does not update actual elements since they are not of type ElementArray or Element[]', async () => { + vi.fn(browser.$$).mockResolvedValueOnce(elementArrayOf2).mockResolvedValueOnce(elementArrayOf5) + const nonAwaitedElements = $$('elements') - expect(result.pass).toBe(true) - expect(receivedArray.length).toBe(2) - }) + const result = await thisContext.toBeElementsArrayOfSize(nonAwaitedElements, 5, { wait: 500 }) + + expect(result.pass).toBe(true) + expect(nonAwaitedElements).toBeInstanceOf(Promise) + expect((await nonAwaitedElements).length).toBe(2) + expect(await nonAwaitedElements).toBe(elementArrayOf2) + expect(browser.$$).toHaveBeenCalledTimes(2) + expect(refetchElements).toHaveBeenNthCalledWith(1, elementArrayOf2, 500, true) + expect(refetchElements).toHaveBeenCalledTimes(1) }) - }) - describe('given an elements of type WebdriverIO.Element[]', () => { - describe('when elements is empty array', () => { - const elements: WebdriverIO.Element[] = [] - describe('success', () => { - test('array of size 0', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const result = await toBeElementsArrayOfSize.call({}, elements, 0, { beforeAssertion, afterAssertion, wait: 0 }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toBeElementsArrayOfSize', - expectedValue: 0, - options: { beforeAssertion, afterAssertion, wait: 0 } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toBeElementsArrayOfSize', - expectedValue: 0, - options: { beforeAssertion, afterAssertion, wait: 0 }, - result - }) - }) - }) + test.for([ + elementArrayFactory('elements', 2), + await chainableElementArrayFactory('elements', 2), + [elementFactory('elements', 0), elementFactory('elements', 1)] + ])('Does not refetch and does not alter the actual elements array when it size matches on first try', async () => { + const receivedArray = elementArrayFactory('elements', 2) + const result = await thisContext.toBeElementsArrayOfSize(receivedArray, 2) + + expect(result.pass).toBe(true) + expect(receivedArray.length).toBe(2) + expect(receivedArray).toBe(receivedArray) + expect(browser.$$).not.toHaveBeenCalled() + expect(refetchElements).not.toHaveBeenCalled() + }) - describe('failure', () => { - let result: AssertionResult + test('refresh once the element array with the NumberOptions wait value', async () => { + vi.mocked(browser.$$).mockReturnValueOnce(elementArrayOf2).mockReturnValue(elementArrayOf5) + const elements = await $$('elements') - beforeEach(async () => { - result = await toBeElementsArrayOfSize.call({}, elements, 5, { wait: 0 }) - }) + const result = await thisContext.toBeElementsArrayOfSize(elements, { gte: 5, wait: 450 }) - test('fails', () => { - expect(result.pass).toBe(false) - }) + expect(result.pass).toBe(true) + expect(elements.length).toBe(5) + expect(refetchElements).toHaveBeenNthCalledWith(1, elementArrayOf2, 450, true) + expect(waitUntil).toHaveBeenCalledWith( + expect.any(Function), + undefined, + expect.objectContaining({ wait: 450 }) + ) + }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to be elements array of size') - }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('5') - }) - test('received message', () => { - expect(getReceived(result.message())).toContain('0') - }) - }) - }) + test('refresh once the element array with the DEFAULT_OPTIONS wait value', async () => { + vi.mocked(browser.$$).mockReturnValueOnce(elementArrayOf2).mockReturnValue(elementArrayOf5) + const elements = await $$('elements') + + const result = await thisContext.toBeElementsArrayOfSize(elements, { gte: 5 }, { beforeAssertion: undefined, afterAssertion: undefined }) + + expect(result.pass).toBe(false) + expect(refetchElements).toHaveBeenNthCalledWith(1, elementArrayOf2, 1, true) + expect(waitUntil).toHaveBeenCalledWith( + expect.any(Function), + undefined, + expect.objectContaining({ wait: 1 }) + ) }) + }) - describe('when elements is not empty array', () => { - const elements: WebdriverIO.Element[] = [{ - elementId: 'element-1' - } satisfies Partial as WebdriverIO.Element,] - describe('success', () => { - test('array of size 1', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const result = await toBeElementsArrayOfSize.call({}, elements, 1, { beforeAssertion, afterAssertion, wait: 0 }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toBeElementsArrayOfSize', - expectedValue: 1, - options: { beforeAssertion, afterAssertion, wait: 0 } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toBeElementsArrayOfSize', - expectedValue: 1, - options: { beforeAssertion, afterAssertion, wait: 0 }, - result - }) - }) - }) + describe('Works with differenet ElementArray or Element[] sizes', () => { + test.for([ + 0, 1, 2, 3, 4, 5, 10 + ])('ChainablePromiseArray of size %i', async (size) => { + const els = chainableElementArrayFactory('elements', size) - describe('failure', () => { - let result: AssertionResult + const result = await thisContext.toBeElementsArrayOfSize(els, size) - beforeEach(async () => { - result = await toBeElementsArrayOfSize.call({}, elements, 5, { wait: 0 }) - }) + expect(result.pass).toBe(true) + }) - test('fails', () => { - expect(result.pass).toBe(false) - }) + test.for([ + 0, 1, 2, 3, 4, 5, 10 + ])('ElementArray of size %i', async (size) => { + const els = elementArrayFactory('elements', size) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to be elements array of size') - }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('5') - }) - test('received message', () => { - expect(getReceived(result.message())).toContain('1') - }) - }) - }) + const result = await thisContext.toBeElementsArrayOfSize(els, size) + + expect(result.pass).toBe(true) + }) + + test.for([ + 0, 1, 2, 3, 4, 5, 10 + ])('Element[] of size %i', async (size) => { + const els = Array(size).fill(null).map((_, index) => elementFactory('element', index)) + + const result = await thisContext.toBeElementsArrayOfSize(els, size) + + expect(result.pass).toBe(true) + }) + }) + + describe('Fails for unsupported types', () => { + + test.for([ + { els: undefined, selectorName: 'undefined' }, + { els: null, selectorName: 'null' }, + { els: 0, selectorName: '0' }, + { els: 1, selectorName: '1' }, + { els: true, selectorName: 'true' }, + { els: false, selectorName: 'false' }, + { els: '', selectorName: '' }, + { els: 'test', selectorName: 'test' }, + { els: {}, selectorName: '{}' }, + { els: [1, 'test'], selectorName: '[1,"test"]' }, + { els: Promise.resolve(true), selectorName: 'true' } + ])('fails for %s', async ({ els, selectorName }) => { + const result = await thisContext.toBeElementsArrayOfSize(els as any, 0) + + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be elements array of size + +Expected: 0 +Received: undefined`) }) }) }) diff --git a/test/matchers/mock/toBeRequested.test.ts b/test/matchers/mock/toBeRequested.test.ts index 05ae9d7cb..7600f18c7 100644 --- a/test/matchers/mock/toBeRequested.test.ts +++ b/test/matchers/mock/toBeRequested.test.ts @@ -1,11 +1,19 @@ -import { vi, test, describe, expect } from 'vitest' +import { vi, test, describe, expect, beforeEach } from 'vitest' // @ts-ignore TODO fix me import type { Matches, Mock } from 'webdriverio' import { toBeRequested } from '../../../src/matchers/mock/toBeRequested.js' vi.mock('@wdio/globals') - +vi.mock('../../../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../../../src/constants.js')).DEFAULT_OPTIONS, + // speed up tests by lowering default wait timeout + wait : 15, + interval: 5 + } +})) class TestMock implements Mock { _calls: any[] @@ -36,7 +44,13 @@ const mockMatch: Matches = { referrerPolicy: 'origin' } -describe('toBeRequested', () => { +describe(toBeRequested, () => { + let thisNotContext: { isNot: true, toBeRequested: typeof toBeRequested } + + beforeEach(() => { + thisNotContext = { isNot: true, toBeRequested } + }) + test('wait for success', async () => { const mock: Mock = new TestMock() const result = await toBeRequested(mock) @@ -45,21 +59,21 @@ describe('toBeRequested', () => { setTimeout(() => { mock.calls.push(mockMatch) mock.calls.push(mockMatch) - }, 10) + }, 5) const beforeAssertion = vi.fn() const afterAssertion = vi.fn() - const result2 = await toBeRequested(mock, { beforeAssertion, afterAssertion }) + const result2 = await toBeRequested(mock, { beforeAssertion, afterAssertion, wait: 500 }) expect(result2.pass).toBe(true) expect(beforeAssertion).toBeCalledWith({ matcherName: 'toBeRequestedTimes', expectedValue: { gte: 1 }, - options: { beforeAssertion, afterAssertion } + options: { beforeAssertion, afterAssertion, wait: 500 } }) expect(afterAssertion).toBeCalledWith({ matcherName: 'toBeRequestedTimes', expectedValue: { gte: 1 }, - options: { beforeAssertion, afterAssertion }, + options: { beforeAssertion, afterAssertion, wait: 500 }, result: result2 }) }) @@ -68,13 +82,13 @@ describe('toBeRequested', () => { const mock: Mock = new TestMock() // expect(mock).not.toBeRequested() should pass=false - const result = await toBeRequested.call({ isNot: true }, mock) + const result = await thisNotContext.toBeRequested(mock) expect(result.pass).toBe(false) // success, boolean is inverted later becuase of `.not` mock.calls.push(mockMatch) // expect(mock).not.toBeRequested() should fail - const result4 = await toBeRequested.call({ isNot: true }, mock) + const result4 = await thisNotContext.toBeRequested(mock) expect(result4.pass).toBe(true) // failure, boolean is inverted later because of `.not` }) @@ -91,8 +105,7 @@ Received: 0` ) mock.calls.push(mockMatch) - const result2 = await toBeRequested.call({ isNot: true }, mock) - + const result2 = await thisNotContext.toBeRequested(mock) expect(result2.pass).toBe(true) // failure, boolean is inverted later because of `.not` expect(result2.message()).toEqual(`\ Expect mock not to be called diff --git a/test/matchers/mock/toBeRequestedTimes.test.ts b/test/matchers/mock/toBeRequestedTimes.test.ts index 65df6b0bc..31e8b42e7 100644 --- a/test/matchers/mock/toBeRequestedTimes.test.ts +++ b/test/matchers/mock/toBeRequestedTimes.test.ts @@ -1,10 +1,19 @@ -import { vi, test, describe, expect } from 'vitest' +import { vi, test, describe, expect, beforeEach } from 'vitest' // @ts-ignore TODO fix me import type { Matches, Mock } from 'webdriverio' import { toBeRequestedTimes } from '../../../src/matchers/mock/toBeRequestedTimes.js' vi.mock('@wdio/globals') +vi.mock('../../../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../../../src/constants.js')).DEFAULT_OPTIONS, + // speed up tests by lowering default wait timeout + wait : 30, + interval: 10 + } +})) class TestMock implements Mock { _calls: Matches[] @@ -37,6 +46,14 @@ const mockMatch: Matches = { } describe('toBeRequestedTimes', () => { + let thisNotContext: { isNot: true; toBeRequestedTimes: typeof toBeRequestedTimes } + let thisContext: { toBeRequestedTimes: typeof toBeRequestedTimes } + + beforeEach(() => { + thisNotContext = { isNot: true, toBeRequestedTimes } + thisContext = { toBeRequestedTimes } + }) + test('wait for success', async () => { const mock: Mock = new TestMock() @@ -46,17 +63,19 @@ describe('toBeRequestedTimes', () => { const beforeAssertion = vi.fn() const afterAssertion = vi.fn() - const result = await toBeRequestedTimes.call({}, mock, 1, { beforeAssertion, afterAssertion }) + + const result = await thisContext.toBeRequestedTimes(mock, 1, { beforeAssertion, afterAssertion, wait: 500 }) + expect(result.pass).toBe(true) expect(beforeAssertion).toBeCalledWith({ matcherName: 'toBeRequestedTimes', expectedValue: 1, - options: { beforeAssertion, afterAssertion } + options: { beforeAssertion, afterAssertion, wait: 500 } }) expect(afterAssertion).toBeCalledWith({ matcherName: 'toBeRequestedTimes', expectedValue: 1, - options: { beforeAssertion, afterAssertion }, + options: { beforeAssertion, afterAssertion, wait: 500 }, result }) }) @@ -68,15 +87,15 @@ describe('toBeRequestedTimes', () => { mock.calls.push(mockMatch) }, 10) - const result = await toBeRequestedTimes.call({}, mock, { gte: 1 }) + const result = await thisContext.toBeRequestedTimes(mock, { gte: 1, wait: 500 }) expect(result.pass).toBe(true) - const result2 = await toBeRequestedTimes.call({}, mock, { eq: 1 }) + const result2 = await thisContext.toBeRequestedTimes(mock, { eq: 1, wait: 500 }) expect(result2.pass).toBe(true) }) test('wait but failure', async () => { const mock: Mock = new TestMock() - const result = await toBeRequestedTimes.call({}, mock, 1) + const result = await thisContext.toBeRequestedTimes(mock, 1) expect(result.pass).toBe(false) setTimeout(() => { @@ -84,15 +103,19 @@ describe('toBeRequestedTimes', () => { mock.calls.push(mockMatch) }, 10) - const result2 = await toBeRequestedTimes.call({}, mock, 1) + const result2 = await thisContext.toBeRequestedTimes(mock, 1) expect(result2.pass).toBe(false) - const result3 = await toBeRequestedTimes.call({}, mock, 2) + + const result3 = await thisContext.toBeRequestedTimes(mock, 2) expect(result3.pass).toBe(true) - const result4 = await toBeRequestedTimes.call({}, mock, { gte: 2 }) + + const result4 = await thisContext.toBeRequestedTimes(mock, { gte: 2, wait: 1 }) expect(result4.pass).toBe(true) - const result5 = await toBeRequestedTimes.call({}, mock, { lte: 2 }) + + const result5 = await thisContext.toBeRequestedTimes(mock, { lte: 2, wait: 1 }) expect(result5.pass).toBe(true) - const result6 = await toBeRequestedTimes.call({}, mock, { lte: 3 }) + + const result6 = await thisContext.toBeRequestedTimes(mock, { lte: 3, wait: 1 }) expect(result6.pass).toBe(true) }) @@ -100,7 +123,7 @@ describe('toBeRequestedTimes', () => { const mock: Mock = new TestMock() // expect(mock).not.toBeRequestedTimes(0) should fail - const result = await toBeRequestedTimes.call({ isNot: true }, mock, 0) + const result = await thisNotContext.toBeRequestedTimes(mock, 0) expect(result.pass).toBe(true) // failure, boolean inverted later because of .not expect(result.message()).toEqual(`\ Expect mock not to be called 0 times @@ -110,17 +133,17 @@ Received : 0` ) // expect(mock).not.toBeRequestedTimes(1) should pass - const result2 = await toBeRequestedTimes.call({ isNot: true }, mock, 1) + const result2 = await thisNotContext.toBeRequestedTimes(mock, 1) expect(result2.pass).toBe(false) // success, boolean inverted later because of .not mock.calls.push(mockMatch) // expect(mock).not.toBeRequestedTimes(0) should pass - const result3 = await toBeRequestedTimes.call({ isNot: true }, mock, 0) + const result3 = await thisNotContext.toBeRequestedTimes(mock, 0) expect(result3.pass).toBe(false) // success, boolean inverted later because of .not // expect(mock).not.toBeRequestedTimes(1) should fail - const result4 = await toBeRequestedTimes.call({ isNot: true }, mock, 1) + const result4 = await thisNotContext.toBeRequestedTimes(mock, 1) expect(result4.pass).toBe(true) // failure, boolean inverted later because of .not expect(result4.message()).toEqual(`\ Expect mock not to be called 1 time @@ -133,16 +156,16 @@ Received : 1` test('message', async () => { const mock: Mock = new TestMock() - const result = await toBeRequestedTimes.call({}, mock, 0, { wait: 1 }) + const result = await thisContext.toBeRequestedTimes(mock, 0) expect(result.message()).toContain('Expect mock to be called 0 times') - const result2 = await toBeRequestedTimes.call({}, mock, 1, { wait: 1 }) + const result2 = await thisContext.toBeRequestedTimes(mock, 1) expect(result2.message()).toContain('Expect mock to be called 1 time') - const result3 = await toBeRequestedTimes.call({}, mock, 2, { wait: 1 }) + const result3 = await thisContext.toBeRequestedTimes(mock, 2) expect(result3.message()).toContain('Expect mock to be called 2 times') - const result4 = await toBeRequestedTimes.call({}, mock, { gte: 3 }, { wait: 1 }) + const result4 = await thisContext.toBeRequestedTimes(mock, { gte: 3 }) expect(result4.pass).toBe(false) expect(result4.message()).toEqual(`\ Expect mock to be called times diff --git a/test/matchers/mock/toBeRequestedWith.test.ts b/test/matchers/mock/toBeRequestedWith.test.ts index d4c8adab2..06b39bb30 100644 --- a/test/matchers/mock/toBeRequestedWith.test.ts +++ b/test/matchers/mock/toBeRequestedWith.test.ts @@ -2,9 +2,16 @@ import { vi, test, describe, expect, beforeEach, afterEach } from 'vitest' import { toBeRequestedWith } from '../../../src/matchers/mock/toBeRequestedWith.js' import type { local } from 'webdriver' -import { removeColors, getExpectMessage, getExpected, getReceived } from '../../__fixtures__/utils.js' vi.mock('@wdio/globals') +vi.mock('../../../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../../../src/constants.js')).DEFAULT_OPTIONS, + // speed up tests by lowering default wait timeout + wait : 0 + } +})) interface Scenario { name: string @@ -105,7 +112,12 @@ const mockPost: local.NetworkAuthRequiredParameters = { // referrerPolicy: 'origin', } as any -describe('toBeRequestedWith', () => { +describe(toBeRequestedWith, () => { + let thisNotContext: { isNot: true, toBeRequestedWith: typeof toBeRequestedWith } + + beforeEach(() => { + thisNotContext = { isNot: true, toBeRequestedWith } + }) test('wait for success, exact match', async () => { const mock: any = new TestMock() @@ -128,17 +140,19 @@ describe('toBeRequestedWith', () => { const beforeAssertion = vi.fn() const afterAssertion = vi.fn() - const result = await toBeRequestedWith.call({}, mock, params, { beforeAssertion, afterAssertion }) + + const result = await toBeRequestedWith(mock, params, { beforeAssertion, afterAssertion, wait: 500 }) + expect(result.pass).toBe(true) expect(beforeAssertion).toBeCalledWith({ matcherName: 'toBeRequestedWith', expectedValue: params, - options: { beforeAssertion, afterAssertion }, + options: { beforeAssertion, afterAssertion, wait: 500 }, }) expect(afterAssertion).toBeCalledWith({ matcherName: 'toBeRequestedWith', expectedValue: params, - options: { beforeAssertion, afterAssertion }, + options: { beforeAssertion, afterAssertion, wait: 500 }, result }) }) @@ -159,7 +173,7 @@ describe('toBeRequestedWith', () => { // response: 'post.body', } - const result = await toBeRequestedWith.call({}, mock, params) + const result = await toBeRequestedWith(mock, params, { wait: 20 }) expect(result.pass).toBe(false) }) @@ -170,7 +184,7 @@ describe('toBeRequestedWith', () => { mock.calls.push({ ...mockGet }, { ...mockPost }) }, 10) - const result = await toBeRequestedWith.call({ isNot: true }, mock, {}) + const result = await thisNotContext.toBeRequestedWith(mock, {}, { wait: 20 }) expect(result.pass).toBe(true) // failure, boolean inverted later because of .not expect(result.message()).toEqual(`\ Expect mock not to be called with @@ -187,7 +201,7 @@ Received : {}` mock.calls.push({ ...mockGet }, { ...mockPost }) }, 10) - const result = await toBeRequestedWith.call({ isNot: true }, mock, { method: 'DELETE' }) + const result = await thisNotContext.toBeRequestedWith(mock, { method: 'DELETE' }, { wait: 20 }) expect(result.pass).toBe(false) // success, boolean inverted later because of .not }) @@ -419,7 +433,7 @@ Received : {}` const mock: any = new TestMock() mock.calls.push(...scenario.mocks) - const result = await toBeRequestedWith.call({}, mock, scenario.params as any) + const result = await toBeRequestedWith(mock, scenario.params as any) expect(result.pass).toBe(scenario.pass) }) }) @@ -434,7 +448,7 @@ Received : {}` const mock: any = new TestMock() mock.calls.push({ ...mockGet }) - const result = await toBeRequestedWith.call({}, mock, { method: 1234 } as any) + const result = await toBeRequestedWith(mock, { method: 1234 } as any) expect(result.pass).toBe(false) expect(global.console.error).toBeCalledWith( 'expect.toBeRequestedWith: unsupported value passed to method 1234' @@ -449,7 +463,7 @@ Received : {}` test('message', async () => { const mock: any = new TestMock() - const requested = await toBeRequestedWith.call({}, mock, { + const requested = await toBeRequestedWith(mock, { url: () => false, method: ['DELETE', 'PUT'], requestHeaders: reduceHeaders(mockPost.request.headers), @@ -457,27 +471,21 @@ Received : {}` postData: expect.anything(), response: [...Array(50).keys()].map((_, id) => ({ id, name: `name_${id}` })), }) - const wasNotCalled = removeColors(requested.message()) - expect(getExpectMessage(wasNotCalled)).toBe('Expect mock to be called with') - expect(getExpected(wasNotCalled)).toBe( - 'Expected: {' + - '"method": ["DELETE", "PUT"], ' + - '"postData": "Anything ", ' + - '"requestHeaders": {"Accept": "*", "Authorization": "Bearer ..2222222", "foo": "bar"}, ' + - '"response": [{"id": 0, "name": "name_0"}, "... 49 more items"], ' + - '"responseHeaders": {}, ' + - '"url": "() => false"}' + expect(requested.pass).toBe(false) + expect(requested.message()).toEqual(`\ +Expect mock to be called with + +Expected: {"method": ["DELETE", "PUT"], "postData": "Anything ", "requestHeaders": {"Accept": "*", "Authorization": "Bearer ..2222222", "foo": "bar"}, "response": [{"id": 0, "name": "name_0"}, "... 49 more items"], "responseHeaders": {}, "url": "() => false"} +Received: "was not called"` ) - expect(getReceived(wasNotCalled)).toBe('Received: "was not called"') mock.calls.push(mockPost) - - const notRequested = await toBeRequestedWith.call({ isNot: true }, mock, { + const notRequested = await thisNotContext.toBeRequestedWith(mock, { url: () => true, method: mockPost.request.method, }) - const wasCalled = removeColors(notRequested.message()) - expect(wasCalled).toBe( + + expect(notRequested.message()).toBe( `Expect mock not to be called with - Expected [not] - 1 diff --git a/test/softAssertions.test.ts b/test/softAssertions.test.ts index 647cde302..f4d639fb2 100644 --- a/test/softAssertions.test.ts +++ b/test/softAssertions.test.ts @@ -5,14 +5,12 @@ import { expect as expectWdio, SoftAssertionService, SoftAssertService } from '. vi.mock('@wdio/globals') describe('Soft Assertions', () => { - // Setup a mock element for testing let el: ChainablePromiseElement beforeEach(async () => { el = $('sel') - // We need to mock getText() which is what the toHaveText matcher actually calls - el.getText = vi.fn().mockImplementation(() => 'Actual Text') - // Clear any soft assertion failures before each test + vi.mocked(el.getText).mockResolvedValue('Actual Text') + expectWdio.clearSoftFailures() }) @@ -21,7 +19,7 @@ describe('Soft Assertions', () => { const softService = SoftAssertService.getInstance() softService.setCurrentTest('test-1', 'test name', 'test file') - await expectWdio.soft(el).toHaveText('Expected Text', { wait: 0 }) + await expectWdio.soft(el).toHaveText('Expected Text') // Verify the failure was recorded const failures = expectWdio.getSoftFailures() @@ -50,9 +48,9 @@ describe('Soft Assertions', () => { softService.setCurrentTest('test-3', 'test name', 'test file') // These should not throw even though they fail - await expectWdio.soft(el).toHaveText('First Expected', { wait: 0 }) - await expectWdio.soft(el).toHaveText('Second Expected', { wait: 0 }) - await expectWdio.soft(el).toHaveText('Third Expected', { wait: 0 }) + await expectWdio.soft(el).toHaveText('First Expected') + await expectWdio.soft(el).toHaveText('Second Expected') + await expectWdio.soft(el).toHaveText('Third Expected') // Verify all failures were recorded const failures = expectWdio.getSoftFailures() @@ -157,11 +155,13 @@ describe('Soft Assertions', () => { describe('Different Matcher Types', () => { beforeEach(async () => { el = $('sel') + // Mock different methods for different matchers - el.getText = vi.fn().mockImplementation(() => 'Actual Text') - el.isDisplayed = vi.fn().mockImplementation(() => false) - el.getAttribute = vi.fn().mockImplementation(() => 'actual-class') - el.isClickable = vi.fn().mockImplementation(() => false) + vi.mocked(el.getText).mockResolvedValue('Actual Text') + vi.mocked(el.isDisplayed).mockResolvedValue(false) + vi.mocked(el.getAttribute).mockResolvedValue('actual-class') + vi.mocked(el.isClickable).mockResolvedValue(false) + expectWdio.clearSoftFailures() }) @@ -183,7 +183,7 @@ describe('Soft Assertions', () => { const softService = SoftAssertService.getInstance() softService.setCurrentTest('attribute-test', 'attribute test', 'test file') - await expectWdio.soft(el).toHaveAttribute('class', 'expected-class', { wait: 0 }) + await expectWdio.soft(el).toHaveAttribute('class', 'expected-class') const failures = expectWdio.getSoftFailures() expect(failures.length).toBe(1) @@ -208,12 +208,12 @@ describe('Soft Assertions', () => { // Test 1 softService.setCurrentTest('isolation-test-1', 'test 1', 'file1') - await expectWdio.soft(el).toHaveText('Expected Text 1', { wait: 0 }) + await expectWdio.soft(el).toHaveText('Expected Text 1') expect(expectWdio.getSoftFailures().length).toBe(1) // Test 2 - should have separate failures softService.setCurrentTest('isolation-test-2', 'test 2', 'file2') - await expectWdio.soft(el).toHaveText('Expected Text 2', { wait: 0 }) + await expectWdio.soft(el).toHaveText('Expected Text 2') // Test 2 should only see its own failure expect(expectWdio.getSoftFailures('isolation-test-2').length).toBe(1) @@ -245,13 +245,13 @@ describe('Soft Assertions', () => { const softService = SoftAssertService.getInstance() softService.setCurrentTest('concurrent-test', 'concurrent', 'test file') - el.getText = vi.fn().mockImplementation(() => 'Actual Text') - el.isDisplayed = vi.fn().mockImplementation(() => false) - el.isClickable = vi.fn().mockImplementation(() => false) + vi.mocked(el.getText).mockResolvedValue('Actual Text') + vi.mocked(el.isDisplayed).mockResolvedValue(false) + vi.mocked(el.isClickable).mockResolvedValue(false) // Fire multiple assertions rapidly const promises = [ - expectWdio.soft(el).toHaveText('Expected 1', { wait: 0 }), + expectWdio.soft(el).toHaveText('Expected 1'), expectWdio.soft(el).toBeDisplayed({ wait: 0 }), expectWdio.soft(el).toBeClickable({ wait: 0 }) ] @@ -276,10 +276,7 @@ describe('Soft Assertions', () => { softService.setCurrentTest('error-test', 'error test', 'test file') // Mock a matcher that throws a unique error - const originalMethod = el.getText - el.getText = vi.fn().mockImplementation(() => { - throw new TypeError('Weird browser error') - }) + vi.mocked(el.getText).mockRejectedValue(new TypeError('Weird browser error')) await expectWdio.soft(el).toHaveText('Expected Text') @@ -287,9 +284,6 @@ describe('Soft Assertions', () => { expect(failures.length).toBe(1) expect(failures[0].error).toBeInstanceOf(Error) expect(failures[0].error.message).toContain('Weird browser error') - - // Restore - el.getText = originalMethod }) it('should handle very long error messages', async () => { @@ -297,7 +291,7 @@ describe('Soft Assertions', () => { softService.setCurrentTest('long-error-test', 'long error', 'test file') const veryLongText = 'A'.repeat(10000) - await expectWdio.soft(el).toHaveText(veryLongText, { wait: 0 }) + await expectWdio.soft(el).toHaveText(veryLongText) const failures = expectWdio.getSoftFailures() expect(failures.length).toBe(1) @@ -310,8 +304,8 @@ describe('Soft Assertions', () => { softService.setCurrentTest('null-test', 'null test', 'test file') // Test with null/undefined values - await expectWdio.soft(el).toHaveText(null as any, { wait: 0 }) - await expectWdio.soft(el).toHaveAttribute('class') + await expectWdio.soft(el).toHaveText(null as any) + await expectWdio.soft(el).toHaveAttribute('class', undefined) const failures = expectWdio.getSoftFailures() expect(failures.length).toBe(2) @@ -321,7 +315,7 @@ describe('Soft Assertions', () => { const softService = SoftAssertService.getInstance() softService.setCurrentTest('location-test', 'location test', 'test file') - await expectWdio.soft(el).toHaveText('Expected Text', { wait: 0 }) + await expectWdio.soft(el).toHaveText('Expected Text') const failures = expectWdio.getSoftFailures() expect(failures.length).toBe(1) @@ -340,7 +334,7 @@ describe('Soft Assertions', () => { // Generate many failures const promises = [] for (let i = 0; i < 150; i++) { - promises.push(expectWdio.soft(el).toHaveText(`Expected ${i}`), { wait: 0 }) + promises.push(expectWdio.soft(el).toHaveText(`Expected ${i}`)) } await Promise.all(promises) diff --git a/test/util/elementsUtil.test.ts b/test/util/elementsUtil.test.ts index 6c1b99b81..4dd7af13d 100644 --- a/test/util/elementsUtil.test.ts +++ b/test/util/elementsUtil.test.ts @@ -1,22 +1,482 @@ -import { vi, test, describe, expect } from 'vitest' -import { $, $$ } from '@wdio/globals' +import { vi, test, describe, expect, beforeEach } from 'vitest' +import { $, $$, } from '@wdio/globals' -import { wrapExpectedWithArray } from '../../src/util/elementsUtil.js' +import { awaitElementOrArray, awaitElementArray, wrapExpectedWithArray, map, isStrictlyElementArray, isElement, isElementArrayLike, isElementOrArrayLike } from '../../src/util/elementsUtil.js' +import { chainableElementArrayFactory, elementArrayFactory, elementFactory, notFoundElementFactory } from '../__mocks__/@wdio/globals.js' vi.mock('@wdio/globals') describe('elementsUtil', () => { - describe('wrapExpectedWithArray', () => { - test('is not array ', async () => { - const el = (await $('sel')) as unknown as WebdriverIO.Element - const actual = wrapExpectedWithArray(el, 'Test Actual', 'Test Expected') - expect(actual).toEqual('Test Expected') + describe(wrapExpectedWithArray, () => { + + describe('given single expect value', () => { + const expected = 'Test Expected' + test('when having single element and single actual value then expected value is not wrapped into an array', async () => { + const actual = 'Test Actual' + const element = await $('sel').getElement() + + const wrappedExpectedValue = wrapExpectedWithArray(element, actual, expected) + + expect(wrappedExpectedValue).toEqual('Test Expected') + }) + + test('given array of elements and multiples actual values then expected value is wrapped into an array', async () => { + const elements = await $$('sel').getElements() + const actual = ['Test Actual', 'Test Actual'] + + const wrappedExpectedValue = wrapExpectedWithArray(elements, actual, expected) + + expect(wrappedExpectedValue).toEqual(['Test Expected', 'Test Expected']) + }) + }) + + describe('given multiple expect values', () => { + const expected = ['Test Expected', 'Test Expected'] + test('when having single element and single actual value then expected values is not wrapped in another array', async () => { + const actual = 'Test Actual' + const element = await $('sel').getElement() + + const wrappedExpectedValue = wrapExpectedWithArray(element, actual, expected) + + expect(wrappedExpectedValue).toEqual(['Test Expected', 'Test Expected']) + }) + + test('given array of elements and multiples actual values then expected values is not wrapped into another array', async () => { + const elements = await $$('sel').getElements() + const actual = ['Test Actual', 'Test Actual'] + + const wrappedExpectedValue = wrapExpectedWithArray(elements, actual, expected) + + expect(wrappedExpectedValue).toEqual(['Test Expected', 'Test Expected']) + }) + }) + }) + + describe(awaitElementOrArray, () => { + + describe('given single element', () => { + + let element: WebdriverIO.Element + let chainableElement: ChainablePromiseElement + + beforeEach(() => { + element = elementFactory('element1') + chainableElement = $('element1') + }) + + test('should return undefined when received is undefined', async () => { + const awaitedElements = await awaitElementOrArray(undefined) + + expect(awaitedElements).toEqual({ + other: undefined + }) + }) + + test('should return undefined when received is Promise of undefined (typing not supported)', async () => { + const awaitedElements = await awaitElementOrArray(Promise.resolve(undefined) as any) + + expect(awaitedElements).toEqual({ + other: undefined + }) + }) + + test('should return single element when received is a non-awaited ChainableElement', async () => { + const awaitedElements = await awaitElementOrArray(chainableElement) + + expect(awaitedElements).toEqual({ + element: expect.objectContaining({ selector: element.selector }) + }) + expect(awaitedElements.elements).toBeUndefined() + }) + + test('should return single element when received is an awaited ChainableElement', async () => { + const awaitedElements = await awaitElementOrArray(await chainableElement) + + expect(awaitedElements).toEqual({ + element: expect.objectContaining({ selector: element.selector }) + }) + expect(awaitedElements.elements).toBeUndefined() + }) + + test('should return single element when received is getElement of non awaited ChainableElement (typing not supported)', async () => { + const awaitedElements = await awaitElementOrArray(chainableElement.getElement() as any) + + expect(awaitedElements).toEqual({ + element: expect.objectContaining({ selector: element.selector }) + }) + expect(awaitedElements.elements).toBeUndefined() + }) + + test('should return single element when received is getElement of an awaited ChainableElement', async () => { + const awaitedElements = await awaitElementOrArray(await chainableElement.getElement()) + + expect(awaitedElements).toEqual({ + element: expect.objectContaining({ selector: element.selector }) + }) + expect(awaitedElements.elements).toBeUndefined() + }) + + test('should return single element when received is WebdriverIO.Element', async () => { + const awaitedElements = await awaitElementOrArray(element) + + expect(awaitedElements).toEqual({ + element: expect.objectContaining({ selector: element.selector }) + }) + expect(awaitedElements.elements).toBeUndefined() + }) + + test('should return multiple elements when received is WebdriverIO.Element[]', async () => { + const elementArray = [elementFactory('element1'), elementFactory('element2')] + + const awaitedElements = await awaitElementOrArray(elementArray) + + expect(awaitedElements.elements).toHaveLength(2) + expect(awaitedElements).toEqual({ + elements: expect.arrayContaining([ + expect.objectContaining({ selector: elementArray[0].selector }), expect.objectContaining({ selector: elementArray[1].selector }) + ]) + }) + expect(awaitedElements.elements).toHaveLength(2) + expect(awaitedElements.elements?.[0].selector).toEqual(elementArray[0].selector) + expect(awaitedElements.elements?.[1].selector).toEqual(elementArray[1].selector) + expect(awaitedElements.element).toBeUndefined() + }) + }) + + describe('given multiple elements', () => { + + let element1: WebdriverIO.Element + let element2: WebdriverIO.Element + let elementArray: WebdriverIO.Element[] + let chainableElementArray: ChainablePromiseArray + + beforeEach(() => { + element1 = elementFactory('element1') + element2 = elementFactory('element2') + elementArray = [element1, element2] + chainableElementArray = $$('element1') + }) + + test('should return multiple elements when received is a non-awaited ChainableElementArray', async () => { + const { elements, element } = await awaitElementOrArray(chainableElementArray) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + expect(element).toBeUndefined() + }) + + test('should return multiple elements when received is an awaited ChainableElementArray', async () => { + const { elements, element } = await awaitElementOrArray(await chainableElementArray) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + expect(element).toBeUndefined() + }) + + test('should return multiple elements when received is getElements of non awaited ChainableElement (typing not supported)', async () => { + const { elements, element } = await awaitElementOrArray(chainableElementArray.getElements() as any) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + expect(element).toBeUndefined() + }) + + test('should return multiple elements when received is getElements of an awaited ChainableElementArray', async () => { + const { elements, element } = await awaitElementOrArray(await chainableElementArray.getElements()) + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + expect(element).toBeUndefined() + }) + + test('should return multiple elements when received is WebdriverIO.Element[]', async () => { + const { elements, element } = await awaitElementOrArray(elementArray) + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element2.selector }) + ])) + expect(element).toBeUndefined() + }) + }) + + test('should return the same object when not any type related to Elements', async () => { + const anyOjbect = { foo: 'bar' } + + const { other } = await awaitElementOrArray(anyOjbect as any) + + expect(other).toBe(anyOjbect) + }) + + }) + + describe(awaitElementArray, () => { + + let element1: WebdriverIO.Element + let element2: WebdriverIO.Element + let elementArray: WebdriverIO.Element[] + let chainableElementArray: ChainablePromiseArray + + beforeEach(() => { + element1 = elementFactory('element1') + element2 = elementFactory('element2') + elementArray = [element1, element2] + chainableElementArray = $$('element1') + }) + + test('should return undefined when received is undefined', async () => { + const result = await awaitElementArray(undefined) + + expect(result).toEqual({ + other: undefined + }) }) - test('is array ', async () => { - const els = (await $$('sel')) as unknown as WebdriverIO.ElementArray - const actual = wrapExpectedWithArray(els, ['Test Actual', 'Test Actual'], 'Test Expected') - expect(actual).toEqual(['Test Expected']) + test('should return undefined when received is Promise of undefined (typing not supported)', async () => { + const result = await awaitElementArray(Promise.resolve(undefined) as any) + + expect(result).toEqual({ + other: undefined + }) + }) + + test('should return multiple elements when received is a non-awaited ChainableElementArray', async () => { + const { elements } = await awaitElementArray(chainableElementArray) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + }) + + test('should return multiple elements when received is an awaited ChainableElementArray', async () => { + const { elements } = await awaitElementArray(await chainableElementArray) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + }) + + test('should return multiple elements when received is getElements of non awaited ChainableElement (typing not supported)', async () => { + const { elements } = await awaitElementArray(chainableElementArray.getElements() as any) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + }) + + test('should return multiple elements when received is getElements of an awaited ChainableElementArray', async () => { + const { elements } = await awaitElementArray(await chainableElementArray.getElements()) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element1.selector }) + ])) + }) + + test('should return multiple elements when received is WebdriverIO.Element[]', async () => { + const { elements } = await awaitElementArray(elementArray) + + expect(elements).toHaveLength(2) + expect(elements).toEqual(expect.objectContaining([ + expect.objectContaining({ selector: element1.selector }), + expect.objectContaining({ selector: element2.selector }) + ])) + }) + + test('should return empty array when received is empty Element[]', async () => { + const { elements } = await awaitElementArray([]) + + expect(elements).toHaveLength(0) + expect(elements).toEqual([]) + }) + + test('should return the same object when not any type related to Elements', async () => { + const anyObject = { foo: 'bar' } + + const { other } = await awaitElementArray(anyObject as any) + + expect(other).toBe(anyObject) + }) + + }) + + describe(map, () => { + test('should map elements of type Element[]', async () => { + const elements: WebdriverIO.Element[] = [elementFactory('el1'), elementFactory('el2')] + const command = vi.fn().mockResolvedValue('mapped') + + const result = await map(elements, command) + + expect(result).toEqual(['mapped', 'mapped']) + expect(command).toHaveBeenCalledTimes(2) + expect(command).toHaveBeenCalledWith(elements[0], 0) + expect(command).toHaveBeenCalledWith(elements[1], 1) + }) + test('should map elements of type ElementArray', async () => { + const elements: WebdriverIO.ElementArray = await $$('elements').getElements() + const command = vi.fn().mockResolvedValue('mapped') + + const result = await map(elements, command) + + expect(result).toEqual(['mapped', 'mapped']) + expect(command).toHaveBeenCalledTimes(2) + expect(command).toHaveBeenCalledWith(elements[0], 0) + expect(command).toHaveBeenCalledWith(elements[1], 1) + }) + }) + + describe(isStrictlyElementArray, async () => { + test.for([ + await $$('elements').getElements(), + await $$('elements'), + elementArrayFactory('elements'), + await chainableElementArrayFactory('elements', 3), + ])('should return true for ElementArray: %s', async (elements) => { + const isElementArrayResult = isStrictlyElementArray(elements) + + expect(elements).toBeDefined() + expect(typeof elements).toBe('object') + expect(isElementArrayResult).toBe(true) + }) + + test.for([ + await $('elements'), + await $('elements').getElement(), + $$('elements'), + $$('elements').getElements(), + elementFactory('element'), + [elementFactory('element1'), elementFactory('element2')], + undefined, + null, + 42, + 'string', + {}, + Promise.resolve(true), + [] + ])('should return false for non-ElementArray: %s', async (elements) => { + const isElementArrayResult = isStrictlyElementArray(elements) + + expect(isElementArrayResult).toBe(false) + }) + }) + + describe(isElement, async () => { + test.for([ + await $('element').getElement(), + await $('element'), + elementFactory('element'), + notFoundElementFactory('notFoundElement') + ])('should return true for Element: %s', async (element) => { + const isElementResult = isElement(element) + + expect(isElementResult).toBe(true) + }) + + test.for([ + $$('elements'), + $$('elements').getElements(), + [elementFactory('element1'), elementFactory('element2')], + undefined, + null, + 42, + 'string', + {}, + Promise.resolve(true) + ])('should return false for non-Element: %s', async (element) => { + const isElementResult = isElement(element) + + expect(isElementResult).toBe(false) + }) + }) + + describe(isElementArrayLike, async () => { + test.for([ + await $$('elements').getElements(), + await $$('elements'), + elementArrayFactory('elements'), + await chainableElementArrayFactory('elements', 3), + [elementFactory('element1'), elementFactory('element2')], + [] + ])('should return true for ElementArray or Element[] %s', async (elements) => { + const isElementArrayResult = isElementArrayLike(elements) + + expect(isElementArrayResult).toBe(true) + }) + + test.for([ + await $('elements'), + await $('elements').getElement(), + $$('elements'), + $$('elements').getElements(), + undefined, + null, + 42, + 'string', + {}, + Promise.resolve(true), + [$('elements')], + [$$('elements')], + [await $$('elements')] + ])('should return false for non-ElementArray or non-Element[]: %s', async (elements) => { + const isElementArrayResult = isElementArrayLike(elements) + + expect(isElementArrayResult).toBe(false) + }) + }) + + describe(isElementOrArrayLike, async () => { + test.for([ + await $('element').getElement(), + await $('element'), + elementFactory('element'), + await $$('elements').getElements(), + await $$('elements'), + elementArrayFactory('elements'), + await chainableElementArrayFactory('elements', 3), + [elementFactory('element1'), elementFactory('element2')], + [] + ])('should return true for Element or ElementArray or Element[]: %s', async (element) => { + const result = isElementOrArrayLike(element) + + expect(result).toBe(true) + }) + + test.for([ + $$('elements'), + $$('elements').getElements(), + $('element'), + $('element').getElement(), + undefined, + null, + 42, + 'string', + {}, + Promise.resolve(true), + [$('elements')], + [$$('elements')], + [await $$('elements')] + ])('should return false for non-Element and non-ElementArray and non-Element[]: %s', async (element) => { + const result = isElementOrArrayLike(element) + + expect(result).toBe(false) }) }) }) diff --git a/test/util/executeCommand.test.ts b/test/util/executeCommand.test.ts new file mode 100644 index 000000000..c7b516ee6 --- /dev/null +++ b/test/util/executeCommand.test.ts @@ -0,0 +1,292 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest' +import { $, $$ } from '@wdio/globals' +import { executeCommand, defaultMultipleElementsIterationStrategy } from '../../src/util/executeCommand' + +vi.mock('@wdio/globals') + +describe(executeCommand, () => { + const conditionPass = vi.fn(async (_element: WebdriverIO.Element) => { + return ({ result: true, value: 'myValue' }) + }) + + describe('given single element', () => { + const selector = 'single-selector' + + test('ChainableElement', async () => { + const chainable = $(selector) + + expect(chainable).toBeInstanceOf(Promise) + + const result = await executeCommand(chainable, conditionPass) + + const unwrapped = await chainable + expect(result).toEqual({ + success: true, + valueOrArray: 'myValue', + elementOrArray: unwrapped, + results: [true] + }) + }) + + test('Element', async () => { + const element = await $(selector) + + const result = await executeCommand(element, conditionPass) + + expect(result).toEqual({ + success: true, + valueOrArray: 'myValue', + elementOrArray: element, + results: [true] + }) + }) + + test('Element with value result being an array', async () => { + const conditionPassWithValueArray = vi.fn(async (_element: WebdriverIO.Element) => { + return ({ result: true, value: ['myValue'] }) + }) + + const element = await $(selector) + + const result = await executeCommand(element, conditionPassWithValueArray) + + expect(result).toEqual({ + success: true, + valueOrArray: ['myValue'], + elementOrArray: element, + results: [true] + }) + }) + + test('Element with value result being an array of array', async () => { + const conditionPassWithValueArray = vi.fn(async (_element: WebdriverIO.Element) => { + return ({ result: true, value: [['myValue']] }) + }) + + const element = await $(selector) + + const result = await executeCommand(element, conditionPassWithValueArray) + + expect(result).toEqual({ + success: true, + valueOrArray: [['myValue']], + elementOrArray: element, + results: [true] + }) + }) + + test('when condition is not met', async () => { + const conditionPass = vi.fn(async (_element: WebdriverIO.Element) => { + return ({ result: false }) + }) + const chainable = $(selector) + + expect(chainable).toBeInstanceOf(Promise) + + const result = await executeCommand(chainable, conditionPass) + + const unwrapped = await chainable + expect(result).toEqual({ + success: false, + valueOrArray: undefined, + elementOrArray: unwrapped, + results: [false] + }) + }) + }) + + describe('given multiple elements', () => { + const selector = 'multi-selector' + + test('ChainableArray', async () => { + const chainableArray = $$(selector) + + expect(chainableArray).toBeInstanceOf(Promise) + + const result = await executeCommand(chainableArray, conditionPass) + + const unwrapped = await chainableArray + expect(result).toEqual({ + success: true, + valueOrArray: ['myValue', 'myValue'], + elementOrArray: unwrapped, + results: [true, true] + }) + }) + + test('ElementArray', async () => { + const elementArray = await $$(selector) + + const result = await executeCommand(elementArray, conditionPass) + + expect(result).toEqual({ + success: true, + valueOrArray: ['myValue', 'myValue'], + elementOrArray: elementArray, + results: [true, true] + }) + }) + + test('Element[]', async () => { + const elementArray = await $$(selector) + const elements = Array.from(elementArray) + + expect(Array.isArray(elements)).toBe(true) + + const result = await executeCommand(elements, conditionPass) + + expect(result).toEqual({ + success: true, + valueOrArray: ['myValue', 'myValue'], + elementOrArray: elements, + results: [true, true] + }) + }) + + test('Arrray of value', async () => { + const conditionPassWithValueArray = vi.fn(async (_element: WebdriverIO.Element) => { + return ({ result: true, value: ['myValue'] }) + }) + + const elementArray = await $$(selector) + const elements = Array.from(elementArray) + + expect(Array.isArray(elements)).toBe(true) + + const result = await executeCommand(elements, conditionPassWithValueArray) + + expect(result).toEqual({ + success: true, + valueOrArray: [['myValue'], ['myValue']], + elementOrArray: elements, + results: [true, true] + }) + }) + }) + + describe('given not elements', () => { + test('undefined', async () => { + const result = await executeCommand(undefined as any, conditionPass) + + expect(result).toEqual({ + success: false, + valueOrArray: undefined, + elementOrArray: undefined, + results: [] + }) + }) + + test('empty array', async () => { + const result = await executeCommand([], conditionPass) + + expect(result).toEqual({ + success: false, + valueOrArray: undefined, + elementOrArray: [], + results: [] + }) + }) + + test('object', async () => { + const anyOjbect = { foo: 'bar' } + + const result = await executeCommand(anyOjbect as any, conditionPass) + + expect(result).toEqual({ + success: false, + valueOrArray: undefined, + elementOrArray: { foo: 'bar' }, + results: [] + }) + }) + + test('number', async () => { + const anyNumber = 42 + + const result = await executeCommand(anyNumber as any, conditionPass) + + expect(result).toEqual({ + success: false, + valueOrArray: undefined, + elementOrArray: 42, + results: [] + }) + }) + }) + + describe('error handling', () => { + test('should throw if no strategies are provided', async () => { + const element = await $('some-selector') + + await expect(executeCommand(element)).rejects.toThrowError('No condition or customMultipleElementCompareStrategy provided to executeCommand') + }) + }) +}) + +describe(defaultMultipleElementsIterationStrategy, () => { + + describe('given single element', () => { + let singleElement: WebdriverIO.Element + let condition: (el: WebdriverIO.Element, expected: any) => Promise<{ result: boolean; value: any }> + + beforeEach(async () => { + singleElement = await $('single-mock-element').getElement() + condition = vi.fn(async (_el, expected) => ({ result: true, value: expected })) + }) + + test('should handle single element and single expected value', async () => { + const result = await defaultMultipleElementsIterationStrategy(singleElement, 'val', condition) + + expect(result).toEqual([{ result: true, value: 'val' }]) + }) + + test('should fail if single element and expected value is array (default)', async () => { + const result = await defaultMultipleElementsIterationStrategy(singleElement, ['val'], condition) + + expect(result).toEqual([{ result: false, value: 'Expected value cannot be an array' }]) + }) + + test('should handle single element and expected value array if supportArrayForSingleElement is true', async () => { + const result = await defaultMultipleElementsIterationStrategy(singleElement, ['val'], condition, { supportArrayForSingleElement: true }) + + expect(result).toEqual([{ result: true, value: ['val'] }]) + expect(condition).toHaveBeenCalledTimes(1) + }) + }) + + describe('given multiple elements', () => { + let elements: WebdriverIO.ElementArray + let condition: (el: WebdriverIO.Element, expected: any) => Promise<{ result: boolean; value: any }> + + beforeEach(async () => { + elements = await $$('elements').getElements() + expect(elements.length).toBe(2) + condition = vi.fn() + .mockResolvedValueOnce({ result: true, value: 'val1' }) + .mockResolvedValueOnce({ result: true, value: 'val2' }) + }) + + test('should iterate over array of elements and array of expected values', async () => { + const result = await defaultMultipleElementsIterationStrategy(elements, ['val1', 'val2'], condition) + + expect(result).toEqual([{ result: true, value: 'val1' }, { result: true, value: 'val2' }]) + expect(condition).toHaveBeenCalledTimes(2) + }) + + test('should fail if array lengths mismatch', async () => { + const result = await defaultMultipleElementsIterationStrategy([elements[0]] as any, ['val1', 'val2'], condition) + + expect(result).toEqual([{ result: false, value: 'Received array length 1, expected 2' }]) + }) + + test('should iterate over array of elements and single expected value', async () => { + condition = vi.fn() + .mockResolvedValue({ result: true, value: 'val' }) + + const result = await defaultMultipleElementsIterationStrategy(elements, 'val', condition) + + expect(result).toEqual([{ result: true, value: 'val' }, { result: true, value: 'val' }]) + expect(condition).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/test/util/formatMessage.test.ts b/test/util/formatMessage.test.ts index 0bfcdd287..0d42b7bcc 100644 --- a/test/util/formatMessage.test.ts +++ b/test/util/formatMessage.test.ts @@ -1,176 +1,639 @@ -import { test, describe, beforeEach, expect } from 'vitest' -import { printDiffOrStringify, printExpected, printReceived } from 'jest-matcher-utils' +import { test, describe, beforeEach, expect, vi } from 'vitest' +import { INVERTED_COLOR, printDiffOrStringify } from 'jest-matcher-utils' -import { enhanceError, numberError } from '../../src/util/formatMessage.js' +import { enhanceError, enhanceErrorBe } from '../../src/util/formatMessage.js' +import { elementArrayFactory, elementFactory } from '../__mocks__/@wdio/globals.js' + +vi.mock('jest-matcher-utils', async (importActual) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importActual() + return { + ...actual, + INVERTED_COLOR: vi.fn(actual.INVERTED_COLOR) + } +}) describe('formatMessage', () => { - describe('enhanceError', () => { + describe(enhanceError, () => { describe('default', () => { - let actual: string + let actualFailureMessage: string + const expected = 'Test Expected Value' + const actual = 'Test Actual Value' beforeEach(() => { - actual = enhanceError( - 'Test Subject', - 'Test Expected', - 'Test Actual', + actualFailureMessage = enhanceError( + 'window', + expected, + actual, { isNot: false }, - 'Test Verb', - 'Test Expectation', - '', - { message: '', containing: false } + 'have', + 'title', ) }) - test('starting message', () => { - expect(actual).toMatch('Expect Test Subject to Test Verb Test Expectation') + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Expect window to have title + +Expected: "Test Expected Value" +Received: "Test Actual Value"`) }) test('diff string', () => { - const diffString = printDiffOrStringify('Test Expected', 'Test Actual', 'Expected', 'Received', true) - expect(actual).toMatch(diffString) + const diffString = printDiffOrStringify('Test Expected Value', 'Test Actual Value', 'Expected', 'Received', true) + expect(diffString).toEqual(`\ +Expected: "Test Expected Value" +Received: "Test Actual Value"`) + expect(actualFailureMessage).toMatch(diffString) }) }) describe('isNot', () => { - let actual: string + let actualFailureMessage: string + const isNot = true + + describe('same', () => { + const expected = 'Test Same' + const actual = expected - describe('different', () => { beforeEach(() => { - actual = enhanceError( - 'Test Subject', - 'Test Expected', - 'Test Actual', - { isNot: true }, - 'Test Verb', - 'Test Expectation', - '', - { message: '', containing: false } + actualFailureMessage = enhanceError( + 'window', + expected, + actual, + { isNot }, + 'have', + 'title' ) }) - test('starting message', () => { - expect(actual).toMatch('Expect Test Subject not to Test Verb Test Expectation') + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Expect window not to have title + +Expected [not]: "Test Same" +Received : "Test Same"`) }) test('diff string', () => { - const diffString = printDiffOrStringify('Test Expected', 'Test Actual', 'Expected [not]', 'Received ', true) - expect(actual).toMatch(diffString) + const diffString = `\ +Expected [not]: "Test Same" +Received : "Test Same"` + expect(actualFailureMessage).toMatch(diffString) }) }) + }) + + describe('containing', () => { + let actualFailureMessage: string + + describe('isNot false', () => { + const expected = 'Test Expected Value' + const actual = 'Test Actual Value' + const isNot = false - describe('same', () => { beforeEach(() => { - actual = enhanceError( - 'Test Subject', - 'Test Same', - 'Test Same', - { isNot: true }, - 'Test Verb', - 'Test Expectation', + actualFailureMessage = enhanceError( + 'window', + expected, + actual, + { isNot }, + 'have', + 'title', '', - { message: '', containing: false } + { message: '', containing: true } ) }) - test('starting message', () => { - expect(actual).toMatch('Expect Test Subject not to Test Verb Test Expectation') - }) + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Expect window to have title containing - test('diff string', () => { - const diffString = `Expected [not]: ${printExpected('Test Same')}\n` + - `Received : ${printReceived('Test Same')}` - expect(actual).toMatch(diffString) +Expected: "Test Expected Value" +Received: "Test Actual Value"`) }) }) + describe('isNot true', () => { + const expected = 'same value' + const actual = expected + const isNot = true + + beforeEach(() => { + actualFailureMessage = enhanceError( + 'window', + expected, + actual, + { isNot }, + 'have', + 'title', + '', + { message: '', containing: true } + ) + }) + + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Expect window not to have title containing + +Expected [not]: "same value" +Received : "same value"`) + }) + }) }) - describe('containing', () => { - let actual: string + describe('custom message', () => { + let actualFailureMessage: string + const customPrefixMessage = 'Test Message' beforeEach(() => { - actual = enhanceError( - 'Test Subject', - 'Test Expected', - 'Test Actual', + actualFailureMessage = enhanceError( + 'window', + 'Test Expected Value', + 'Test Actual Value', { isNot: false }, - 'Test Verb', - 'Test Expectation', + 'have', + 'title', '', - { message: '', containing: true } + { message: customPrefixMessage, containing: false } ) }) - test('starting message', () => { - expect(actual).toMatch('Expect Test Subject to Test Verb Test Expectation containing') - }) + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Test Message +Expect window to have title - test('diff string', () => { - const diffString = printDiffOrStringify('Test Expected', 'Test Actual', 'Expected', 'Received', true) - expect(actual).toMatch(diffString) +Expected: "Test Expected Value" +Received: "Test Actual Value"`) }) }) - describe('message', () => { - let actual: string + describe('Expected Value Argument 2', () => { + let actualFailureMessage: string + const expectedArg2 = 'myPropertyName' - beforeEach(() => { - actual = enhanceError( - 'Test Subject', - 'Test Expected', - 'Test Actual', - { isNot: false }, - 'Test Verb', - 'Test Expectation', - '', - { message: 'Test Message', containing: false } - ) + describe('isNot false', () => { + const expected = 'Expected Property Value' + const actual = 'Actual Property Value' + const isNot = false + + beforeEach(() => { + actualFailureMessage = enhanceError( + 'window', + expected, + actual, + { isNot }, + 'have', + 'property', + expectedArg2, + ) + }) + + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Expect window to have property myPropertyName + +Expected: "Expected Property Value" +Received: "Actual Property Value"`) + }) }) - test('starting message', () => { - expect(actual).toMatch('Test Message\nExpect Test Subject to Test Verb Test Expectation') + describe('isNot true', () => { + const expected = 'Expected Property Value' + const actual = 'Actual Property Value' + const isNot = true + + beforeEach(() => { + actualFailureMessage = enhanceError( + 'window', + expected, + actual, + { isNot }, + 'have', + 'property', + expectedArg2, + ) + }) + + test('message', () => { + expect(actualFailureMessage).toEqual(`\ +Expect window not to have property myPropertyName + +Expected [not]: "Expected Property Value" +Received : "Actual Property Value"`) + }) }) + }) - test('diff string', () => { - const diffString = printDiffOrStringify('Test Expected', 'Test Actual', 'Expected', 'Received', true) - expect(actual).toMatch(diffString) + test.for([ + { actual: undefined, selectorName: 'undefined' }, + { actual: null, selectorName: 'null' }, + { actual: true, selectorName: 'true' }, + { actual: 5, selectorName: '5' }, + { actual: 'test', selectorName: 'test' }, + { actual: {}, selectorName: '{}' }, + { actual: ['1', '2'], selectorName: '["1","2"]' }, + ])('should return failure message for unsupported type $actual when isNot is false', async ({ actual, selectorName }) => { + const result = await enhanceError(actual as any, 'webdriverio', undefined, { isNot: false }, 'have', 'text') + + expect(result).toEqual(`\ +Expect ${selectorName} to have text + +Expected: "webdriverio" +Received: undefined`) + }) + + test.for([ + { actual: undefined, selectorName: 'undefined' }, + { actual: null, selectorName: 'null' }, + { actual: true, selectorName: 'true' }, + { actual: 5, selectorName: '5' }, + { actual: 'test', selectorName: 'test' }, + { actual: {}, selectorName: '{}' }, + { actual: ['1', '2'], selectorName: '["1","2"]' }, + ])('should return failure message for unsupported type $actual when isNot is true', async ({ actual, selectorName }) => { + const result = await enhanceError(actual as any, 'webdriverio', undefined, { isNot: true }, 'have', 'text') + + expect(result).toEqual(`\ +Expect ${selectorName} not to have text + +Expected [not]: "webdriverio" +Received : undefined`) + }) + + describe('given multiple elements', () => { + const elements = elementArrayFactory('elements', 2) + const elementName = '$$(`elements`)' + + describe('elements when isNot is false', () => { + const isNot = false + test('all elements failure', () => { + const expected = ['Test Expected Value 1', 'Test Expected Value 2'] + const actual = ['Test Actual Value 1', 'Test Actual Value 2'] + + const actualFailureMessage = enhanceError( + elements, + expected, + actual, + { isNot }, + 'have', + 'text', + ) + + expect(actualFailureMessage).toEqual(`\ +Expect ${elementName} to have text + +- Expected - 2 ++ Received + 2 + + Array [ +- "Test Expected Value 1", +- "Test Expected Value 2", ++ "Test Actual Value 1", ++ "Test Actual Value 2", + ]`) + }) + + test('First elements failure', () => { + const expected = ['Test Expected Value 1', 'Test Expected Value 2'] + const actual = ['Test Actual Value 1', 'Test Expected Value 2'] + + const actualFailureMessage = enhanceError( + elements, + expected, + actual, + { isNot }, + 'have', + 'text', + ) + + expect(actualFailureMessage).toEqual(`\ +Expect ${elementName} to have text + +- Expected - 1 ++ Received + 1 + + Array [ +- "Test Expected Value 1", ++ "Test Actual Value 1", + "Test Expected Value 2", + ]`) + }) + + test('Seconds elements failure', () => { + const expected = ['Test Expected Value 1', 'Test Expected Value 2'] + const actual = ['Test Expected Value 1', 'Test Actual Value 2'] + + const actualFailureMessage = enhanceError( + elements, + expected, + actual, + { isNot }, + 'have', + 'text', + ) + + expect(actualFailureMessage).toEqual(`\ +Expect ${elementName} to have text + +- Expected - 1 ++ Received + 1 + + Array [ + "Test Expected Value 1", +- "Test Expected Value 2", ++ "Test Actual Value 2", + ]`) + }) + }) + + describe('elements when isNot is true', () => { + const isNot = true + test('all elements failure then all values are highlighted as failure', () => { + const expected = ['Test Expected Value 1', 'Test Expected Value 2'] + const actual = ['Test Expected Value 1', 'Test Expected Value 2'] + + const actualFailureMessage = enhanceError( + elements, + expected, + actual, + { isNot }, + 'have', + 'text', + ) + + expect(actualFailureMessage).toEqual(`\ +Expect ${elementName} not to have text + +Expected [not]: ["Test Expected Value 1", "Test Expected Value 2"] +Received : ["Test Expected Value 1", "Test Expected Value 2"]` + ) + + expect(INVERTED_COLOR).toHaveBeenCalledTimes(4) + }) + + test('First elements failure then only first values are highlighted as failure', () => { + const expected = ['Test Expected Value 1', 'Test Expected Value 2'] + const actual = ['Test Expected Value 1', 'Test Actual Value 2'] + + const actualFailureMessage = enhanceError( + elements, + expected, + actual, + { isNot }, + 'have', + 'text', + ) + + expect(actualFailureMessage).toEqual(`\ +Expect ${elementName} not to have text + +Expected [not]: ["Test Expected Value 1", "Test Expected Value 2"] +Received : ["Test Expected Value 1", "Test Actual Value 2"]` + ) + + expect(INVERTED_COLOR).toHaveBeenCalledTimes(2) + expect(INVERTED_COLOR).toHaveBeenNthCalledWith(1, '"Test Expected Value 1"') + expect(INVERTED_COLOR).toHaveBeenNthCalledWith(2, '"Test Expected Value 1"') + }) + + test('Second elements failure then only second values are highlighted as failure', () => { + const expected = ['Test Expected Value 1', 'Test Expected Value 2'] + const actual = ['Test Actual Value 1', 'Test Expected Value 2'] + + const actualFailureMessage = enhanceError( + elements, + expected, + actual, + { isNot }, + 'have', + 'text', + ) + + expect(actualFailureMessage).toEqual(`\ +Expect ${elementName} not to have text + +Expected [not]: ["Test Expected Value 1", "Test Expected Value 2"] +Received : ["Test Actual Value 1", "Test Expected Value 2"]` + ) + + expect(INVERTED_COLOR).toHaveBeenCalledTimes(2) + expect(INVERTED_COLOR).toHaveBeenNthCalledWith(1, '"Test Expected Value 2"') + expect(INVERTED_COLOR).toHaveBeenNthCalledWith(2, '"Test Expected Value 2"') + }) }) }) + }) - describe('arg2', () => { - let actual: string + describe(enhanceErrorBe, () => { + const verb = 'be' + const expectation = 'displayed' + const options = {} - beforeEach(() => { - actual = enhanceError( - 'Test Subject', - 'Test Expected', - 'Test Actual', - { isNot: false }, - 'Test Verb', - 'Test Expectation', - 'Test Arg2', - { message: 'Test Message', containing: false } - ) + describe('given a single element', () => { + const subject = elementFactory('element') + + const isNot = false + test('when isNot is false and failure with result having pass=false', () => { + const message = enhanceErrorBe(subject, [false], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $(\`element\`) to be displayed + +Expected: "displayed" +Received: "not displayed"`) }) - test('starting message', () => { - expect(actual).toMatch('Expect Test Subject to Test Verb Test Expectation Test Arg2') + test('with custom message', () => { + const customMessage = 'Custom Error Message' + const message = enhanceErrorBe(subject, [false], { isNot, verb, expectation }, { ...options, message: customMessage }) + expect(message).toEqual(`\ +Custom Error Message +Expect $(\`element\`) to be displayed + +Expected: "displayed" +Received: "not displayed"`) }) - test('diff string', () => { - const diffString = printDiffOrStringify('Test Expected', 'Test Actual', 'Expected', 'Received', true) - expect(actual).toMatch(diffString) + test('when isNot is true and failure with result having pass=true (inverted later by Jest)', () => { + const isNot = true + const message = enhanceErrorBe(subject, [true], { isNot, verb, expectation }, options) + expect(message).toEqual(`\ +Expect $(\`element\`) not to be displayed + +Expected: "not displayed" +Received: "displayed"`) + + }) + + test('when isNot is true and failure with result having pass=true (inverted later by Jest)', () => { + const isNot = true + const message = enhanceErrorBe(subject, [true], { isNot, verb, expectation }, options) + expect(message).toEqual(`\ +Expect $(\`element\`) not to be displayed + +Expected: "not displayed" +Received: "displayed"`) + + }) + + test.for([ + { actual: undefined, selectorName: 'undefined' }, + { actual: null, selectorName: 'null' }, + { actual: true, selectorName: 'true' }, + { actual: 5, selectorName: '5' }, + { actual: 'test', selectorName: 'test' }, + { actual: {}, selectorName: '{}' }, + { actual: ['1', '2'], selectorName: '["1","2"]' }, + ])('should return failure message for unsupported type $actual when isNot is false and not result from element function call', async ({ actual: subject, selectorName }) => { + const result = await enhanceErrorBe(subject as any, [], { isNot, verb, expectation }, options) + + expect(result).toEqual(`\ +Expect ${selectorName} to be displayed + +Expected: "displayed" +Received: "not displayed"`) + }) + + test.for([ + { actual: undefined, selectorName: 'undefined' }, + { actual: null, selectorName: 'null' }, + { actual: true, selectorName: 'true' }, + { actual: 5, selectorName: '5' }, + { actual: 'test', selectorName: 'test' }, + { actual: {}, selectorName: '{}' }, + { actual: ['1', '2'], selectorName: '["1","2"]' }, + ])('should return failure message for unsupported type $actual when isNot is true and not result from element function call', async ({ actual: subject, selectorName }) => { + const result = await enhanceErrorBe(subject as any, [], { isNot: true, verb, expectation }, options) + + expect(result).toEqual(`\ +Expect ${selectorName} not to be displayed + +Expected: "not displayed" +Received: "displayed"`) }) }) - }) - describe('numberError', () => { - test('should return correct message', () => { - expect(numberError()).toBe('no params') - expect(numberError({ eq: 0 })).toBe(0) - expect(numberError({ gte: 1 })).toBe('>= 1') - expect(numberError({ lte: 1 })).toBe(' <= 1') - expect(numberError({ gte: 2, lte: 1 })).toBe('>= 2 && <= 1') + describe('given multiples elements', () => { + const subject = elementArrayFactory('elements', 2) + + describe('when isNot is false', () => { + const isNot = false + + test('failure with all results having pass=false', () => { + const message = enhanceErrorBe(subject, [false, false], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $$(\`elements\`) to be displayed + +- Expected - 2 ++ Received + 2 + + Array [ +- "displayed", +- "displayed", ++ "not displayed", ++ "not displayed", + ]`) + }) + + test('failure with first results having pass=true', () => { + const message = enhanceErrorBe(subject, [true, false], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $$(\`elements\`) to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ + "displayed", +- "displayed", ++ "not displayed", + ]`) + }) + + test('failure with second results having pass=true', () => { + const message = enhanceErrorBe(subject, [false, true], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $$(\`elements\`) to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ +- "displayed", ++ "not displayed", + "displayed", + ]`) + }) + + test('when no element', () => { + const message = enhanceErrorBe([], [], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect [] to be displayed + +Expected: "at least one result" +Received: []`) + }) + }) + + describe('when isNot is true where failure are pass=true since Jest inverts the result', () => { + const isNot = true + + test('failure with all results having pass=true', () => { + const message = enhanceErrorBe(subject, [true, true], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $$(\`elements\`) not to be displayed + +- Expected - 2 ++ Received + 2 + + Array [ +- "not displayed", +- "not displayed", ++ "displayed", ++ "displayed", + ]`) + }) + + test('failure with first results having success pass=false (inverted later)', () => { + const message = enhanceErrorBe(subject, [false, true], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $$(\`elements\`) not to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ + "not displayed", +- "not displayed", ++ "displayed", + ]`) + }) + + test('failure with second results having success pass=false (inverted later)', () => { + const message = enhanceErrorBe(subject, [true, false], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect $$(\`elements\`) not to be displayed + +- Expected - 1 ++ Received + 1 + + Array [ +- "not displayed", ++ "displayed", + "not displayed", + ]`) + }) + + test('when no elements', () => { + const message = enhanceErrorBe([], [], { isNot, verb, expectation }, options ) + expect(message).toEqual(`\ +Expect [] not to be displayed + +Expected: "at least one result" +Received: []`) + }) + }) }) }) }) diff --git a/test/util/numberOptionsUtil.test.ts b/test/util/numberOptionsUtil.test.ts new file mode 100644 index 000000000..5efc6d48e --- /dev/null +++ b/test/util/numberOptionsUtil.test.ts @@ -0,0 +1,315 @@ +import { test, describe, expect } from 'vitest' +import { + isNumber, + validateNumberOptions, + validateNumberOptionsArray, + NumberMatcher, + numberMatcherTester +} from '../../src/util/numberOptionsUtil.js' + +describe('numberOptionsUtil', () => { + describe(isNumber, () => { + test('returns true for numbers', () => { + expect(isNumber(0)).toBe(true) + expect(isNumber(1)).toBe(true) + expect(isNumber(-1)).toBe(true) + expect(isNumber(3.14)).toBe(true) + expect(isNumber(Number.MAX_VALUE)).toBe(true) + expect(isNumber(Number.MIN_VALUE)).toBe(true) + expect(isNumber(Infinity)).toBe(true) + expect(isNumber(-Infinity)).toBe(true) + expect(isNumber(NaN)).toBe(true) + }) + + test('returns false for non-numbers', () => { + expect(isNumber('5')).toBe(false) + expect(isNumber(null)).toBe(false) + expect(isNumber(undefined)).toBe(false) + expect(isNumber(true)).toBe(false) + expect(isNumber({})).toBe(false) + expect(isNumber([])).toBe(false) + expect(isNumber(() => {})).toBe(false) + }) + }) + + describe(validateNumberOptions, () => { + test('converts plain number to NumberMatcher with eq option', () => { + const result = validateNumberOptions(5) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect(result.numberMatcher.equals(5)).toBe(true) + expect(result.numberMatcher.toString()).toBe('5') + expect(result.numberCommandOptions).toBeUndefined() + }) + + test('converts NumberOptions with eq to NumberMatcher and extract command options', () => { + const result = validateNumberOptions({ eq: 10, wait: 2000, interval: 100 }) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect(result.numberMatcher.equals(10)).toBe(true) + expect(result.numberCommandOptions).toEqual({ wait: 2000, interval: 100 }) + }) + + test('converts NumberOptions with gte to NumberMatcher', () => { + const result = validateNumberOptions({ gte: 5 }) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect(result.numberMatcher.equals(5)).toBe(true) + expect(result.numberMatcher.equals(10)).toBe(true) + expect(result.numberMatcher.equals(4)).toBe(false) + }) + + test('converts NumberOptions with lte to NumberMatcher', () => { + const result = validateNumberOptions({ lte: 10 }) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect(result.numberMatcher.equals(10)).toBe(true) + expect(result.numberMatcher.equals(5)).toBe(true) + expect(result.numberMatcher.equals(11)).toBe(false) + }) + + test('converts NumberOptions with gte and lte to NumberMatcher', () => { + const result = validateNumberOptions({ gte: 5, lte: 10 }) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect(result.numberMatcher.equals(5)).toBe(true) + expect(result.numberMatcher.equals(7)).toBe(true) + expect(result.numberMatcher.equals(10)).toBe(true) + expect(result.numberMatcher.equals(4)).toBe(false) + expect(result.numberMatcher.equals(11)).toBe(false) + }) + + test('throws error for invalid options', () => { + expect(() => validateNumberOptions({} as any)).toThrow('Invalid NumberOptions. Received: {}') + expect(() => validateNumberOptions(null as any)).toThrow('Invalid NumberOptions. Received: null') + expect(() => validateNumberOptions({ wait: 1000 } as any)).toThrow('Invalid NumberOptions') + }) + }) + + describe(validateNumberOptionsArray, () => { + test('converts array of numbers to array of NumberMatchers', () => { + const result = validateNumberOptionsArray([1, 2, 3]) + + expect(Array.isArray(result.numberMatcher)).toBe(true) + expect(result.numberMatcher).toHaveLength(3) + expect((result.numberMatcher as NumberMatcher[])[0].equals(1)).toBe(true) + expect((result.numberMatcher as NumberMatcher[])[1].equals(2)).toBe(true) + expect((result.numberMatcher as NumberMatcher[])[2].equals(3)).toBe(true) + expect(result.numberCommandOptions).toBeUndefined() + }) + + test('converts array of NumberOptions to array of NumberMatchers', () => { + const result = validateNumberOptionsArray([{ eq: 1 }, { gte: 5 }, { lte: 10 }]) + + expect(Array.isArray(result.numberMatcher)).toBe(true) + expect(result.numberMatcher).toHaveLength(3) + expect((result.numberMatcher as NumberMatcher[])[0].equals(1)).toBe(true) + expect((result.numberMatcher as NumberMatcher[])[1].equals(5)).toBe(true) + expect((result.numberMatcher as NumberMatcher[])[2].equals(10)).toBe(true) + }) + + test('converts single number to NumberMatcher', () => { + const result = validateNumberOptionsArray(5) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect((result.numberMatcher as NumberMatcher).equals(5)).toBe(true) + }) + + test('converts single NumberOptions to NumberMatcher', () => { + const result = validateNumberOptionsArray({ gte: 5, lte: 10 }) + + expect(result.numberMatcher).toBeInstanceOf(NumberMatcher) + expect((result.numberMatcher as NumberMatcher).equals(7)).toBe(true) + }) + + test('converts single NumberOptions to command options', () => { + const { numberMatcher, numberCommandOptions } = validateNumberOptionsArray({ gte: 5, lte: 10, wait: 2000, interval: 100 }) + + expect(numberMatcher).toBeInstanceOf(NumberMatcher) + expect(numberCommandOptions).toEqual({ wait: 2000, interval: 100 }) + }) + + test('Does not converts multiple NumberOptions to command options since it is not supported', () => { + const { numberMatcher, numberCommandOptions } = validateNumberOptionsArray([{ gte: 5, lte: 10, wait: 2000, interval: 100 }]) + + expect(numberMatcher).toBeInstanceOf(Array) + expect(numberCommandOptions).toBeUndefined() + }) + }) + + describe(NumberMatcher, () => { + describe('equals', () => { + test('returns false for undefined', () => { + const matcher = new NumberMatcher({ eq: 5 }) + expect(matcher.equals(undefined)).toBe(false) + }) + + describe('with eq option', () => { + test('returns true for exact match', () => { + const matcher = new NumberMatcher({ eq: 5 }) + expect(matcher.equals(5)).toBe(true) + }) + + test('returns false for non-match', () => { + const matcher = new NumberMatcher({ eq: 5 }) + expect(matcher.equals(4)).toBe(false) + expect(matcher.equals(6)).toBe(false) + }) + + test('works with 0', () => { + const matcher = new NumberMatcher({ eq: 0 }) + expect(matcher.equals(0)).toBe(true) + expect(matcher.equals(1)).toBe(false) + }) + }) + + describe('with gte option', () => { + test('returns true for values greater than or equal', () => { + const matcher = new NumberMatcher({ gte: 5 }) + expect(matcher.equals(5)).toBe(true) + expect(matcher.equals(6)).toBe(true) + expect(matcher.equals(100)).toBe(true) + }) + + test('returns false for values less than', () => { + const matcher = new NumberMatcher({ gte: 5 }) + expect(matcher.equals(4)).toBe(false) + expect(matcher.equals(0)).toBe(false) + }) + }) + + describe('with lte option', () => { + test('returns true for values less than or equal', () => { + const matcher = new NumberMatcher({ lte: 10 }) + expect(matcher.equals(10)).toBe(true) + expect(matcher.equals(9)).toBe(true) + expect(matcher.equals(0)).toBe(true) + }) + + test('returns false for values greater than', () => { + const matcher = new NumberMatcher({ lte: 10 }) + expect(matcher.equals(11)).toBe(false) + expect(matcher.equals(100)).toBe(false) + }) + }) + + describe('with gte and lte options', () => { + test('returns true for values in range', () => { + const matcher = new NumberMatcher({ gte: 5, lte: 10 }) + expect(matcher.equals(5)).toBe(true) + expect(matcher.equals(7)).toBe(true) + expect(matcher.equals(10)).toBe(true) + }) + + test('returns false for values outside range', () => { + const matcher = new NumberMatcher({ gte: 5, lte: 10 }) + expect(matcher.equals(4)).toBe(false) + expect(matcher.equals(11)).toBe(false) + }) + }) + + describe('with no options', () => { + test('returns false for any value', () => { + const matcher = new NumberMatcher({}) + expect(matcher.equals(0)).toBe(false) + expect(matcher.equals(5)).toBe(false) + expect(matcher.equals(100)).toBe(false) + }) + }) + }) + + describe('toString', () => { + test('returns string number for eq option', () => { + expect(new NumberMatcher({ eq: 5 }).toString()).toBe('5') + expect(new NumberMatcher({ eq: 0 }).toString()).toBe('0') + expect(new NumberMatcher({ eq: -10 }).toString()).toBe('-10') + }) + + test('returns range string for gte and lte options', () => { + expect(new NumberMatcher({ gte: 5, lte: 10 }).toString()).toBe('>= 5 && <= 10') + expect(new NumberMatcher({ gte: 0, lte: 100 }).toString()).toBe('>= 0 && <= 100') + }) + + test('returns gte string for gte option only', () => { + expect(new NumberMatcher({ gte: 5 }).toString()).toBe('>= 5') + expect(new NumberMatcher({ gte: 0 }).toString()).toBe('>= 0') + }) + + test('returns lte string for lte option only', () => { + expect(new NumberMatcher({ lte: 10 }).toString()).toBe('<= 10') + expect(new NumberMatcher({ lte: 0 }).toString()).toBe('<= 0') + }) + + test('returns error message for invalid options', () => { + expect(new NumberMatcher({}).toString()).toBe('Incorrect number options provided') + }) + }) + + describe('toJSON', () => { + test('returns number for eq option', () => { + expect(new NumberMatcher({ eq: 5 }).toJSON()).toBe(5) + expect(new NumberMatcher({ eq: 0 }).toJSON()).toBe(0) + expect(new NumberMatcher({ eq: -10 }).toJSON()).toBe(-10) + }) + + test('returns string for range options', () => { + expect(new NumberMatcher({ gte: 5, lte: 10 }).toJSON()).toBe('>= 5 && <= 10') + expect(new NumberMatcher({ gte: 5 }).toJSON()).toBe('>= 5') + expect(new NumberMatcher({ lte: 10 }).toJSON()).toBe('<= 10') + }) + + test('serializes correctly with JSON.stringify', () => { + expect(JSON.stringify(new NumberMatcher({ eq: 5 }))).toBe('5') + expect(JSON.stringify(new NumberMatcher({ gte: 5, lte: 10 }))).toBe('">= 5 && <= 10"') + expect(JSON.stringify([new NumberMatcher({ eq: 1 }), new NumberMatcher({ eq: 2 })])).toBe('[1,2]') + }) + }) + }) + + describe(numberMatcherTester, () => { + test('returns true when NumberMatcher matches number', () => { + const matcher = new NumberMatcher({ eq: 5 }) + + expect(numberMatcherTester(matcher, 5)).toBe(true) + expect(numberMatcherTester(5, matcher)).toBe(true) + }) + + test('returns false when NumberMatcher does not match number', () => { + const matcher = new NumberMatcher({ eq: 5 }) + + expect(numberMatcherTester(matcher, 10)).toBe(false) + expect(numberMatcherTester(10, matcher)).toBe(false) + }) + + test('works with range matchers', () => { + const matcher = new NumberMatcher({ gte: 5, lte: 10 }) + + expect(numberMatcherTester(matcher, 7)).toBe(true) + expect(numberMatcherTester(7, matcher)).toBe(true) + expect(numberMatcherTester(matcher, 3)).toBe(false) + expect(numberMatcherTester(3, matcher)).toBe(false) + }) + + test('returns undefined for non-NumberMatcher comparisons', () => { + expect(numberMatcherTester(5, 5)).toBeUndefined() + expect(numberMatcherTester('5', 5)).toBeUndefined() + expect(numberMatcherTester({}, 5)).toBeUndefined() + expect(numberMatcherTester(null, 5)).toBeUndefined() + }) + + test('returns undefined when both are NumberMatchers', () => { + const matcher1 = new NumberMatcher({ eq: 5 }) + const matcher2 = new NumberMatcher({ eq: 5 }) + + expect(numberMatcherTester(matcher1, matcher2)).toBeUndefined() + }) + + test('returns undefined when neither is a number', () => { + const matcher = new NumberMatcher({ eq: 5 }) + + expect(numberMatcherTester(matcher, '5')).toBeUndefined() + expect(numberMatcherTester(matcher, null)).toBeUndefined() + expect(numberMatcherTester(matcher, undefined)).toBeUndefined() + }) + }) +}) diff --git a/test/util/refetchElements.test.ts b/test/util/refetchElements.test.ts index 39543a0e8..71dd3e91f 100644 --- a/test/util/refetchElements.test.ts +++ b/test/util/refetchElements.test.ts @@ -2,60 +2,49 @@ import { vi, test, describe, beforeEach, expect } from 'vitest' import { $$ } from '@wdio/globals' import { refetchElements } from '../../src/util/refetchElements.js' +import { browserFactory, chainableElementArrayFactory, elementFactory } from '../__mocks__/@wdio/globals.js' -const createMockElementArray = (length: number): WebdriverIO.ElementArray => { - const array = Array.from({ length }, () => ({})) - const mockArray = { - selector: 'parent', - get length() { return array.length }, - set length(newLength: number) { array.length = newLength }, - parent: { - $: vi.fn(), - $$: vi.fn().mockReturnValue(array), - }, - foundWith: '$$', - props: [], - [Symbol.iterator]: array[Symbol.iterator].bind(array), - filter: vi.fn().mockReturnThis(), - map: vi.fn().mockReturnThis(), - find: vi.fn().mockReturnThis(), - forEach: vi.fn(), - some: vi.fn(), - every: vi.fn(), - slice: vi.fn().mockReturnThis(), - toArray: vi.fn().mockReturnThis(), - } - return Object.assign(array, mockArray) as unknown as WebdriverIO.ElementArray -} - -vi.mock('@wdio/globals', () => ({ - $$: vi.fn().mockImplementation(() => createMockElementArray(5)) -})) - -describe('refetchElements', () => { +vi.mock('@wdio/globals') + +describe(refetchElements, () => { describe('given WebdriverIO.ElementArray type', () => { let elements: WebdriverIO.ElementArray beforeEach(async () => { - elements = (await $$('parent')) as unknown as WebdriverIO.ElementArray - // @ts-ignore - elements.parent._length = 5 + elements = await $$('elements').getElements() + + // Have a different browser instance and $$ implementation to be able to assert calls + elements.parent = browserFactory() + elements.parent.$$ = vi.fn().mockResolvedValue( + chainableElementArrayFactory('elements', 5) as unknown as ChainablePromiseArray & WebdriverIO.MultiRemoteElement[] + ) }) - test('default', async () => { + test('default should refresh once', async () => { + const actual = await refetchElements(elements, 5, true) + expect(actual.length).toBe(5) + expect(actual).not.toBe(elements) + expect(elements.parent.$$).toHaveBeenCalledTimes(1) }) - test('wait is 0', async () => { - const actual = await refetchElements(elements, 0, true) + test('wait is 0 should not refresh', async () => { + const wait = 0 + + const actual = await refetchElements(elements, wait, true) + expect(actual).toEqual(elements) + expect(actual).toHaveLength(2) + expect(elements.parent.$$).not.toHaveBeenCalled() }) test('should call $$ with all props', async () => { elements.props = ['prop1', 'prop2'] + await refetchElements(elements, 5, true) - expect(elements.parent.$$).toHaveBeenCalledWith('parent', 'prop1', 'prop2') + + expect(elements.parent.$$).toHaveBeenCalledWith('elements', 'prop1', 'prop2') }) test('should call $$ with the proper parent this context', async () => { @@ -68,11 +57,17 @@ describe('refetchElements', () => { }) describe('given WebdriverIO.Element[] type', () => { - const elements: WebdriverIO.Element[] = [] as unknown as WebdriverIO.Element[] + let elements: WebdriverIO.Element[] + + beforeEach(() => { + elements = [elementFactory('element1'), elementFactory('element2')] + }) - test('default', async () => { - const actual = await refetchElements(elements, 0) - expect(actual).toEqual([]) + test('default should not refresh', async () => { + const actual = await refetchElements(elements, 5, true) + + expect(actual).toEqual(elements) + expect(actual).toHaveLength(2) }) }) }) diff --git a/test/util/stringUtil.test.ts b/test/util/stringUtil.test.ts new file mode 100644 index 000000000..2d35c010e --- /dev/null +++ b/test/util/stringUtil.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from 'vitest' +import { isString, toJsonString } from '../../src/util/stringUtil' + +describe('stringUtil', () => { + describe(isString, () => { + test('should return true for a string', () => { + expect(isString('hello')).toBe(true) + expect(isString('')).toBe(true) + }) + + test('should return false for non-string values', () => { + expect(isString(123)).toBe(false) + expect(isString(true)).toBe(false) + expect(isString({})).toBe(false) + expect(isString(null)).toBe(false) + expect(isString(undefined)).toBe(false) + expect(isString([])).toBe(false) + }) + }) + + describe(toJsonString, () => { + test('should return the string as is if input is a string', () => { + expect(toJsonString('hello')).toBe('hello') + }) + + test('should return JSON string if input is a serializable object', () => { + const obj = { foo: 'bar', num: 123 } + expect(toJsonString(obj)).toBe(JSON.stringify(obj)) + }) + + test('should return string representation if JSON.stringify throws', () => { + const circular: any = { foo: 'bar' } + circular.self = circular + + expect(toJsonString(circular)).toBe('[object Object]') + expect(toJsonString(BigInt(9007199254740991))).toBe('9007199254740991') + }) + + test('should return string representation for other types', () => { + expect(toJsonString(123)).toBe('123') + expect(toJsonString(true)).toBe('true') + expect(toJsonString(null)).toBe('null') + }) + + test('should handle undefined correctly', () => { + expect(toJsonString(undefined)).toBeUndefined() + }) + }) +}) diff --git a/test/util/waitUntil.test.ts b/test/util/waitUntil.test.ts new file mode 100644 index 000000000..adf38bd86 --- /dev/null +++ b/test/util/waitUntil.test.ts @@ -0,0 +1,774 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest' +import type { ConditionResult } from '../../src/util/waitUntil' +import { waitUntil } from '../../src/util/waitUntil' + +vi.mock('../../src/constants.js', async () => ({ + DEFAULT_OPTIONS: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('../../src/constants.js')).DEFAULT_OPTIONS, + // speed up tests by lowering default wait timeout + wait : 50, + interval: 10 + } +})) + +describe(waitUntil, () => { + + describe('when condition returns single boolean', () => { + + describe('given we should wait for the condition to be met (modifier `.not` is not used)', () => { + const isNot = undefined + + describe('should be pass=true for normal success', () => { + let successCondition: () => Promise + + beforeEach(() => { + successCondition = vi.fn().mockResolvedValue(true) + }) + + test('should return true when condition is met', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 1000, interval: 100 }) + + expect(result).toBe(true) + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return true with wait 0', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return true when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + + test('should return true when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(new Error('Test error')) + .mockResolvedValueOnce(true) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=false for normal failure', () => { + + let failureCondition: () => Promise + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(false) + }) + + test('should return false when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return false when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(1) + }) + + test('should return false if condition throws but still return false', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(false) + + const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(condition).toBeCalledTimes(4) + }) + + test('should return false with default options', async () => { + const result = await waitUntil(failureCondition, undefined, {}) + + expect(result).toBe(false) + expect(failureCondition).toHaveBeenCalled() + }) + }) + + describe('when condition throws', () => { + const error = new Error('failing') + + test('should throw with wait', async () => { + const condition = vi.fn().mockRejectedValue(error) + + await expect(() => waitUntil(condition, isNot, { wait: 180, interval: 50 })).rejects.toThrowError('failing') + expect(condition).toBeCalledTimes(4) + }) + + test('should throw with wait 0', async () => { + const condition = vi.fn().mockRejectedValue(error) + + await expect(() => waitUntil(condition, isNot, { wait: 0 })).rejects.toThrowError('failing') + expect(condition).toBeCalledTimes(1) + }) + }) + }) + + describe('given we should wait for the reverse condition to be met since element state can take time to update (modifier `.not` is true to for reverse condition)', () => { + const isNot = true + describe('should be pass=false for normal success (inverted later by jest expect library)', () => { + let successCondition: () => Promise + + beforeEach(() => { + successCondition = vi.fn().mockResolvedValue(false) + }) + + test('should return success (false) when condition is met', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 1000, interval: 100 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return success (false) with wait 0', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return success (false) when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + + test('should return success (false) when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(new Error('Test error')) + .mockResolvedValueOnce(false) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=true for normal failure', () => { + let failureCondition: () => Promise + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(true) + }) + + test('should return failure (true) when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(true) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return true when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) + }) + + test('should return true if condition throws but still return true', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(true) + + const result = await waitUntil(condition, isNot, { wait: 190, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(4) + }) + }) + + describe('when condition throws', () => { + const error = new Error('failing') + + test('should throw with wait', async () => { + const condition = vi.fn().mockRejectedValue(error) + + await expect(() => waitUntil(condition, isNot, { wait: 200, interval: 50 })).rejects.toThrowError('failing') + }) + + test('should throw with wait 0', async () => { + const condition = vi.fn().mockRejectedValue(error) + + await expect(() => waitUntil(condition, isNot, { wait: 0 })).rejects.toThrowError('failing') + + }) + }) + }) + }) + + describe('when condition returns single ConditionResult', () => { + + describe('given we should wait for the condition to be met (modifier `.not` is not used)', () => { + const isNot = undefined + + describe('should be pass=true for normal success', () => { + let successCondition: () => Promise + const successResult: ConditionResult = { success: true, results: [true] } + const failureResult: ConditionResult = { success: false, results: [false] } + + beforeEach(() => { + successCondition = vi.fn().mockResolvedValue(successResult) + }) + + test('should return true when condition is met', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 1000, interval: 100 }) + + expect(result).toBe(true) + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return true with wait 0', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return true when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + + test('should return true when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(new Error('Test error')) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=false for normal failure', () => { + let failureCondition: () => Promise + const failureResult: ConditionResult = { success: false, results: [false] } + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(failureResult) + }) + + test('should return false when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return false when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(1) + }) + + test('should return false if condition throws but still return false', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(failureResult) + + const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(condition).toBeCalledTimes(4) + }) + + test('should return false with default options', async () => { + const result = await waitUntil(failureCondition, undefined, {}) + + expect(result).toBe(false) + expect(failureCondition).toHaveBeenCalled() + }) + }) + }) + + describe('given we should wait for the reverse condition to be met since element state can take time to update (modifier `.not` is true to for reverse condition)', () => { + const isNot = true + describe('should be pass=false for normal success (inverted later by jest expect library)', () => { + let successCondition: () => Promise + const successResult: ConditionResult = { success: false, results: [false] } + const failureResult: ConditionResult = { success: true, results: [true] } + + beforeEach(() => { + successCondition = vi.fn().mockResolvedValue(successResult) + }) + + test('should return success (false) when condition is met', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 1000, interval: 100 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return success (false) with wait 0', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return success (false) when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + + test('should return success (false) when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(new Error('Test error')) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=true for normal failure', () => { + let failureCondition: () => Promise + const failureResult: ConditionResult = { success: true, results: [true] } + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(failureResult) + }) + + test('should return failure (true) when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(true) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return true when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) + expect(failureCondition).toBeCalledTimes(1) + }) + + test('should return true if condition throws but still return true', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(failureResult) + + const result = await waitUntil(condition, isNot, { wait: 190, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(4) + }) + }) + }) + }) + + describe('when condition returns multiple ConditionResult', () => { + describe('when ConditionResult are all the same', () => { + describe('given we should wait for the condition to be met (modifier `.not` is not used)', () => { + const successResult: ConditionResult = { success: true, results: [true, true] } + const failureResult: ConditionResult = { success: false, results: [false, false] } + + const isNot = undefined + + describe('should be pass=true for normal success', () => { + let successCondition: () => Promise + + beforeEach(() => { + successCondition = vi.fn().mockResolvedValue(successResult) + }) + + test('should return true when condition is met', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 1000, interval: 100 }) + + expect(result).toBe(true) + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return true with wait 0', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return true when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + + test('should return true when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(new Error('Test error')) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=false for normal failure', () => { + let failureCondition: () => Promise + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(failureResult) + }) + + test('should return false when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return false when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(1) + }) + + test('should return false if condition throws but still return false', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(failureResult) + + const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(condition).toBeCalledTimes(4) + }) + + test('should return false with default options', async () => { + const result = await waitUntil(failureCondition, undefined, {}) + + expect(result).toBe(false) + expect(failureCondition).toHaveBeenCalled() + }) + }) + }) + + describe('given we should wait for the reverse condition to be met since element state can take time to update (modifier `.not` is true to for reverse condition)', () => { + const isNot = true + describe('should be pass=false for normal success (inverted later by jest expect library)', () => { + let successCondition: () => Promise + const successResult: ConditionResult = { success: false, results: [false, false] } + const failureResult: ConditionResult = { success: true, results: [true, true] } + + beforeEach(() => { + successCondition = vi.fn().mockResolvedValue(successResult) + }) + + test('should return success (false) when condition is met', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 1000, interval: 100 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return success (false) with wait 0', async () => { + const result = await waitUntil(successCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(successCondition).toBeCalledTimes(1) + }) + + test('should return success (false) when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(failureResult) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + + test('should return success (false) when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(new Error('Test error')) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=true for normal failure', () => { + let failureCondition: () => Promise + const failureResult: ConditionResult = { success: true, results: [true] } + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(failureResult) + }) + + test('should return failure (true) when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(true) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return true when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) + }) + + test('should return true if condition throws but still return true', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(failureResult) + + const result = await waitUntil(condition, isNot, { wait: 190, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(4) + }) + }) + }) + }) + + describe('when ConditionResult are not always the same', () => { + + describe('given we should wait for the condition to be met (modifier `.not` is not used)', () => { + const isNot = undefined + + const failureResult1: ConditionResult = { success: false, results: [true, false] } + const failureResult2: ConditionResult = { success: false, results: [false, true] } + + describe('should be pass=true for normal success', () => { + const successResult: ConditionResult = { success: true, results: [true, true] } + + test('should return true when condition is met within wait time', async () => { + const condition = vi.fn() + .mockResolvedValueOnce(failureResult1) + .mockResolvedValueOnce(failureResult2) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + + test('should return true when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(failureResult1) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=false for normal failure', () => { + let failureCondition: () => Promise + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(failureResult1) + }) + + test('should return false when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return false when condition is not met and wait is 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(false) + expect(failureCondition).toBeCalledTimes(1) + }) + + test('should return false if condition throws but still return false', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(failureResult2) + + const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(condition).toBeCalledTimes(4) + }) + + test('should return false with default options', async () => { + const result = await waitUntil(failureCondition, undefined, {}) + + expect(result).toBe(false) + expect(failureCondition).toHaveBeenCalled() + }) + }) + }) + + describe('given we should wait for the reverse condition to be met since element state can take time to update (modifier `.not` is true to for reverse condition)', () => { + const isNot = true + + const failureResult1: ConditionResult = { success: false, results: [true, false] } + const failureResult2: ConditionResult = { success: false, results: [false, true] } + + describe('should be pass=false for normal success (inverted later by jest expect library)', () => { + let condition: () => Promise + const successResult: ConditionResult = { success: false, results: [false, false] } + + beforeEach(() => { + condition = vi.fn() + .mockResolvedValueOnce(failureResult1) + .mockResolvedValueOnce(failureResult2) + .mockResolvedValueOnce(successResult) + }) + + test('should return success (false) when condition is met within wait time', async () => { + + const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + + test('should return success (false) when condition errors but still is met within wait time', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Test error')) + .mockRejectedValueOnce(failureResult1) + .mockResolvedValueOnce(successResult) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + }) + + describe('should be pass=true for normal failure', () => { + let failureCondition: () => Promise + + beforeEach(() => { + failureCondition = vi.fn().mockResolvedValue(failureResult1) + }) + + test('should return failure (true) when condition is not met within wait time', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(true) + expect(failureCondition).toBeCalledTimes(4) + }) + + test('should return fail (true) with wait 0', async () => { + const result = await waitUntil(failureCondition, isNot, { wait: 0 }) + + expect(result).toBe(true) // failure for .not, boolean is inverted later by jest's expect library + expect(failureCondition).toBeCalledTimes(1) + }) + + test('should return true if condition throws but still return failure', async () => { + const condition = vi.fn() + .mockRejectedValueOnce(new Error('Always failing')) + .mockRejectedValueOnce(new Error('Always failing')) + .mockResolvedValue(failureResult2) + + const result = await waitUntil(condition, isNot, { wait: 190, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(4) + }) + }) + }) + }) + }) + + describe('when not results aka no elements found cases we DO NOT RETRY', () => { + const emptyResult: ConditionResult = { success: false, results: [] } + + let emptyCondition: () => Promise + + beforeEach(() => { + emptyCondition = vi.fn().mockResolvedValue(emptyResult) + }) + + test('should NOT RETRY and fails with pass=false when isNot is undefined', async () => { + const isNot = undefined + + const result = await waitUntil(emptyCondition, isNot, { wait: 280, interval: 100 }) + + expect(result).toBe(false) + expect(emptyCondition).toBeCalledTimes(1) + }) + + test('should NOT RETRY and fails with pass=true when isNot is true', async () => { + const isNot = true + + const result = await waitUntil(emptyCondition, isNot, { wait: 280, interval: 100 }) + + expect(result).toBe(true) // failure for .not, boolean is inverted later by jest's expect library + expect(emptyCondition).toBeCalledTimes(1) + }) + }) +}) diff --git a/test/utils.test.ts b/test/utils.test.ts index 377628621..0489ce9be 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,8 +1,48 @@ -import { describe, test, expect, vi } from 'vitest' -import { compareNumbers, compareObject, compareText, compareTextWithArray, waitUntil } from '../src/utils' +import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' +import { compareNumbers, compareObject, compareText, compareTextWithArray, executeCommandBe, waitUntil } from '../src/utils' +import { enhanceErrorBe } from '../src/util/formatMessage' +import type { CommandOptions } from 'expect-webdriverio' +import { executeCommand } from '../src/util/executeCommand' +import { $, $$ } from '@wdio/globals' + +vi.mock('@wdio/globals') + +vi.mock('../src/util/executeCommand', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + executeCommand: vi.spyOn(actual, 'executeCommand'), + } +}) +vi.mock('../src/util/formatMessage', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + enhanceErrorBe: vi.spyOn(actual, 'enhanceErrorBe'), + } +}) +vi.mock('../src/util/elementsUtil.js', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + awaitElementOrArray: vi.spyOn(actual, 'awaitElementOrArray'), + map: vi.spyOn(actual, 'map'), + } +}) +vi.mock('../src/util/waitUntil.js', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal() + return { + ...actual, + waitUntil: vi.spyOn(actual, 'waitUntil'), + } +}) describe('utils', () => { - describe('compareText', () => { + describe(compareText, () => { test('should pass when strings match', () => { expect(compareText('foo', 'foo', {}).result).toBe(true) }) @@ -42,7 +82,7 @@ describe('utils', () => { }) }) - describe('compareTextWithArray', () => { + describe(compareTextWithArray, () => { test('should pass if strings match in array', () => { expect(compareTextWithArray('foo', ['foo', 'bar'], {}).result).toBe(true) }) @@ -95,7 +135,7 @@ describe('utils', () => { }) }) - describe('compareNumbers', () => { + describe(compareNumbers, () => { test('should work when equal', () => { const actual = 10 const eq = 10 @@ -136,7 +176,7 @@ describe('utils', () => { }) }) - describe('compareObject', () => { + describe(compareObject, () => { test('should pass if the objects are equal', () => { expect(compareObject({ 'foo': 'bar' }, { 'foo': 'bar' }).result).toBe(true) }) @@ -159,188 +199,316 @@ describe('utils', () => { }) }) - describe(waitUntil, () => { - - describe('given we should wait for the condition to be met (modifier `.not` is not used)', () => { - const isNot = undefined - describe('should be pass=true for normal success', () => { - test('should return true when condition is met', async () => { - const condition = vi.fn().mockResolvedValue(true) - - const result = await waitUntil(condition, isNot, { wait: 1000, interval: 100 }) + describe(executeCommandBe, () => { + let context: { isNot: boolean; expectation: string; verb: string } + let command: (el: WebdriverIO.Element) => Promise + let options: CommandOptions + + beforeEach(() => { + context = { + isNot: false, + expectation: 'displayed', + verb: 'be' + } + command = vi.fn().mockResolvedValue(true) + options = { wait: 0, interval: 1 } + }) - expect(result).toBe(true) - }) + afterEach(() => { + vi.clearAllMocks() + }) - test('should return true with wait 0', async () => { - const condition = vi.fn().mockResolvedValue(true) + describe('given no elements', () => { + test('should fail given undefined', async () => { + const result = await executeCommandBe.call(context, undefined as any, command, options) - const result = await waitUntil(condition, isNot, { wait: 0 }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect undefined to be displayed - expect(result).toBe(true) - }) +Expected: "displayed" +Received: "not displayed"`) + expect(waitUntil).toHaveBeenCalled() + }) - test('should return true when condition is met within wait time', async () => { - const condition = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValueOnce(true) + test('should fail given empty array', async () => { + const result = await executeCommandBe.call(context, [], command, options) - const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect [] to be displayed - expect(result).toBe(true) - expect(condition).toBeCalledTimes(3) - }) +Expected: "at least one result" +Received: []`) + expect(waitUntil).toHaveBeenCalled() + }) + }) - test('should return true when condition errors but still is met within wait time', async () => { - const condition = vi.fn().mockRejectedValueOnce(new Error('Test error')).mockRejectedValueOnce(new Error('Test error')).mockResolvedValueOnce(true) + describe('given single element', () => { + let received: ChainablePromiseElement - const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + beforeEach(() => { + received = $('element1') + }) - expect(result).toBe(true) - expect(condition).toBeCalledTimes(3) - }) + test('should pass given ChainableElement', async () => { + const result = await executeCommandBe.call(context, received, command, options) - test('should use default options when not provided', async () => { - const condition = vi.fn().mockResolvedValue(true) + expect(result.pass).toBe(true) + expect(executeCommand).toHaveBeenCalledWith(received, expect.any(Function)) + expect(waitUntil).toHaveBeenCalledWith(expect.any(Function), false, options) + }) - const result = await waitUntil(condition) + test('should pass given WebdriverIO.Element', async () => { + const result = await executeCommandBe.call(context, received, command, options) - expect(result).toBe(true) - }) + expect(result.pass).toBe(true) + expect(executeCommand).toHaveBeenCalledWith(received, expect.any(Function)) }) - describe('should be pass=false for normal failure', () => { + test('should fail if command returns false', async () => { + vi.mocked(command).mockResolvedValue(false) - test('should return false when condition is not met within wait time', async () => { - const condition = vi.fn().mockResolvedValue(false) + const result = await executeCommandBe.call(context, received, command, options) - const result = await waitUntil(condition, isNot, { wait: 200, interval: 50 }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect $(\`element1\`) to be displayed - expect(result).toBe(false) - }) +Expected: "displayed" +Received: "not displayed"`) + expect(enhanceErrorBe).toHaveBeenCalledWith( + await received, + [false], + expect.objectContaining({ isNot: false }), + options + ) + }) - test('should return false when condition is not met and wait is 0', async () => { - const condition = vi.fn().mockResolvedValue(false) + describe('given isNot is true', () => { + let negatedContext: { isNot: boolean; expectation: string; verb: string } + + beforeEach(() => { + // Success for `.not` + vi.mocked(command).mockResolvedValue(false) + negatedContext = { + expectation: 'displayed', + verb: 'be', + isNot: true + } + }) - const result = await waitUntil(condition, isNot, { wait: 0 }) + test('should succeed so pass=false since it is inverted later', async () => { + const result = await executeCommandBe.call(negatedContext, received, command, options) + + expect(result.pass).toBe(false) + expect(enhanceErrorBe).toHaveBeenCalledWith( + await received, + [false], + { + expectation: 'displayed', + isNot: true, + verb: 'be', + }, + options + ) + expect(waitUntil).toHaveBeenCalledWith(expect.any(Function), true, options) + }) - expect(result).toBe(false) + test('should failed so pass=true since it is inverted later', async () => { + vi.mocked(command).mockResolvedValue(true) + const result = await executeCommandBe.call(negatedContext, received, command, options) + + expect(result.pass).toBe(true) + expect(result.message()).toEqual(`\ +Expect $(\`element1\`) not to be displayed + +Expected: "not displayed" +Received: "displayed"`) + expect(enhanceErrorBe).toHaveBeenCalledWith( + await received, + [true], + { + expectation: 'displayed', + isNot: true, + verb: 'be', + }, + options + ) + expect(waitUntil).toHaveBeenCalledWith(expect.any(Function), true, options) }) + }) + }) - test('should return false if condition throws but still return false', async () => { - const condition = vi.fn().mockRejectedValueOnce(new Error('Always failing')).mockRejectedValueOnce(new Error('Always failing')).mockResolvedValue(false) + describe('given multiple elements', () => { + const elements = $$('elements') + const selectorName = '$$(`elements`)' - const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + test('should pass given ChainableArray', async () => { + const result = await executeCommandBe.call(context, elements, command, options) - expect(result).toBe(false) - expect(condition).toBeCalledTimes(4) - }) + expect(result.pass).toBe(true) + expect(executeCommand).toHaveBeenCalledWith(elements, expect.any(Function)) + expect(command).toHaveBeenCalledTimes(2) + expect(waitUntil).toHaveBeenCalledWith(expect.any(Function), false, options) }) - describe('when condition throws', () => { - const error = new Error('failing') + test('should pass given ElementArray', async () => { + const elementArray: WebdriverIO.ElementArray = await elements.getElements() - test('should throw with wait', async () => { - const condition = vi.fn().mockRejectedValue(error) + const result = await executeCommandBe.call(context, elementArray, command, options) - await expect(() => waitUntil(condition, isNot, { wait: 200, interval: 50 })).rejects.toThrowError('failing') - }) + expect(result.pass).toBe(true) + expect(executeCommand).toHaveBeenCalledWith(elementArray, expect.any(Function)) + expect(command).toHaveBeenCalledTimes(2) + }) - test('should throw with wait 0', async () => { - const condition = vi.fn().mockRejectedValue(error) + test('should pass given Element[]', async () => { + const elementArray: WebdriverIO.Element[] = await (await elements.getElements()).filter(el => el.isDisplayed()) - await expect(() => waitUntil(condition, isNot, { wait: 0 })).rejects.toThrowError('failing') + const result = await executeCommandBe.call(context, elementArray, command, options) - }) + expect(result.pass).toBe(true) + expect(executeCommand).toHaveBeenCalledWith(elementArray, expect.any(Function)) + expect(command).toHaveBeenCalledTimes(2) }) - }) - describe('given we should wait for the reverse condition to meet since element state can take time to update (modifier `.not` is true to for reverse condition)', () => { - const isNot = true - describe('should be pass=false for normal success', () => { - test('should return false when condition is met', async () => { - const condition = vi.fn().mockResolvedValue(false) + test('should fail when first element fails', async () => { + vi.mocked(command).mockResolvedValueOnce(false).mockResolvedValueOnce(true) - const result = await waitUntil(condition, isNot, { wait: 1000, interval: 100 }) + const result = await executeCommandBe.call(context, elements, command, options) - expect(result).toBe(false) - }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be displayed - test('should return false with wait 0', async () => { - const condition = vi.fn().mockResolvedValue(false) +- Expected - 1 ++ Received + 1 - const result = await waitUntil(condition, isNot, { wait: 0 }) + Array [ +- "displayed", ++ "not displayed", + "displayed", + ]`) + }) - expect(result).toBe(false) - }) + test('should fail when last element fails', async () => { + vi.mocked(command).mockResolvedValueOnce(true).mockResolvedValueOnce(false) - test('should return false when condition is met within wait time', async () => { - const condition = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(true).mockResolvedValueOnce(false) + const result = await executeCommandBe.call(context, elements, command, options) - const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be displayed - expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library - expect(condition).toBeCalledTimes(3) - }) +- Expected - 1 ++ Received + 1 + + Array [ + "displayed", +- "displayed", ++ "not displayed", + ]`) + }) - test('should return false when condition errors but still is met within wait time', async () => { - const condition = vi.fn().mockRejectedValueOnce(new Error('Test error')).mockRejectedValueOnce(new Error('Test error')).mockResolvedValueOnce(false) + test('should fail when all elements fail', async () => { + vi.mocked(command).mockResolvedValue(false) - const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + const result = await executeCommandBe.call(context, elements, command, options) - expect(result).toBe(false) - expect(condition).toBeCalledTimes(3) - }) + expect(result.pass).toBe(false) + expect(result.message()).toEqual(`\ +Expect ${selectorName} to be displayed - test('should use default options when not provided', async () => { - const condition = vi.fn().mockResolvedValue(false) +- Expected - 2 ++ Received + 2 - const result = await waitUntil(condition, isNot) + Array [ +- "displayed", +- "displayed", ++ "not displayed", ++ "not displayed", + ]`) + }) - expect(result).toBe(false) + describe('given isNot is true', () => { + let negatedContext: { isNot: boolean; expectation: string; verb: string } + + beforeEach(() => { + // Success for `.not` + vi.mocked(command).mockResolvedValue(false) + negatedContext = { + expectation: 'displayed', + verb: 'be', + isNot: true + } }) - }) - describe('should be pass=true for normal failure', () => { + test('should succeed so pass=false since it is inverted later', async () => { + const result = await executeCommandBe.call(negatedContext, elements, command, options) - test('should return true when condition is not met within wait time', async () => { - const condition = vi.fn().mockResolvedValue(true) + expect(result.pass).toBe(false) + expect(executeCommand).toHaveBeenCalledWith(elements, expect.any(Function)) + expect(command).toHaveBeenCalledTimes(2) + expect(waitUntil).toHaveBeenCalledWith(expect.any(Function), true, options) + }) - const result = await waitUntil(condition, isNot, { wait: 200, interval: 50 }) + test('should fail (so pass=true since it is inverted later) when first element fails', async () => { + vi.mocked(command).mockResolvedValueOnce(true).mockResolvedValueOnce(false) - expect(result).toBe(true) - }) + const result = await executeCommandBe.call(negatedContext, elements, command, options) - test('should return true when condition is not met and wait is 0', async () => { - const condition = vi.fn().mockResolvedValue(true) + expect(result.pass).toBe(true) + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be displayed - const result = await waitUntil(condition, isNot, { wait: 0 }) +- Expected - 1 ++ Received + 1 - expect(result).toBe(true) + Array [ +- "not displayed", ++ "displayed", + "not displayed", + ]`) }) - test('should return true if condition throws but still return true', async () => { - const condition = vi.fn().mockRejectedValueOnce(new Error('Always failing')).mockRejectedValueOnce(new Error('Always failing')).mockResolvedValue(true) + test('should fail (so pass=true since it is inverted later) when last element fails', async () => { + vi.mocked(command).mockResolvedValueOnce(false).mockResolvedValueOnce(true) - const result = await waitUntil(condition, isNot, { wait: 190, interval: 50 }) + const result = await executeCommandBe.call(negatedContext, elements, command, options) - expect(result).toBe(true) - expect(condition).toBeCalledTimes(4) - }) - }) - - describe('when condition throws', () => { - const error = new Error('failing') + expect(result.pass).toBe(true) + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be displayed - test('should throw with wait', async () => { - const condition = vi.fn().mockRejectedValue(error) +- Expected - 1 ++ Received + 1 - await expect(() => waitUntil(condition, isNot, { wait: 200, interval: 50 })).rejects.toThrowError('failing') + Array [ + "not displayed", +- "not displayed", ++ "displayed", + ]`) }) - test('should throw with wait 0', async () => { - const condition = vi.fn().mockRejectedValue(error) + test('should fail (so pass=true since it is inverted later) when all elements fail', async () => { + vi.mocked(command).mockResolvedValue(true) + + const result = await executeCommandBe.call(negatedContext, elements, command, options) + + expect(result.pass).toBe(true) + expect(result.message()).toEqual(`\ +Expect ${selectorName} not to be displayed - await expect(() => waitUntil(condition, isNot, { wait: 0 })).rejects.toThrowError('failing') +- Expected - 2 ++ Received + 2 + Array [ +- "not displayed", +- "not displayed", ++ "displayed", ++ "displayed", + ]`) }) }) }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 9b729ff34..5d65fd056 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -27,6 +27,8 @@ type RawMatcherFn): ExpectLibExpectationResult; } +type MaybeArray = T | T[] + /** * Real Promise and wdio chainable promise types. */ @@ -139,11 +141,47 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { */ toBeExisting: FnWhenElementOrArrayLike Promise> + /** + * `WebdriverIO.Element` -> `isClickable` + */ + toBeClickable: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `!isEnabled` + */ + toBeDisabled: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isDisplayedInViewport` + */ + toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isEnabled` + */ + toBeEnabled: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isFocused` + */ + toBeFocused: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isSelected` + */ + toBeSelected: FnWhenElementOrArrayLike Promise> + + /** + * `WebdriverIO.Element` -> `isSelected` + */ + toBeChecked: FnWhenElementOrArrayLike Promise> + /** * `WebdriverIO.Element` -> `getAttribute` */ toHaveAttribute: FnWhenElementOrArrayLike, + attribute: string, + value?: MaybeArray>, options?: ExpectWebdriverIO.StringOptions) => Promise> @@ -151,16 +189,8 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * `WebdriverIO.Element` -> `getAttribute` */ toHaveAttr: FnWhenElementOrArrayLike, - options?: ExpectWebdriverIO.StringOptions - ) => Promise> - - /** - * `WebdriverIO.Element` -> `getAttribute` class - * @deprecated since v1.3.1 - use `toHaveElementClass` instead. - */ - toHaveClass: FnWhenElementOrArrayLike, + attribute: string, + value?: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -181,79 +211,42 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * ``` */ toHaveElementClass: FnWhenElementOrArrayLike | ExpectWebdriverIO.PartialMatcher, + className: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` */ - toHaveElementProperty: FnWhenElementOrArrayLike< - ActualT, - ( - property: string, - value?: string | RegExp | WdioAsymmetricMatcher | null, - options?: ExpectWebdriverIO.StringOptions, - ) => Promise - > + toHaveElementProperty: FnWhenElementOrArrayLike>, + options?: ExpectWebdriverIO.StringOptions + ) => Promise> /** * `WebdriverIO.Element` -> `getProperty` value */ toHaveValue: FnWhenElementOrArrayLike, + value: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> - /** - * `WebdriverIO.Element` -> `isClickable` - */ - toBeClickable: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `!isEnabled` - */ - toBeDisabled: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `isDisplayedInViewport` - */ - toBeDisplayedInViewport: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `isEnabled` - */ - toBeEnabled: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `isFocused` - */ - toBeFocused: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `isSelected` - */ - toBeSelected: FnWhenElementOrArrayLike Promise> - - /** - * `WebdriverIO.Element` -> `isSelected` - */ - toBeChecked: FnWhenElementOrArrayLike Promise> - /** * `WebdriverIO.Element` -> `$$('./*').length` * supports less / greater then or equals to be passed in options */ toHaveChildren: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.CommandOptions ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute` href */ toHaveHref: FnWhenElementOrArrayLike, + href: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -261,7 +254,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * `WebdriverIO.Element` -> `getAttribute` href */ toHaveLink: FnWhenElementOrArrayLike, + href: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -269,7 +262,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * `WebdriverIO.Element` -> `getProperty` value */ toHaveId: FnWhenElementOrArrayLike, + id: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -277,7 +270,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * `WebdriverIO.Element` -> `getSize` value */ toHaveSize: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -301,7 +294,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * ``` */ toHaveText: FnWhenElementOrArrayLike | Array>, + text: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -310,7 +303,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * Element's html equals the html provided */ toHaveHTML: FnWhenElementOrArrayLike | Array, + html: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -319,7 +312,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * Element's computed label equals the computed label provided */ toHaveComputedLabel: FnWhenElementOrArrayLike | Array, + computedLabel: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -328,7 +321,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * Element's computed role equals the computed role provided */ toHaveComputedRole: FnWhenElementOrArrayLike | Array, + computedRole: MaybeArray>, options?: ExpectWebdriverIO.StringOptions ) => Promise> @@ -336,7 +329,9 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * `WebdriverIO.Element` -> `getSize('width')` * Element's width equals the width provided */ - toHaveWidth: FnWhenElementOrArrayLike Promise> + toHaveWidth: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.CommandOptions) => Promise> /** * `WebdriverIO.Element` -> `getSize('height')` or `getSize()` @@ -352,14 +347,16 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { * ``` */ toHaveHeight: FnWhenElementOrArrayLike, options?: ExpectWebdriverIO.CommandOptions ) => Promise> /** * `WebdriverIO.Element` -> `getAttribute("style")` */ - toHaveStyle: FnWhenElementOrArrayLike Promise> + toHaveStyle: FnWhenElementOrArrayLike, + options?: ExpectWebdriverIO.StringOptions) => Promise> } /** @@ -689,11 +686,16 @@ declare namespace ExpectWebdriverIO { asString?: boolean } + // Number options is the only options that also serves as a expected value container + // This can caused problems with multiple expected values vs global command options + // Potentially we should have this object as a NumberExpect type and have the options separate interface NumberOptions extends CommandOptions { + /** * equals */ eq?: number + /** * less than or equals */ diff --git a/vitest.config.ts b/vitest.config.ts index 7b0bc2e75..b9ee956d2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,6 +8,8 @@ export default defineConfig({ '**/node_modules/**' ], testTimeout: 15 * 1000, + clearMocks: true, // clears all mock call histories before each test + restoreMocks: true, // restores the original implementation of spies coverage: { enabled: true, exclude: [ @@ -26,10 +28,14 @@ export default defineConfig({ 'types-checks-filter-out-node_modules.js', ], thresholds: { - lines: 88, - functions: 86, - statements: 88, - branches: 79, + lines: 88.7, + functions: 87.1, + statements: 88.7, + branches: 84.1, + // lines: 100, + // functions: 100, + // statements: 100, + // branches: 100, } } }