diff --git a/CHANGELOG.md b/CHANGELOG.md index 14615eae..fd2fa24b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +2.1.0 / 2019-05-23 +================== + +* Support for urls without protocol +* Get cookies from different domain +* AddClass, removeClass and setAttribute methods +* Delete cookie support for Puppeteer's interface +* Added incognito attribute to browser +* assert.textContains and not.textContains support for an array as expected texts +* Browser.selector support for DOMElement and XPath +* WaitForText support for simple quotes +* Minor delay added to waitUntilCalled method in request mocks +* Minor improvements in assertion error handling + 2.0.2 / 2019-05-21 ================== diff --git a/README.md b/README.md index 6c3fb736..ddfcaf5d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,18 @@ await browser.waitFor("#my-modal"); await browser.assert.text("#my-modal", "Button Clicked"); ``` -**Contents** +## Features + +* Assertion library built-in. +* Cookies, LocalStorage and WebWorkers handling. +* Requests mocker. +* Plugins! +* Full access to Puppeteer methods. +* Easy and flexible query system with support for CSS selectors and XPath. +* Docker and CI friendly. +* Easy to setup, just node required. + +## Contents * [Getting Started](#getting-started) * [Requirements](#requirements) @@ -187,6 +198,9 @@ await browser.page.evaluate(() => { **loaded** True if the page has already opened and loaded. +**incognito** +True if the browser is configured as incognito page. + #### Methods All the methods in Browser return a Promise than can easily be handled by using `async/await`. @@ -199,8 +213,10 @@ await browser.open("http://localhost:8000"); The following options can be passed: -* `viewport`: Viewport config to set when opening the browser, uses the same syntax as `setViewport` -* `queryString`: Querystring to be appended to the url, can be a string or object. Avoid using this parameter if a query string is already present in the url +* `viewport`: Viewport config to set when opening the browser, uses the same syntax as `setViewport`. +* `queryString`: Querystring to be appended to the url, can be a string or object. Avoid using this parameter if a query string is already present in the url. + +If no protocol is defined (e.g. `https://`), `http://` will be used. **openFile(path, options?)** Opens the given file. Same options as `open` can be passed. The file will be passed by appending `file://` to the absolute path. @@ -474,8 +490,6 @@ await browser.select("select.language-select", ["spanish", "english"]); // Retur If the option doesn't have a value, the text should be provided. -> Only Css Selectors supported - **clearValue(selector)** Clears any value that exists in any of the elements matched by the given selector. Setting the value to `""`. @@ -483,6 +497,15 @@ Clears any value that exists in any of the elements matched by the given selecto await browser.clearValue("input.my-input"); ``` +**setAttribute(selector, attributeName, value)** +Sets the attribute of given name to a string value. If null is passed, the attribute will be removed from the element. + +**addClass(selector, className)** +Adds the given css class to the element. + +**removeClass(selector, className)** +Removes the given css class from the element if exists. + **wait(ms=250)** Waits the given milliseconds. @@ -511,8 +534,6 @@ Waits until the given selector is no longer visible or doesn't exists, with the await browser.waitUntilNotVisible(".toast"); ``` -> Css selectors supported only. - **waitForUrl(url, timeout=500)** Waits for the page to have the given url. The url can be a string or a RegExp. @@ -702,7 +723,7 @@ await browser.assert.text("p", "My First Paragraph"); ``` **assert.textContains(selector, expected, msg?)** -Asserts that at least one element matching the given selector contains the expected text. +Asserts that at least one element matching the given selector contains the expected text. Expected text can be an array of strings, in this case **all** expected texts should match at least one element. ```js await browser.assert.textContains("p", "My First"); @@ -869,8 +890,7 @@ await browser.assert.not.text("p", ["This text doesn't exists", "neither do this ``` **assert.not.textContains(selector, expected, msg?)** -Asserts that no elements matching the given selector contain the expected text. -If expected is an array, no text in it should be contained any element with given selector +Asserts that no elements matching the given selector contain the expected text. If expected is an array, no text in it should be contained any element with given selector. ```js await browser.assert.not.textContains("p", "doesn't exist"); @@ -962,8 +982,8 @@ Returns all the cookies in the current page as an object with key being the name const cookies = await browser.cookies.all(); // {username: "arthur_dent", email: "arthur@dent.com"} ``` -**cookies.get(name)** -Returns the cookie object with given name. Returns undefined if the cookie doesn't exists. +**cookies.get(name, url?)** +Returns the cookie object with given name. Returns undefined if the cookie doesn't exists. Cookies from current page will be returned by default. ```js const cookie = await browser.cookies.get("username"); @@ -971,13 +991,32 @@ cookie.name; // "username" cookie.value; // "arthur_dent" ``` +If parameter url is set, cookies from the given url domain will be returned. + **cookies.set(name, value)** -Sets the value of the cookie with given name. If it already exists it will be replaced. The value can be a string (it will only set the cookie value) or a whole cookie object. +Sets the value of the cookie with given name. If it already exists it will be replaced. The value can be a string (it will only set the cookie value) or a [cookie object](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagecookiesurls). ```js await browser.cookies.set("username", "marvin"); + +await browser.cookies.set("another-cookie", { + value: "foo", + secure: false +}) ``` +The possible parameters of the object are: + +* _name_ (required) +* _value_ (required) +* _url_ +* _domain_ +* _path_ +* _expires_ Unix time in seconds. +* _httpOnly_ +* _secure_ +* _sameSite_ Can be Strict or Lax. + **cookies.delete(name)** Deletes the cookie with given name if exists. Optionally an array can be passed and all the cookies will be removed. Won't do anything if the cookie doesn't exists. @@ -986,6 +1025,13 @@ await browser.cookies.delete("username"); await browser.cookies.delete(["username", "email"]); ``` +Optionally, an object with same interface as [Puppeteer](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagedeletecookiecookies) can be passed to delete cookies from different pages. This object can provide the following arguments: + +* _name_ (required) +* _domain_ +* _path_ +* _url_ + **cookies.clear()** Deletes all the cookies of the current page. diff --git a/lib/browser/assertions/assert_elements.ts b/lib/browser/assertions/assert_elements.ts index bf2bda4a..59ddc3b9 100644 --- a/lib/browser/assertions/assert_elements.ts +++ b/lib/browser/assertions/assert_elements.ts @@ -1,4 +1,4 @@ -import { rejectAssertion } from '../../utils/assert_utils'; +import {AssertionError} from '../../errors'; import { isNumber } from '../../utils/utils'; import { WendigoSelector } from '../../types'; @@ -57,7 +57,7 @@ async function equalAssertion(countData: CountConfig, elementsFound: number, msg const expected = Number(countData.equal); if (elementsFound !== expected) { if (!msg) msg = `Expected selector "${selector}" to find exactly ${countData.equal} elements, ${elementsFound} found`; - return rejectAssertion("assert.elements", msg, elementsFound, expected); + throw new AssertionError("assert.elements", msg, elementsFound, expected); } } @@ -65,7 +65,7 @@ async function atLeastAssertion(countData: CountConfig, elementsFound: number, m const expected = Number(countData.atLeast); if (elementsFound < expected) { if (!msg) msg = `Expected selector "${selector}" to find at least ${countData.atLeast} elements, ${elementsFound} found`; - return rejectAssertion("assert.elements", msg); + throw new AssertionError("assert.elements", msg); } } @@ -73,7 +73,7 @@ async function atMostAssertion(countData: CountConfig, elementsFound: number, ms const expected = Number(countData.atMost); if (elementsFound > expected) { if (!msg) msg = `Expected selector "${selector}" to find up to ${countData.atMost} elements, ${elementsFound} found`; - return rejectAssertion("assert.elements", msg); + throw new AssertionError("assert.elements", msg); } } @@ -82,6 +82,6 @@ async function bothAssertion(countData: CountConfig, elementsFound: number, msg: const least = Number(countData.atLeast); if (elementsFound < least || elementsFound > most) { if (!msg) msg = `Expected selector "${selector}" to find between ${countData.atLeast} and ${countData.atMost} elements, ${elementsFound} found`; // eslint-disable-line max-len - return rejectAssertion("assert.elements", msg); + throw new AssertionError("assert.elements", msg); } } diff --git a/lib/browser/assertions/assertions_core.ts b/lib/browser/assertions/assertions_core.ts index 9e50da7e..72e4a715 100644 --- a/lib/browser/assertions/assertions_core.ts +++ b/lib/browser/assertions/assertions_core.ts @@ -2,7 +2,7 @@ import * as utils from '../../utils/utils'; import * as elementsAssertionUtils from './assert_elements'; import * as assertUtils from '../../utils/assert_utils'; -import { QueryError, FatalError, WendigoError } from '../../errors'; +import { QueryError, FatalError, WendigoError, AssertionError } from '../../errors'; import { WendigoSelector } from '../../types'; import BrowserInterface from '../browser_interface'; @@ -21,7 +21,7 @@ export default class AssertionsCore { } catch (err) { throw WendigoError.overrideFnName(err, "assert.exists"); } - if (!element) return assertUtils.rejectAssertion("assert.exists", msg); + if (!element) throw new AssertionError("assert.exists", msg); } public async visible(selector: WendigoSelector, msg?: string): Promise { @@ -36,11 +36,11 @@ export default class AssertionsCore { return false; }, selector); } catch (err) { - return assertUtils.rejectAssertion("assert.visible", `Selector "${selector}" doesn't match any elements.`); + throw new AssertionError("assert.visible", `Selector "${selector}" doesn't match any elements.`); } if (!visible) { if (!msg) msg = `Expected element "${selector}" to be visible.`; - return assertUtils.rejectAssertion("assert.visible", msg); + throw new AssertionError("assert.visible", msg); } } @@ -62,7 +62,7 @@ export default class AssertionsCore { if (!msg) { msg = `No element with tag "${expected}" found.`; } - return assertUtils.rejectAssertion("assert.tag", msg); + throw new AssertionError("assert.tag", msg); } public async text(selector: WendigoSelector, expected: string | RegExp | Array, msg?: string): Promise { @@ -77,21 +77,28 @@ export default class AssertionsCore { const foundText = texts.length === 0 ? "no text" : `"${texts.join(" ")}"`; msg = `Expected element "${selector}" to have text "${expectedText}", ${foundText} found.`; } - return assertUtils.rejectAssertion("assert.text", msg); + throw new AssertionError("assert.text", msg); } } } public async textContains(selector: WendigoSelector, expected: string, msg?: string): Promise { - const texts = await this._browser.text(selector); - for (const text of texts) { - if (text && text.includes(expected)) return Promise.resolve(); + if ((!expected && expected !== "") || (Array.isArray(expected) && expected.length === 0)) { + throw new WendigoError("assert.textContains", `Missing expected text for assertion.`); } - if (!msg) { - const foundText = texts.length === 0 ? "no text" : `"${texts.join(" ")}"`; - msg = `Expected element "${selector}" to contain text "${expected}", ${foundText} found.`; + + const processedExpected = utils.arrayfy(expected); + const texts = await this._browser.text(selector); + + for (const expectedText of processedExpected) { + if (!utils.matchTextContainingList(texts, expectedText)) { + if (!msg) { + const foundText = texts.length === 0 ? "no text" : `"${texts.join(" ")}"`; + msg = `Expected element "${selector}" to contain text "${expectedText}", ${foundText} found.`; + } + throw new AssertionError("assert.textContains", msg); + } } - return assertUtils.rejectAssertion("assert.textContains", msg); } public async title(expected: string | RegExp, msg?: string): Promise { @@ -99,7 +106,7 @@ export default class AssertionsCore { const foundTitle = title ? `"${title}"` : "no title"; if (!utils.matchText(title, expected)) { if (!msg) msg = `Expected page title to be "${expected}", ${foundTitle} found.`; - return assertUtils.rejectAssertion("assert.title", msg); + throw new AssertionError("assert.title", msg); } } @@ -115,7 +122,7 @@ export default class AssertionsCore { const foundClasses = classes.length === 0 ? "no classes" : `"${classes.join(" ")}"`; msg = `Expected element "${selector}" to contain class "${expected}", ${foundClasses} found.`; } - return assertUtils.rejectAssertion("assert.class", msg); + throw new AssertionError("assert.class", msg); } } @@ -128,7 +135,7 @@ export default class AssertionsCore { } if (!utils.matchText(url, expected)) { if (!msg) msg = `Expected url to be "${utils.stringify(expected)}", "${url}" found`; - return assertUtils.rejectAssertion("assert.url", msg, url, expected); + throw new AssertionError("assert.url", msg, url, expected); } } @@ -139,7 +146,7 @@ export default class AssertionsCore { if (value === null) msg = `Expected element "${selector}" to have value "${expected}", no value found`; else msg = `Expected element "${selector}" to have value "${expected}", "${value}" found`; } - return assertUtils.rejectAssertion("assert.value", msg, value, expected); + throw new AssertionError("assert.value", msg, value, expected); } } @@ -179,7 +186,7 @@ export default class AssertionsCore { if (attributes.length === 0) { if (!customMessage) msg = `${msg}, no element found.`; - return assertUtils.rejectAssertion("assert.attribute", msg as string); + throw new AssertionError("assert.attribute", msg as string); } const filteredAttributes = attributes.filter(a => a !== null); @@ -203,7 +210,7 @@ export default class AssertionsCore { msg = `${msg}, ["${foundArr.join('", "')}"] found.`; } } - return assertUtils.rejectAssertion("assert.attribute", msg as string); + throw new AssertionError("assert.attribute", msg as string); } public async style(selector: WendigoSelector, style: string, expected: string, msg?: string): Promise { @@ -224,7 +231,7 @@ export default class AssertionsCore { if (value) msg = `${msg}, "${value}" found.`; else msg = `${msg}, style not found.`; } - return assertUtils.rejectAssertion("assert.style", msg); + throw new AssertionError("assert.style", msg); } } @@ -252,7 +259,7 @@ export default class AssertionsCore { msg = `Expected element "${selector}" to have inner html "${expected}", "${found.join(" ")}" found.`; } - return assertUtils.rejectAssertion("assert.innerHtml", msg, found, expected); + throw new AssertionError("assert.innerHtml", msg, found, expected); } public async options(selector: WendigoSelector, expected: string | Array, msg?: string): Promise { @@ -265,7 +272,7 @@ export default class AssertionsCore { const optionsText = options.join(", "); msg = `Expected element "${selector}" to have options "${expectedText}", "${optionsText}" found.`; } - return assertUtils.rejectAssertion("assert.options", msg, options, expected); + throw new AssertionError("assert.options", msg, options, expected); } } @@ -279,7 +286,7 @@ export default class AssertionsCore { const optionsText = selectedOptions.join(", "); msg = `Expected element "${selector}" to have options "${expectedText}" selected, "${optionsText}" found.`; } - return assertUtils.rejectAssertion("assert.selectedOptions", msg, selectedOptions, expected); + throw new AssertionError("assert.selectedOptions", msg, selectedOptions, expected); } } @@ -292,13 +299,13 @@ export default class AssertionsCore { if (!msg) { msg = `Expected "${key}" to be defined as global variable.`; } - return assertUtils.rejectAssertion("assert.global", msg); + throw new AssertionError("assert.global", msg); } } else if (value !== expected) { if (!msg) { msg = `Expected "${key}" to be defined as global variable with value "${expected}", "${value}" found.`; } - return assertUtils.rejectAssertion("assert.global", msg, value, expected); + throw new AssertionError("assert.global", msg, value, expected); } return Promise.resolve(); } @@ -312,7 +319,7 @@ export default class AssertionsCore { } if (value !== true) { if (!msg) msg = `Expected element "${selector}" to be checked.`; - return assertUtils.rejectAssertion("assert.checked", msg, value, true); + throw new AssertionError("assert.checked", msg, value, true); } } @@ -325,7 +332,7 @@ export default class AssertionsCore { } if (value === null) { if (!msg) msg = `Expected element "${selector}" to be disabled.`; - return assertUtils.rejectAssertion("assert.disabled", msg); + throw new AssertionError("assert.disabled", msg); } } @@ -338,7 +345,7 @@ export default class AssertionsCore { } if (value !== null) { if (!msg) msg = `Expected element "${selector}" to be enabled.`; - return assertUtils.rejectAssertion("assert.enabled", msg); + throw new AssertionError("assert.enabled", msg); } } @@ -358,20 +365,19 @@ export default class AssertionsCore { } if (!focused) { if (!msg) msg = `Expected element "${selector}" to be focused.`; - return assertUtils.rejectAssertion("assert.focus", msg); + throw new AssertionError("assert.focus", msg); } } - public redirect(msg?: string): Promise { + public async redirect(msg?: string): Promise { if (!msg) msg = `Expected current url to be a redirection.`; - if (!this._browser.initialResponse) assertUtils.rejectAssertion("assert.redirect", msg); + if (!this._browser.initialResponse) throw new AssertionError("assert.redirect", msg); else { const chain = this._browser.initialResponse.request().redirectChain(); if (chain.length === 0) { - return assertUtils.rejectAssertion("assert.redirect", msg); + throw new AssertionError("assert.redirect", msg); } } - return Promise.resolve(); } } diff --git a/lib/browser/assertions/browser_not_assertions.ts b/lib/browser/assertions/browser_not_assertions.ts index 1461c11e..1810b99f 100644 --- a/lib/browser/assertions/browser_not_assertions.ts +++ b/lib/browser/assertions/browser_not_assertions.ts @@ -1,6 +1,6 @@ import * as utils from '../../utils/utils'; -import * as assertUtils from '../../utils/assert_utils'; -import { WendigoError, QueryError } from '../../errors'; +import {invertify} from '../../utils/assert_utils'; +import { WendigoError, QueryError, AssertionError } from '../../errors'; import BrowserAssertions from '../browser_assertions'; import BrowserInterface from '../../browser/browser_interface'; import { WendigoSelector } from '../../types'; @@ -16,21 +16,21 @@ export default class BrowserNotAssertions { public exists(selector: WendigoSelector, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" to not exists.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.exists(selector, "x"); }, "assert.not.exists", msg); } public visible(selector: WendigoSelector, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" to not be visible.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.visible(selector, "x"); }, "assert.not.visible", msg); } public tag(selector: WendigoSelector, expected: string, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" to not have "${expected}" tag.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.tag(selector, expected, "x"); }, "assert.not.tag", msg); } @@ -45,42 +45,53 @@ export default class BrowserNotAssertions { for (const expectedText of processedExpected) { if (utils.matchTextList(texts, expectedText)) { if (!msg) msg = `Expected element "${selector}" not to have text "${expectedText}".`; - return assertUtils.rejectAssertion("assert.not.text", msg); + throw new AssertionError("assert.not.text", msg); } } } - public textContains(selector: WendigoSelector, expected: string, msg?: string): Promise { - if (!msg) msg = `Expected element "${selector}" to not contain text "${expected}".`; - return assertUtils.invertify(() => { - return this._assertions.textContains(selector, expected, "x"); - }, "assert.not.textContains", msg); + public async textContains(selector: WendigoSelector, expected: string, msg?: string): Promise { + if ((!expected && expected !== "") || (Array.isArray(expected) && expected.length === 0)) { + throw new WendigoError("assert.not.textContains", `Missing expected text for assertion.`); + } + + const processedExpected = utils.arrayfy(expected); + const texts = await this._browser.text(selector); + + for (const expectedText of processedExpected) { + if (utils.matchTextContainingList(texts, expectedText)) { + if (!msg) { + msg = `Expected element "${selector}" to not contain text "${expectedText}".`; + } + throw new AssertionError("assert.not.textContains", msg); + } + } } public title(expected: string | RegExp, msg?: string): Promise { if (!msg) msg = `Expected page title not to be "${expected}".`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.title(expected, "x"); }, "assert.not.title", msg); } public class(selector: WendigoSelector, expected: string, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" not to contain class "${expected}".`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.class(selector, expected, "x"); }, "assert.not.class", msg); } public url(expected: string | RegExp, msg?: string): Promise { if (!msg) msg = `Expected url not to be "${expected}"`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.url(expected, "x"); }, "assert.not.url", msg); } public value(selector: WendigoSelector, expected: string | null, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" not to have value "${expected}".`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.value(selector, expected, "x"); }, "assert.not.value", msg); } @@ -96,17 +107,17 @@ export default class BrowserNotAssertions { if (!customMessage) { msg = `${msg}, no element found.`; } - return assertUtils.rejectAssertion("assert.not.attribute", msg as string); + throw new AssertionError("assert.not.attribute", msg as string); } if (!customMessage) msg = `${msg}.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.attribute(selector, attribute, expectedValue, "x"); }, "assert.not.attribute", msg as string); } public style(selector: WendigoSelector, style: string, expected: string, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" not to have style "${style}" with value "${expected}".`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.style(selector, style, expected, "x"); }, "assert.not.style", msg); } @@ -121,7 +132,7 @@ export default class BrowserNotAssertions { public innerHtml(selector: WendigoSelector, expected: string | RegExp, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" not to have inner html "${expected}".`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.innerHtml(selector, expected, "x"); }, "assert.not.innerHtml", msg); } @@ -132,7 +143,7 @@ export default class BrowserNotAssertions { const expectedText = parsedExpected.join(", "); msg = `Expected element "${selector}" not to have options "${expectedText}" selected.`; } - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.selectedOptions(selector, expected, "x"); }, "assert.not.selectedOptions", msg); } @@ -142,7 +153,7 @@ export default class BrowserNotAssertions { if (expected === undefined) msg = `Expected "${key}" not to be defined as global variable.`; else msg = `Expected "${key}" not to be defined as global variable with value "${expected}".`; } - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.global(key, expected, "x"); }, "assert.not.global", msg); } @@ -156,34 +167,34 @@ export default class BrowserNotAssertions { } if (value !== false) { if (!msg) msg = `Expected element "${selector}" to not be checked.`; - return assertUtils.rejectAssertion("assert.not.checked", msg, value, false); + throw new AssertionError("assert.not.checked", msg, value, false); } } public disabled(selector: WendigoSelector, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" not to be disabled.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.disabled(selector, "x"); }, "assert.not.disabled", msg); } public enabled(selector: WendigoSelector, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" not to be enabled.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.enabled(selector, "x"); }, "assert.not.enabled", msg); } public focus(selector: WendigoSelector, msg?: string): Promise { if (!msg) msg = `Expected element "${selector}" to be unfocused.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.focus(selector, "x"); }, "assert.not.focus", msg); } public redirect(msg?: string): Promise { if (!msg) msg = `Expected current url not to be a redirection.`; - return assertUtils.invertify(() => { + return invertify(() => { return this._assertions.redirect("x"); }, "assert.not.redirect", msg); } diff --git a/lib/browser/browser_core.ts b/lib/browser/browser_core.ts index f765f9b7..6055fb0a 100644 --- a/lib/browser/browser_core.ts +++ b/lib/browser/browser_core.ts @@ -65,9 +65,14 @@ export default abstract class BrowserCore { return this._loaded && !this.disabled; } + public get incognito(): boolean { + return Boolean(this.settings.incognito); + } + public async open(url: string, options?: OpenSettings): Promise { this._loaded = false; options = Object.assign({}, defaultOpenOptions, options); + url = this._processUrl(url); if (options.queryString) { const qs = this._generateQueryString(options.queryString); url = `${url}${qs}`; @@ -86,7 +91,7 @@ export default abstract class BrowserCore { public async openFile(filepath: string, options: OpenSettings): Promise { const absolutePath = path.resolve(filepath); try { - await this.open(`file:${absolutePath}`, options); + await this.open(`file://${absolutePath}`, options); } catch (err) { return Promise.reject(new FatalError("openFile", `Failed to open "${filepath}". File not found.`)); } @@ -217,4 +222,10 @@ export default abstract class BrowserCore { return `?${querystring.stringify(qs)}`; } } + + private _processUrl(url: string): string { + if (url.split("://").length === 1) { + return `http://${url}`; + } else return url; + } } diff --git a/lib/browser/mixins/browser_actions.ts b/lib/browser/mixins/browser_actions.ts index f1ccf7e0b..96713100 100644 --- a/lib/browser/mixins/browser_actions.ts +++ b/lib/browser/mixins/browser_actions.ts @@ -2,10 +2,12 @@ import { Base64ScreenShotOptions } from 'puppeteer'; import BrowserQueries from './browser_queries'; -import { arrayfy } from '../../utils/utils'; +import { arrayfy, isXPathQuery } from '../../utils/utils'; import { QueryError, WendigoError } from '../../errors'; import { WendigoSelector } from '../../types'; +import DOMELement from '../../models/dom_element'; +// Mixin with user actions export default abstract class BrowserActions extends BrowserQueries { public async type(selector: WendigoSelector, text: string, options?: { delay: number }): Promise { this._failIfNotLoaded("type"); @@ -42,14 +44,20 @@ export default abstract class BrowserActions extends BrowserQueries { }); } - public async select(cssSelector: string, values: Array | string): Promise> { + public async select(selector: WendigoSelector, values: Array | string): Promise> { this._failIfNotLoaded("select"); if (!Array.isArray(values)) values = [values]; try { - const result = await this.page.select(cssSelector, ...values); - return result; + let cssPath: string; + // Native select only support css selectors + if (selector instanceof DOMELement || isXPathQuery(selector)) { + const element = await this.query(selector); + if (!element) throw new Error(); + cssPath = await this.findCssPath(element); + } else cssPath = selector; + return await this.page.select(cssPath, ...values); } catch (err) { - throw new QueryError("select", `Element "${cssSelector}" not found.`); + throw new QueryError("select", `Element "${selector}" not found.`); } } @@ -165,11 +173,13 @@ export default abstract class BrowserActions extends BrowserQueries { } } - // async dragAndDrop(from, to) { + // public async dragAndDrop(from: WendigoSelector, to: WendigoSelector): Promise { // const fromElement = await this.query(from); // const toElement = await this.query(to); + // if (!fromElement || !toElement) throw new QueryError("dragAndDrop", `Elements "${from} and ${to} not found."`); // const boxFrom = await fromElement.element.boundingBox(); // const boxTo = await toElement.element.boundingBox(); + // if (!boxFrom || !boxTo) throw new FatalError("dragAndDrop", "Bounding box not found"); // const mouse = this.page.mouse; // await mouse.up(); // await mouse.move(boxFrom.x + (boxFrom.width / 2), boxFrom.y + (boxFrom.height / 2)); diff --git a/lib/browser/mixins/browser_edit.ts b/lib/browser/mixins/browser_edit.ts new file mode 100644 index 00000000..e79291e0 --- /dev/null +++ b/lib/browser/mixins/browser_edit.ts @@ -0,0 +1,43 @@ +import BrowserInfo from './browser_info'; + +import { WendigoSelector } from '../../types'; +import { WendigoError, QueryError } from '../../errors'; + +// Mixin with methods to edit the DOM and state +export default abstract class BrowserEdit extends BrowserInfo { + public async addClass(selector: WendigoSelector, className: string): Promise { + this._failIfNotLoaded("addClass"); + try { + const rawClasses = await this.attribute(selector, "class"); + await this.setAttribute(selector, "class", `${rawClasses} ${className}`); + } catch (err) { + throw WendigoError.overrideFnName(err, "addClass"); + } + } + + public async removeClass(selector: WendigoSelector, className: string): Promise { + this._failIfNotLoaded("removeClass"); + try { + const classList = await this.class(selector); + const finalClassList = classList.filter((cl) => { + return cl !== className; + }); + await this.setAttribute(selector, "class", finalClassList.join(" ")); + } catch (err) { + throw WendigoError.overrideFnName(err, "removeClass"); + } + } + + public async setAttribute(selector: WendigoSelector, attribute: string, value: string): Promise { + this._failIfNotLoaded("setAttribute"); + try { + await this.evaluate((q, attr, val) => { + const element = WendigoUtils.queryElement(q); + if (val === null) element.removeAttribute(attr); + else element.setAttribute(attr, val); + }, selector, attribute, value); + } catch (err) { + throw new QueryError("setAttribute", `Element "${selector}" not found.`); + } + } +} diff --git a/lib/browser/mixins/browser_events.ts b/lib/browser/mixins/browser_events.ts index 1c9380db..c6ebfa66 100644 --- a/lib/browser/mixins/browser_events.ts +++ b/lib/browser/mixins/browser_events.ts @@ -1,8 +1,8 @@ -import BrowserInfo from './browser_info'; +import BrowserEdit from './browser_edit'; import { WendigoSelector } from '../../types'; -export default abstract class BrowserEvents extends BrowserInfo { +export default abstract class BrowserEvents extends BrowserEdit { public triggerEvent(selector: WendigoSelector, eventName: string, options: EventInit): Promise { this._failIfNotLoaded("triggerEvent"); return this.evaluate((q, evName, opt) => { diff --git a/lib/browser/mixins/browser_queries.ts b/lib/browser/mixins/browser_queries.ts index 339aa8e3..57ca08db 100644 --- a/lib/browser/mixins/browser_queries.ts +++ b/lib/browser/mixins/browser_queries.ts @@ -4,7 +4,7 @@ import { ElementHandle } from 'puppeteer'; import DomElement from '../../models/dom_element'; import { FatalError, WendigoError } from '../../errors'; import { WendigoSelector } from '../../types'; -import { isXPathQuery, cleanStringForXpath } from '../../utils/utils'; +import { isXPathQuery, createFindTextXPath } from '../../utils/utils'; export default abstract class BrowserQueries extends BrowserCore { public async query(selector: WendigoSelector, optionalSelector?: string): Promise { @@ -57,7 +57,7 @@ export default abstract class BrowserQueries extends BrowserCore { public async findByText(text: string | DomElement, optionalText?: string): Promise> { this._failIfNotLoaded("findByText"); const xPathText = optionalText || text as string; - const xPath = `//*[text()=${cleanStringForXpath(xPathText)}]`; + const xPath = createFindTextXPath(xPathText); if (optionalText) { try { @@ -73,7 +73,8 @@ export default abstract class BrowserQueries extends BrowserCore { public async findByTextContaining(text: string | DomElement, optionalText?: string): Promise> { this._failIfNotLoaded("findByTextContaining"); const xPathText = optionalText || text as string; - const xPath = `//*[contains(text(),${cleanStringForXpath(xPathText)})]`; + const xPath = createFindTextXPath(xPathText, true); + if (optionalText) { try { return await this.queryAll(text, xPath); diff --git a/lib/browser/mixins/browser_wait.ts b/lib/browser/mixins/browser_wait.ts index dae431e9..94b5a1db 100644 --- a/lib/browser/mixins/browser_wait.ts +++ b/lib/browser/mixins/browser_wait.ts @@ -6,6 +6,7 @@ import * as utils from '../../utils/utils'; import DomElement from '../../models/dom_element'; import { TimeoutError, WendigoError } from '../../errors'; import { WendigoSelector } from '../../types'; +import { createFindTextXPath } from '../../utils/utils'; export default abstract class BrowserWait extends BrowserNavigation { public wait(ms: number = 250): Promise { @@ -13,7 +14,6 @@ export default abstract class BrowserWait extends BrowserNavigation { return utils.delay(ms); } - // TODO: Only css selector supported public async waitFor(selector: string | EvaluateFn, timeout = 500, ...args: Array): Promise { this._failIfNotLoaded("waitFor"); args = args.map((e) => { @@ -105,16 +105,15 @@ export default abstract class BrowserWait extends BrowserNavigation { public async waitForText(text: string, timeout: number = 500): Promise { try { - await this.waitFor((txt: string) => { - const xpath = `//*[text()='${txt}']`; // NOTE: Duplicate of findByText - return Boolean(WendigoUtils.xPathQuery(xpath).length > 0); - }, timeout, text); + const xPath = createFindTextXPath(text); + await this.waitFor((xp: string) => { + return Boolean(WendigoUtils.xPathQuery(xp).length > 0); + }, timeout, xPath); } catch (err) { throw new TimeoutError("waitForText", `Waiting for text "${text}"`, timeout); } } - // NOTE: Only Css Selector supported public async waitAndClick(selector: string, timeout: number = 500): Promise { try { await this.waitFor(selector, timeout); diff --git a/lib/modules/console/console_assertion.ts b/lib/modules/console/console_assertion.ts index 8fe2759d..1e4bfea9 100644 --- a/lib/modules/console/console_assertion.ts +++ b/lib/modules/console/console_assertion.ts @@ -1,7 +1,7 @@ -import * as assertUtils from '../../utils/assert_utils'; import BrowserConsole from './browser_console'; import { ConsoleFilter } from './types'; import { isNumber } from '../../utils/utils'; +import { AssertionError } from '../../errors'; function processMessage(filterOptions: ConsoleFilter, count: number, actualCount: number): string { const filterMessage = processFilterMessage(filterOptions); @@ -19,18 +19,17 @@ function processFilterMessage(filterOptions: ConsoleFilter): string { return `${typeMsg}${textMsg}`; } -export default function(consoleModule: BrowserConsole, filterOptions: ConsoleFilter, count?: number, msg?: string): Promise { +export default async function(consoleModule: BrowserConsole, filterOptions: ConsoleFilter, count?: number, msg?: string): Promise { const logs = consoleModule.filter(filterOptions); if (!isNumber(count)) { if (logs.length <= 0) { if (!msg) msg = processMessageWithoutExpectedCount(filterOptions); - return assertUtils.rejectAssertion("assert.console", msg); + throw new AssertionError("assert.console", msg); } } else { if (logs.length !== count) { if (!msg) msg = processMessage(filterOptions, count, logs.length); - return assertUtils.rejectAssertion("assert.console", msg); + throw new AssertionError("assert.console", msg); } } - return Promise.resolve(); } diff --git a/lib/modules/cookies/browser_cookies.ts b/lib/modules/cookies/browser_cookies.ts index 012863b0..a8306fc5 100644 --- a/lib/modules/cookies/browser_cookies.ts +++ b/lib/modules/cookies/browser_cookies.ts @@ -1,4 +1,4 @@ -import { Cookie as CookieData, SetCookie } from 'puppeteer'; +import { Cookie as CookieData, SetCookie, DeleteCookie } from 'puppeteer'; import WendigoModule from '../wendigo_module'; import { WendigoError } from '../../errors'; import { arrayfy } from '../../utils/utils'; @@ -12,8 +12,11 @@ export default class BrowserCookies extends WendigoModule { }, {} as { [s: string]: string }); } - public async get(name: string): Promise { - const cookies = await this._browser.page.cookies(); + public async get(name: string, url?: string): Promise { + let cookies: Array; + if (url) { + cookies = await this._browser.page.cookies(url); + } else cookies = await this._browser.page.cookies(); return cookies.find((cookie) => { return cookie.name === name; }); @@ -32,8 +35,13 @@ export default class BrowserCookies extends WendigoModule { return this._browser.page.setCookie(data); } - public delete(name: string | Array): Promise { + public delete(name: string | Array | DeleteCookie): Promise { if (name === undefined || name === null) throw new WendigoError("cookies.delete", "Delete cookie name missing"); + + if (this.isDeleteCookieInterface(name)) { + return this._browser.page.deleteCookie(name); + } + const cookiesList = arrayfy(name); if (cookiesList.length === 0) return Promise.resolve(); const cookiesObjects = cookiesList.map((n) => { @@ -47,4 +55,9 @@ export default class BrowserCookies extends WendigoModule { const cookiesList = cookies.map(c => c.name); return this.delete(cookiesList); } + + private isDeleteCookieInterface(data: any): data is DeleteCookie { + if (data.name) return true; + else return false; + } } diff --git a/lib/modules/cookies/cookies_assertion.ts b/lib/modules/cookies/cookies_assertion.ts index ff8a7d67..069b5862 100644 --- a/lib/modules/cookies/cookies_assertion.ts +++ b/lib/modules/cookies/cookies_assertion.ts @@ -1,6 +1,7 @@ -import * as assertUtils from '../../utils/assert_utils'; +import { invertify } from '../../utils/assert_utils'; import BrowserCookies from './browser_cookies'; import BrowserInterface from '../../browser/browser_interface'; +import { AssertionError } from '../../errors'; export default { async assert(_browser: BrowserInterface, cookiesModule: BrowserCookies, name: string, expected?: string, msg?: string): Promise { @@ -10,7 +11,7 @@ export default { if (!msg) { msg = `Expected cookie "${name}" to exist.`; } - return assertUtils.rejectAssertion("assert.cookies", msg); + throw new AssertionError("assert.cookies", msg); } } else { const value = cookie ? cookie.value : undefined; @@ -18,7 +19,7 @@ export default { if (!msg) { msg = `Expected cookie "${name}" to have value "${expected}", "${value}" found.`; } - return assertUtils.rejectAssertion("assert.cookies", msg, value, expected); + throw new AssertionError("assert.cookies", msg, value, expected); } } }, @@ -28,7 +29,7 @@ export default { `Expected cookie "${name}" to not exist.` : `Expected cookie "${name}" to not have value "${expected}".`; } - return assertUtils.invertify(() => { + return invertify(() => { return browser.assert.cookies(name, expected, "x"); }, "assert.not.cookies", msg); } diff --git a/lib/modules/local_storage/browser_local_storage.ts b/lib/modules/local_storage/browser_local_storage.ts index b7d5612d..9cc2cae1 100644 --- a/lib/modules/local_storage/browser_local_storage.ts +++ b/lib/modules/local_storage/browser_local_storage.ts @@ -8,7 +8,7 @@ export default class BrowserLocalStorage extends WendigoModule { return localStorage.getItem(k); }, key); } catch (err) { - return Promise.reject(WendigoError.overrideFnName(err, "localStorage.getItem")); + throw WendigoError.overrideFnName(err, "localStorage.getItem"); } } @@ -18,7 +18,7 @@ export default class BrowserLocalStorage extends WendigoModule { return localStorage.setItem(k, v); }, key, value); } catch (err) { - return Promise.reject(WendigoError.overrideFnName(err, "localStorage.setItem")); + throw WendigoError.overrideFnName(err, "localStorage.setItem"); } } @@ -28,7 +28,7 @@ export default class BrowserLocalStorage extends WendigoModule { return localStorage.removeItem(k); }, key); } catch (err) { - return Promise.reject(WendigoError.overrideFnName(err, "localStorage.removeItem")); + throw WendigoError.overrideFnName(err, "localStorage.removeItem"); } } @@ -38,7 +38,7 @@ export default class BrowserLocalStorage extends WendigoModule { return localStorage.clear(); }); } catch (err) { - return Promise.reject(WendigoError.overrideFnName(err, "localStorage.clear")); + throw WendigoError.overrideFnName(err, "localStorage.clear"); } } @@ -49,7 +49,7 @@ export default class BrowserLocalStorage extends WendigoModule { }); return result; } catch (err) { - return Promise.reject(WendigoError.overrideFnName(err, "localStorage.length")); + throw WendigoError.overrideFnName(err, "localStorage.length"); } } } diff --git a/lib/modules/local_storage/local_storage_assertions.ts b/lib/modules/local_storage/local_storage_assertions.ts index 9eb32124..8e13f9ff 100644 --- a/lib/modules/local_storage/local_storage_assertions.ts +++ b/lib/modules/local_storage/local_storage_assertions.ts @@ -1,7 +1,7 @@ import BrowserLocalStorageNotAssertions from './local_storage_not_assertions'; -import * as assertUtils from '../../utils/assert_utils'; import { arrayfy } from '../../utils/utils'; import BrowserLocalStorage from './browser_local_storage'; +import { AssertionError } from '../../errors'; export default class BrowserLocalStorageAssertions { private _localStorage: BrowserLocalStorage; @@ -23,7 +23,7 @@ export default class BrowserLocalStorageAssertions { }); if (nullValues.length !== 0) { if (!msg) msg = `Expected ${itemWord} "${keyList.join(" ")}" to exist in localStorage.`; - return assertUtils.rejectAssertion("assert.localStorage.exist", msg); + throw new AssertionError("assert.localStorage.exist", msg); } } @@ -49,7 +49,7 @@ export default class BrowserLocalStorageAssertions { const realVals = values.map(val => String(val[1])).join(" "); msg = `Expected ${itemText} "${keys.join(" ")}" to have ${valuesText} "${expectedVals}" in localStorage, "${realVals}" found.`; // eslint-disable-line max-len } - return assertUtils.rejectAssertion("assert.localStorage.value", msg); + throw new AssertionError("assert.localStorage.value", msg); } } } @@ -58,7 +58,7 @@ export default class BrowserLocalStorageAssertions { const res = await this._localStorage.length(); if (res !== expected) { if (!msg) msg = `Expected localStorage to have ${expected} items, ${res} found.`; - return assertUtils.rejectAssertion("assert.localStorage.length", msg, res, expected); + throw new AssertionError("assert.localStorage.length", msg, res, expected); } } @@ -69,7 +69,7 @@ export default class BrowserLocalStorageAssertions { const itemText = res === 1 ? "item" : "items"; msg = `Expected localStorage to be empty, ${res} ${itemText} found.`; } - return assertUtils.rejectAssertion("assert.localStorage.empty", msg); + throw new AssertionError("assert.localStorage.empty", msg); } } } diff --git a/lib/modules/requests/request_mock.ts b/lib/modules/requests/request_mock.ts index 2daaea6d..af25dda0 100644 --- a/lib/modules/requests/request_mock.ts +++ b/lib/modules/requests/request_mock.ts @@ -105,8 +105,7 @@ export default class RequestMock implements RequestMockInterface { } public async waitUntilCalled(timeout: number = 500): Promise { - if (this.called) return Promise.resolve(); - await new Promise((resolve, reject) => { + if (!this.called) await new Promise((resolve, reject) => { let rejected = false; const tid = setTimeout(() => { rejected = true; @@ -119,7 +118,7 @@ export default class RequestMock implements RequestMockInterface { } }); }); - await utils.delay(30); // Give time to the browser to handle the response + await utils.delay(20); // Give time to the browser to handle the response } public async onRequest(request: Request): Promise { diff --git a/lib/modules/webworkers/webworkers_assertions.ts b/lib/modules/webworkers/webworkers_assertions.ts index eefdcdbc..f3c2b063 100644 --- a/lib/modules/webworkers/webworkers_assertions.ts +++ b/lib/modules/webworkers/webworkers_assertions.ts @@ -1,6 +1,6 @@ -import * as assertUtils from '../../utils/assert_utils'; import BrowserWebWorker from './browser_webworker'; import WebWorker from './webworker'; +import { AssertionError } from '../../errors'; interface WebWorkersOptions { url?: string; @@ -18,11 +18,11 @@ export default async function(webworkerModule: BrowserWebWorker, options?: WebWo if (options.count !== undefined && options.count !== null) { if (workers.length !== options.count) { if (!msg) msg = `Expected ${options.count} webworkers running${urlMsg}, ${workers.length} found.`; - await assertUtils.rejectAssertion("assert.webworkers", msg); + throw new AssertionError("assert.webworkers", msg); } } else if (workers.length === 0) { if (!msg) msg = `Expected at least 1 webworker running${urlMsg}, 0 found.`; - await assertUtils.rejectAssertion("assert.webworkers", msg); + throw new AssertionError("assert.webworkers", msg); } } diff --git a/lib/utils/assert_utils.ts b/lib/utils/assert_utils.ts index 2ac2e3fc..67a8f6b8 100644 --- a/lib/utils/assert_utils.ts +++ b/lib/utils/assert_utils.ts @@ -11,7 +11,7 @@ export async function invertify(cb: () => Promise, fnName: string, msg: st return Promise.reject(newError); } else return Promise.reject(err); } - return rejectAssertion(fnName, msg); + throw new AssertionError(fnName, msg); } export function sameMembers(arr1: Array, arr2: Array): boolean { @@ -22,7 +22,3 @@ export function sameMembers(arr1: Array, arr2: Array): boolean { } return true; } - -export function rejectAssertion(fnName: string, msg: string, actual?: any, expected?: any): Promise { - return Promise.reject(new AssertionError(fnName, msg, actual, expected)); -} diff --git a/lib/utils/rejectAssertion.ts b/lib/utils/rejectAssertion.ts new file mode 100644 index 00000000..e69de29b diff --git a/lib/utils/utils.ts b/lib/utils/utils.ts index 3de0c585..a27871a0 100644 --- a/lib/utils/utils.ts +++ b/lib/utils/utils.ts @@ -60,6 +60,13 @@ export function matchTextList(list: Array, expected: string | RegExp): b return false; } +export function matchTextContainingList(list: Array, expected: string): boolean { + for (const text of list) { + if (text.includes(expected)) return true; + } + return false; +} + export function delay(ms: number): Promise { return new Promise((resolve) => { setTimeout(() => { @@ -94,7 +101,13 @@ export function arrayfy(raw: T | Array): Array { else return [raw]; } -export function cleanStringForXpath(str: string): string { +export function createFindTextXPath(text: string, contains: boolean = false): string { + const cleanedString = cleanStringForXpath(text); + if (contains) return `//*[contains(text(),${cleanedString})]`; + else return `//*[text()=${cleanedString}]`; +} + +function cleanStringForXpath(str: string): string { const parts = str.split('\''); if (parts.length === 1) return `'${parts[0]}'`; diff --git a/package-lock.json b/package-lock.json index cd256d09..568bba3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wendigo", - "version": "2.0.2", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 84c3c2b6..0a921cd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wendigo", - "version": "2.0.2", + "version": "2.1.0", "description": "A proper monster for front-end automated testing", "engines": { "node": ">=8.0.0" diff --git a/tests/assertions/assert_text_contains.test.js b/tests/assertions/assert_text_contains.test.js index cffca8ca..f1de8729 100644 --- a/tests/assertions/assert_text_contains.test.js +++ b/tests/assertions/assert_text_contains.test.js @@ -86,4 +86,46 @@ describe("Assert Text Contains", function() { await browser.assert.not.textContains(".container p", "first", "not textcontains fails"); }, `[assert.not.textContains] not textcontains fails`); }); + + it("Multiple Texts in Array With Regex", async() => { + await browser.assert.textContains("p", ["My first", "My second"]); + await browser.assert.textContains("p", ["My first"]); + }); + + it("Multiple Texts in Array Throws", async() => { + await utils.assertThrowsAssertionAsync(async() => { + await browser.assert.textContains("p", ["My first paragraph", "My pa2ragraph"]); + }, `[assert.textContains] Expected element "p" to contain text "My pa2ragraph", "My first paragraph My second paragraph" found.`); + }); + + it("Not Multiple Texts in Array", async() => { + await browser.assert.not.textContains("p", ["not a correct text", "another incorrect text"]); + }); + + it("Not Multiple Texts in Array Throws", async() => { + await utils.assertThrowsAssertionAsync(async() => { + await browser.assert.not.textContains("p", ["incorrect", "My first", "not a correct text"]); + }, `[assert.not.textContains] Expected element "p" to not contain text "My first".`); + }); + + it("Text With Empty Parameter Array Throws", async() => { + await utils.assertThrowsAsync(async() => { + await browser.assert.textContains("h1", []); + }, `Error: [assert.textContains] Missing expected text for assertion.`); + }); + + it("Not Text With Empty Parameter Array Throws", async() => { + await utils.assertThrowsAsync(async() => { + await browser.assert.not.textContains("h1", []); + }, `Error: [assert.not.textContains] Missing expected text for assertion.`); + }); + + it("Text Contains Throws", async() => { + await utils.assertThrowsAssertionAsync(async() => { + await browser.assert.textContains(".container p", "My second"); + }, `[assert.textContains] Expected element ".container p" to contain text "My second", "My first paragraph" found.`); + await utils.assertThrowsAssertionAsync(async() => { + await browser.assert.textContains("p", "My paragraph"); + }, `[assert.textContains] Expected element "p" to contain text "My paragraph", "My first paragraph My second paragraph" found.`); + }); }); diff --git a/tests/browser/edit_class.test.js b/tests/browser/edit_class.test.js new file mode 100644 index 00000000..df446f97 --- /dev/null +++ b/tests/browser/edit_class.test.js @@ -0,0 +1,92 @@ +"use strict"; + +const Wendigo = require('../..'); +const configUrls = require('../config.json').urls; +const utils = require('../test_utils'); + +describe("Edit Class", function() { + this.timeout(5000); + + let browser; + before(async() => { + browser = await Wendigo.createBrowser(); + }); + + beforeEach(async() => { + await browser.open(configUrls.index); + }); + + after(async() => { + await browser.close(); + }); + + it("Add Class", async() => { + await browser.assert.class(".container", "extra-class"); + await browser.assert.not.class(".container", "new-class"); + await browser.addClass(".container", "new-class"); + await browser.assert.class(".container", "extra-class"); + await browser.assert.class(".container", "new-class"); + }); + + it("Add Repeated Class", async() => { + await browser.assert.class(".container", "extra-class"); + await browser.assert.not.class(".container", "new-class"); + await browser.addClass(".container", "extra-class"); + await browser.assert.class(".container", "extra-class"); + await browser.assert.not.class(".container", "new-class"); + }); + + it("Add Class DOMElement", async() => { + const element = await browser.query(".container"); + await browser.assert.class(".container", "extra-class"); + await browser.assert.not.class(".container", "new-class"); + await browser.addClass(element, "new-class"); + await browser.assert.class(".container", "extra-class"); + await browser.assert.class(".container", "new-class"); + }); + + it("Add Class XPath", async() => { + await browser.assert.class(".container", "extra-class"); + await browser.assert.not.class(".container", "new-class"); + await browser.addClass("//*[contains(@class, 'container')]", "new-class"); + await browser.assert.class(".container", "extra-class"); + await browser.assert.class(".container", "new-class"); + }); + + it("Add Class Invalid Element", async() => { + await utils.assertThrowsAsync(async() => { + await browser.addClass(".not-an-element", "extra-class"); + }, `QueryError: [addClass] Element ".not-an-element" not found.`); + }); + + it("Remove Class", async() => { + await browser.assert.class(".container", "extra-class"); + await browser.removeClass(".container", "extra-class"); + await browser.assert.not.class(".container", "extra-class"); + }); + + it("Remove Non Existing Class", async() => { + await browser.assert.not.class(".container", "new-class"); + await browser.removeClass(".container", "new-class"); + await browser.assert.not.class(".container", "new-class"); + }); + + it("Remove Class DOMElement", async() => { + const element = await browser.query(".container"); + await browser.assert.class(".container", "extra-class"); + await browser.removeClass(element, "extra-class"); + await browser.assert.not.class(".container", "extra-class"); + }); + + it("Remove Class XPath", async() => { + await browser.assert.class(".container", "extra-class"); + await browser.removeClass("//*[contains(@class, 'container')]", "extra-class"); + await browser.assert.not.class(".container", "extra-class"); + }); + + it("Remove Class Invalid Element", async() => { + await utils.assertThrowsAsync(async() => { + await browser.removeClass(".not-an-element", "extra-class"); + }, `QueryError: [removeClass] Selector ".not-an-element" doesn't match any elements.`); + }); +}); diff --git a/tests/browser/incognito.test.js b/tests/browser/incognito.test.js index 2ec2e8da..b15dcf14 100644 --- a/tests/browser/incognito.test.js +++ b/tests/browser/incognito.test.js @@ -1,24 +1,28 @@ "use strict"; +const assert = require('assert'); const Wendigo = require('../..'); const configUrls = require('../config.json').urls; +// This test does not explicitly check if the browser is in incognito mode as it cannot be realiably detected on a web describe("Incognito", function() { - this.timeout(50000); + this.timeout(5000); - it("Not Incognito", async() => { - const browser = await Wendigo.createBrowser(); - await browser.open(configUrls.incognito); - await browser.wait(10); - await browser.assert.text("#check-text", "Not Incognito"); + it("Incognito Browser", async() => { + const browser = await Wendigo.createBrowser({incognito: true}); + await browser.open(configUrls.simple); + await browser.assert.text("p", "html_test"); + assert.strictEqual(browser.settings.incognito, true); + assert.strictEqual(browser.incognito, true); await browser.close(); }); - it("Incognito", async() => { - const browser = await Wendigo.createBrowser({incognito: true}); - await browser.open(configUrls.incognito); - await browser.wait(10); - await browser.assert.text("#check-text", "Incognito"); + it("Not Incognito Browser", async() => { + const browser = await Wendigo.createBrowser(); + await browser.open(configUrls.simple); + await browser.assert.text("p", "html_test"); + assert.strictEqual(browser.settings.incognito, false); + assert.strictEqual(browser.incognito, false); await browser.close(); }); }); diff --git a/tests/browser/open.test.js b/tests/browser/open.test.js index 8302ea31..6d5b76f9 100644 --- a/tests/browser/open.test.js +++ b/tests/browser/open.test.js @@ -20,8 +20,8 @@ describe("Open", function() { it("Open Fails", async() => { await utils.assertThrowsAsync(async() => { - await browser.open("not-a-page"); - }, `FatalError: [open] Failed to open "not-a-page". Protocol error (Page.navigate): Cannot navigate to invalid URL`); + await browser.open(`http://localhost:3433/not-a-page.html`); + }, `FatalError: [open] Failed to open "http://localhost:3433/not-a-page.html". net::ERR_CONNECTION_REFUSED at http://localhost:3433/not-a-page.html`); }); it("Before Open Fails", async() => { @@ -92,4 +92,10 @@ describe("Open", function() { }); await browser.assert.text(".qs", "test=foo"); }); + + it("Open Without Protocol", async() => { + await browser.open("localhost:3456/index.html"); + await browser.assert.url(configUrls.index); + await browser.close(); + }); }); diff --git a/tests/browser/select.test.js b/tests/browser/select.test.js index 34b5bdde..90f03507 100644 --- a/tests/browser/select.test.js +++ b/tests/browser/select.test.js @@ -64,6 +64,27 @@ describe("Select", function() { assert.strictEqual(selectResult[0], "Value 4"); }); + it("Select Option DOMElement", async() => { + const element = await browser.query("#normal-select"); + const selectResult = await browser.select(element, "value2"); + await browser.assert.value("#normal-select", "value2"); + const selectedOptions = await browser.selectedOptions("#normal-select"); + assert.strictEqual(selectedOptions.length, 1); + assert.strictEqual(selectedOptions[0], "value2"); + assert.strictEqual(selectResult.length, 1); + assert.strictEqual(selectResult[0], "value2"); + }); + + it("Select Option XPath", async() => { + const selectResult = await browser.select("//*[@id='normal-select']", "value2"); + await browser.assert.value("#normal-select", "value2"); + const selectedOptions = await browser.selectedOptions("#normal-select"); + assert.strictEqual(selectedOptions.length, 1); + assert.strictEqual(selectedOptions[0], "value2"); + assert.strictEqual(selectResult.length, 1); + assert.strictEqual(selectResult[0], "value2"); + }); + it("Select Multiple Values On Single Select", async() => { const selectResult = await browser.select("#normal-select", ["Value 4", "value2"]); await browser.assert.value("#normal-select", "value2"); diff --git a/tests/browser/set_attribute.test.js b/tests/browser/set_attribute.test.js new file mode 100644 index 00000000..ada83d70 --- /dev/null +++ b/tests/browser/set_attribute.test.js @@ -0,0 +1,59 @@ +"use strict"; + +const Wendigo = require('../..'); +const configUrls = require('../config.json').urls; +const utils = require('../test_utils'); + +describe("Set Attribute", function() { + this.timeout(5000); + + let browser; + before(async() => { + browser = await Wendigo.createBrowser(); + }); + + beforeEach(async() => { + await browser.open(configUrls.index); + }); + + after(async() => { + await browser.close(); + }); + + it("Set Attribute", async() => { + await browser.assert.attribute(".second-element", "title", "title"); + await browser.setAttribute(".second-element", "title", "title2"); + await browser.assert.attribute(".second-element", "title", "title2"); + }); + + it("Set Attribute DOMElement", async() => { + const element = await browser.query(".second-element"); + await browser.assert.attribute(".second-element", "title", "title"); + await browser.setAttribute(element, "title", "title2"); + await browser.assert.attribute(".second-element", "title", "title2"); + }); + + it("Set Attribute XPath", async() => { + await browser.assert.attribute(".second-element", "title", "title"); + await browser.setAttribute("//*[contains(@class, 'second-element')]", "title", "title2"); + await browser.assert.attribute(".second-element", "title", "title2"); + }); + + it("Set Attribute Invalid Element", async() => { + await utils.assertThrowsAsync(async() => { + await browser.setAttribute(".not-an-element", "extra-class", "value"); + }, `QueryError: [setAttribute] Element ".not-an-element" not found.`); + }); + + it("Set Attribute To Empty String", async() => { + await browser.assert.attribute(".second-element", "title", "title"); + await browser.setAttribute(".second-element", "title", ""); + await browser.assert.attribute(".second-element", "title", ""); + }); + + it("Remove Attribute", async() => { + await browser.assert.attribute(".second-element", "title", "title"); + await browser.setAttribute(".second-element", "title", null); + await browser.assert.attribute(".second-element", "title", null); + }); +}); diff --git a/tests/browser/wait_for.test.js b/tests/browser/wait_for.test.js index da23230f..029ae954 100644 --- a/tests/browser/wait_for.test.js +++ b/tests/browser/wait_for.test.js @@ -132,6 +132,15 @@ describe("Wait For", function() { }, `TimeoutError: [waitForText] Waiting for text "Off", timeout of 10ms exceeded.`); }); + it("Wait For Text With Quotes Timeout", async() => { + await browser.open(configUrls.click); + await browser.assert.text("#switch", "On"); + await browser.click(".btn2"); + await utils.assertThrowsAsync(async() => { + await browser.waitForText(`"'Off`, 10); + }, `TimeoutError: [waitForText] Waiting for text ""'Off", timeout of 10ms exceeded.`); + }); + it("Wait For Url Regexp", async() => { await browser.open(configUrls.index); await browser.click("a"); diff --git a/tests/browser_modules/browser_cookies.test.js b/tests/browser_modules/browser_cookies.test.js index c411a799..e9382f32 100644 --- a/tests/browser_modules/browser_cookies.test.js +++ b/tests/browser_modules/browser_cookies.test.js @@ -72,6 +72,27 @@ describe("Cookies", function() { }, `Error: [cookies.delete] Delete cookie name missing`); }); + it("Delete Cookie Of Different Domain", async() => { + await browser.cookies.set("android", { + value: "marvin", + domain: "not-localhost" + }); + + await browser.cookies.delete({ + name: "android" + }); + + const cookie = await browser.cookies.get("android", "http://not-localhost/path"); + assert.strictEqual(cookie.value, "marvin"); + await browser.cookies.delete({ + name: "android", + domain: "not-localhost" + }); + + const cookie2 = await browser.cookies.get("android", "http://not-localhost/path"); + assert.strictEqual(cookie2, undefined); + }); + it("Delete Multiple Cookies", async() => { await browser.cookies.set("android", "marvin"); await browser.cookies.delete(["username", "android"]); @@ -99,4 +120,16 @@ describe("Cookies", function() { const cookies = await browser.cookies.all(); assert.strictEqual(Object.keys(cookies).length, 0); }); + + it("Set And Get Cookies From Different Domain", async() => { + await browser.cookies.set("android", { + value: "marvin", + domain: "not-localhost" + }); + + const currentUrlValue = await browser.cookies.get("android"); + assert.strictEqual(currentUrlValue, undefined); + const cookie = await browser.cookies.get("android", "http://not-localhost/path"); + assert.strictEqual(cookie.value, "marvin"); + }); }); diff --git a/tests/browser_modules/request_mocker.test.js b/tests/browser_modules/request_mocker.test.js index dba3b43f..de9f452c 100644 --- a/tests/browser_modules/request_mocker.test.js +++ b/tests/browser_modules/request_mocker.test.js @@ -345,9 +345,7 @@ describe("Requests Mocker", function() { }); const mock = await browser.requests.mock(/api/, response); await browser.clickText("click me"); - // await browser.wait(500); await mock.waitUntilCalled(); - // console.log(browser.requests.all()); await browser.assert.text("#result", "MOCK"); }); diff --git a/tests/config.json b/tests/config.json index dc6514f3..f67cb23d 100644 --- a/tests/config.json +++ b/tests/config.json @@ -17,7 +17,6 @@ "console": "http://localhost:3456/console.html", "scroll": "http://localhost:3456/scroll.html", "webworker": "http://localhost:3456/webworker.html", - "incognito": "http://localhost:3456/incognito.html", "iframes": "http://localhost:3456/iframes.html", "redirect": "http://localhost:3456/redirect", "drag": "http://localhost:3456/drag.html", diff --git a/tests/dummy_server/index.js b/tests/dummy_server/index.js index e1129964..85e19d0a 100644 --- a/tests/dummy_server/index.js +++ b/tests/dummy_server/index.js @@ -13,7 +13,6 @@ app.use((req, res, next) => { // To avoid 304 }); app.use("/", express.static(path.join(__dirname, "static"))); - app.get("/api", (req, res) => { if (req.query.query === "hi2") res.json({result: "QUERY"}); else res.json({result: "DUMMY"}); diff --git a/tests/dummy_server/static/incognito.html b/tests/dummy_server/static/incognito.html deleted file mode 100644 index 26b69ca6..00000000 --- a/tests/dummy_server/static/incognito.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Incognito Test - - - - -

On

- - - - -