diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 0ba029be..b70ee0c6 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -19,9 +19,7 @@ jobs: cd tests/integration/docker_test docker-compose up -d docker exec drivers deno test -A --config tsconfig.json tests/integration - docker exec drivers deno test -A --config tsconfig.json tests/unit/chrome_client_test.ts - docker exec drivers deno test -A --config tsconfig.json tests/unit/firefox_client_test.ts - docker exec drivers deno test -A --config tsconfig.json tests/unit/mod_test.ts + docker exec drivers deno test -A --config tsconfig.json tests/unit tests: @@ -44,9 +42,7 @@ jobs: - name: Run Unit Tests run: | - deno test -A --config tsconfig.json tests/unit/chrome_client_test.ts - deno test -A --config tsconfig.json tests/unit/firefox_client_test.ts - deno test -A --config tsconfig.json tests/unit/mod_test.ts + deno test -A --config tsconfig.json tests/unit linter: # Only one OS is required since fmt is cross platform diff --git a/examples/a.ts b/examples/a.ts deleted file mode 100644 index ab7ddb16..00000000 --- a/examples/a.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ChromeClient } from "../mod.ts"; - -const Sinco = await ChromeClient.build(); -await Sinco.goTo("https://drash.land"); -//await Sinco.evaluatePage(`document.querySelector('a').getBoundingClientRect()`) -await Sinco.evaluatePage(() => { - return window.location.href; -}); -await Sinco.goTo("https://deno.land"); -//await Sinco.evaluatePage(`document.querySelector('a').getBoundingClientRect()`) -console.log( - await Sinco.evaluatePage(() => { - return window.location.href; - }), -); -await Sinco.done(); diff --git a/mod.ts b/mod.ts index 0c9dec57..3e7c13d7 100644 --- a/mod.ts +++ b/mod.ts @@ -1,18 +1,46 @@ -import { ChromeClient } from "./src/chrome_client.ts"; -import { FirefoxClient } from "./src/firefox_client.ts"; -import { BuildOptions } from "./src/client.ts"; +import { Client } from "./src/client.ts"; +import { BuildOptions, Cookie, ScreenshotOptions } from "./src/interfaces.ts"; +import type { Browsers } from "./src/types.ts"; +import { getChromeArgs, getFirefoxArgs } from "./src/utility.ts"; -export { ChromeClient, FirefoxClient }; - -type Browsers = "firefox" | "chrome"; +export type { BuildOptions, Cookie, ScreenshotOptions }; export async function buildFor( browser: Browsers, - options?: BuildOptions, -): Promise { - if (browser === "firefox") { - return await FirefoxClient.build(options); + options: BuildOptions = { + hostname: "localhost", + debuggerPort: 9292, + binaryPath: undefined, + }, +): Promise { + if (!options.debuggerPort) options.debuggerPort = 9292; + if (!options.hostname) options.hostname = "localhost"; + if (browser === "chrome") { + const args = getChromeArgs(options.debuggerPort, options.binaryPath); + return await Client.create( + args, + { + hostname: options.hostname, + port: options.debuggerPort, + }, + browser, + undefined, + ); } else { - return await ChromeClient.build(options); + const tmpDirName = Deno.makeTempDirSync(); + const args = getFirefoxArgs( + tmpDirName, + options.debuggerPort, + options.binaryPath, + ); + return await Client.create( + args, + { + hostname: options.hostname, + port: options.debuggerPort, + }, + browser, + tmpDirName, + ); } } diff --git a/src/chrome_client.ts b/src/chrome_client.ts deleted file mode 100644 index 5938931d..00000000 --- a/src/chrome_client.ts +++ /dev/null @@ -1,84 +0,0 @@ -// https://peter.sh/experiments/chromium-command-line-switches/ -// https://chromedevtools.github.io/devtools-protocol/tot/Network/ - -import { BuildOptions, Client } from "./client.ts"; -import { existsSync } from "./utility.ts"; - -/** - * Gets the full path to the chrome executable on the users filesystem - * - * @returns The path to chrome - */ -export function getChromePath(): string { - const paths = { - // deno-lint-ignore camelcase - windows_chrome_exe: - "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", - // deno-lint-ignore camelcase - windows_chrome_exe_x86: - "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", - darwin: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - linux: "/usr/bin/google-chrome", - }; - let chromePath = ""; - switch (Deno.build.os) { - case "darwin": - chromePath = paths.darwin; - break; - case "windows": - if (existsSync(paths.windows_chrome_exe)) { - chromePath = paths.windows_chrome_exe; - break; - } - if (existsSync(paths.windows_chrome_exe_x86)) { - chromePath = paths.windows_chrome_exe_x86; - break; - } - - throw new Error( - "Cannot find path for chrome in windows. Submit an issue if you encounter this error", - ); - case "linux": - chromePath = paths.linux; - break; - } - return chromePath; -} - -export class ChromeClient extends Client { - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Entry point for creating a headless chrome browser. - * - * @param buildOptions - Any extra options you wish to provide to customise how the headless browser sub process is ran - * - hostname: Defaults to localhost - * - port: Defaults to 9293 - * - * @returns An instance of ChromeClient, that is now ready. - */ - public static async build(options: BuildOptions = {}): Promise { - // Setup build options - if (!options.debuggerPort) { - options.debuggerPort = 9292; - } - if (!options.hostname) { - options.hostname = "localhost"; - } - - // Create the sub process - const args = [ - options.binaryPath || getChromePath(), - "--headless", - "--remote-debugging-port=" + options.debuggerPort, - "--disable-gpu", - "--no-sandbox", - ]; - return await Client.create(args, { - hostname: options.hostname, - port: options.debuggerPort, - }, "chrome"); - } -} diff --git a/src/client.ts b/src/client.ts index aca36e89..a66bea03 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,138 +1,73 @@ -import { - assertEquals, - Deferred, - deferred, - Protocol, - readLines, -} from "../deps.ts"; -import { existsSync, generateTimestamp } from "./utility.ts"; - -export interface BuildOptions { - debuggerPort?: number; // The port to start the debugger on for Chrome, so that we can connect to it. Defaults to 9292 - hostname?: string; // The hostname the browser process starts on. If on host machine, this will be "localhost", if in docker, it will bee the container name. Defaults to localhost - binaryPath?: string; //The Full Path to the browser binary. If using an alternative chromium based browser, this field is necessary. -} - -interface MessageResponse { // For when we send an event to get one back, eg running a JS expression - id: number; - result?: Record; // Present on success, OR for example if we use goTo and the url doesnt exist (in firefox) - error?: unknown; // Present on error -} - -interface NotificationResponse { // Not entirely sure when, but when we send the `Network.enable` method - method: string; - params: unknown; -} - +import { Protocol as ProtocolClass } from "./protocol.ts"; +import { deferred, Protocol as ProtocolTypes, readLines } from "../deps.ts"; +import { Page } from "./page.ts"; +import type { Browsers } from "./types.ts"; + +// https://stackoverflow.com/questions/50395719/firefox-remote-debugging-with-websockets +// FYI for reference, we can connect using websockets, but severe lack of documentation gives us NO info on how to proceed after: +/** + * $ --profile --headless --remote-debugging-port 1448 + * ```ts + * const res = await fetch("http://localhost:1448/json/list") + * const json = await res.json() + * consy url = json[json.length - 1]["webSocketDebuggerUrl"] + * const c = new WebSocket(url) + * ``` + */ + +/** + * A way to interact with the headless browser instance. + * + * This is the entrypoint API to creating and interacting with the chrome or + * firefox browser. It allows: + * - Starting the headless browser (subprocess) + * - Methods to interact with the client such as: + * - Visiting a page and returning a `Page` class + * to interact with that page. + * + * @example + * ```js + * const client = await Client.create([ + * "chrome", "--headless", + * ], { + * hostname: "localhost", + * port: 1234 + * }, "chrome", undefined); + * const page = await client.goTo("https://drash.land"); + * ``` + */ export class Client { - /** - * The sub process that runs headless chrome - */ - private readonly browser_process: Deno.Process; - - /** - * Our web socket connection to the remote debugging port - */ - private readonly socket: WebSocket; - - /** - * A counter that acts as the message id we use to send as part of the event data through the websocket - */ - private next_message_id = 1; - - private frame_id: string; - - /** - * To keep hold of promises waiting for a notification from the websocket - */ - private notification_resolvables: Map> = new Map(); - - /** - * Track if we've closed the sub process, so we dont try close it when it already has been - */ - private browser_process_closed = false; - - /** - * To keep hold of our promises waiting for messages from the websocket - */ - private resolvables: Map> = new Map(); - - private browser: "firefox" | "chrome"; - - /** - * Only if the browser is firefox, is this present. - * This is the path to the directory that firefox uses - * to write a profile - */ - private firefox_profile_path: string | undefined = undefined; - + #protocol: ProtocolClass; constructor( - socket: WebSocket, - browserProcess: Deno.Process, - browser: "firefox" | "chrome", - frameId: string, - firefoxProfilePath?: string, + protocol: ProtocolClass, ) { - this.browser = browser; - this.socket = socket; - this.browser_process = browserProcess; - this.firefox_profile_path = firefoxProfilePath; - this.frame_id = frameId; - // Register on message listener - this.socket.onmessage = (msg) => { - const data = JSON.parse(msg.data); - if (data.method === "Page.frameStartedLoading") { - this.frame_id = data.params.frameId; - } - this.handleSocketMessage(data); - }; + this.#protocol = protocol; } /** - * Asserts a given url matches the current - * - * @param expectedUrl - The expected url, eg `https://google.com/hello` - */ - public async assertUrlIs(expectedUrl: string): Promise { - const actualUrl = await this.evaluatePage(`window.location.href`); - if (actualUrl !== expectedUrl) { // Before we know the test will fail, close everything - await this.done(); - } - assertEquals(actualUrl, expectedUrl); - } - - /** - * Check if the given text exists on the dom - * - * @param text - The text to check for + * Close/stop the sub process, and close the ws connection. Must be called when finished with all your testing */ - public async assertSee(text: string): Promise { - const command = `document.body.innerText.indexOf('${text}') >= 0`; - const res = await this.sendWebSocketMessage("Runtime.evaluate", { - expression: command, - }) as { // Tried and tested - result: { - type: "boolean"; - value: boolean; - }; - }; - const exists = res.result.value; - if (exists !== true) { // We know it's going to fail, so before an assertion error is thrown, cleanupup - await this.done(); - } - assertEquals(exists, true); + public async done() { + await this.#protocol.done(); } /** * Go to a specific page * * @param urlToVisit - The page to go to + * + * @returns A page instance, with methods to help you directly interact with the page */ - public async goTo(urlToVisit: string): Promise { + public async goTo(urlToVisit: string): Promise { const method = "Page.loadEventFired"; - this.notification_resolvables.set(method, deferred()); - const notificationPromise = this.notification_resolvables.get(method); - const res = await this.sendWebSocketMessage( + this.#protocol.notification_resolvables.set(method, deferred()); + const notificationPromise = this.#protocol.notification_resolvables.get( + method, + ); + const res = await this.#protocol.sendWebSocketMessage< + ProtocolTypes.Page.NavigateRequest, + ProtocolTypes.Page.NavigateResponse + >( "Page.navigate", { url: urlToVisit, @@ -140,288 +75,13 @@ export class Client { ); await notificationPromise; if (res.errorText) { - await this.done(); - throw new Error( + await this.#protocol.done( `${res.errorText}: Error for navigating to page "${urlToVisit}"`, ); } + return new Page(this.#protocol); } - /** - * Clicks a button with the given selector - * - * await this.click("#username"); - * await this.click('button[type="submit"]') - * - * @param selector - The tag name, id or class - */ - public async click(selector: string): Promise { - const command = `document.querySelector('${selector}').click()`; - const result = await this.sendWebSocketMessage< - Protocol.Runtime.AwaitPromiseResponse - >("Runtime.evaluate", { - expression: command, - }); - await this.checkForErrorResult(result, command); - } - - /** - * Invoke a function or string expression on the current frame. - * - * @param pageCommand - The function to be called or the line of code to execute. - * - * @returns The result of the evaluation - */ - public async evaluatePage( - pageCommand: (() => unknown) | string, - // As defined by the protocol, the `value` is `any` - // deno-lint-ignore no-explicit-any - ): Promise { - if (typeof pageCommand === "string") { - const result = await this.sendWebSocketMessage< - Protocol.Runtime.AwaitPromiseResponse - >("Runtime.evaluate", { - expression: pageCommand, - }); - await this.checkForErrorResult(result, pageCommand); - return result.result.value; - } - - if (typeof pageCommand === "function") { - const { executionContextId } = await this.sendWebSocketMessage( - "Page.createIsolatedWorld", - { - frameId: this.frame_id, - }, - ); - - const res = await this.sendWebSocketMessage< - Protocol.Runtime.AwaitPromiseResponse - >( - "Runtime.callFunctionOn", - { - functionDeclaration: pageCommand.toString(), - executionContextId: executionContextId, - returnByValue: true, - awaitPromise: true, - userGesture: true, - }, - ); - await this.checkForErrorResult(res, pageCommand.toString()); - return res.result.value; - } - } - - /** - * Wait for the page to change. Can be used with `click()` if clicking a button or anchor tag that redirects the user - */ - public async waitForPageChange(): Promise { - const method = "Page.loadEventFired"; - this.notification_resolvables.set(method, deferred()); - const notificationPromise = this.notification_resolvables.get(method); - await notificationPromise; - this.notification_resolvables.delete(method); - } - - /** - * Gets the text for the given selector - * Must be an input element - * - * @param selector - eg input[type="submit"] or #submit - * - * @throws When: - * - Error with the element (using selector) - * - * @returns The text inside the selector, eg could be "" or "Edward" - */ - public async getInputValue(selector: string): Promise { - const command = `document.querySelector('${selector}').value`; - const res = await this.sendWebSocketMessage< - Protocol.Runtime.AwaitPromiseResponse - >("Runtime.evaluate", { - expression: command, - }); - await this.checkForErrorResult(res, command); - const type = res.result.type; - if (type === "undefined") { // not an input elem - await this.done(); - throw new Error( - `${selector} is either not an input element, or does not exist`, - ); - } - // Tried and tested, value and type are a string and `res.result.value` definitely exists at this stage - const value = res.result.value; - return value || ""; - } - - /** - * Close/stop the sub process, and close the ws connection. Must be called when finished with all your testing - * - * @param errMsg - If supplied, will finally throw an error with the message after closing all processes - */ - public async done(errMsg?: string): Promise { - // Say a user calls an assertion method, and then calls done(), we make sure that if - // the subprocess is already closed, dont try close it again - if (this.browser_process_closed === true) { - return; - } - const clientIsClosed = deferred(); - this.socket.onclose = () => clientIsClosed.resolve(); - // cloing subprocess will also close the ws endpoint - this.browser_process.stderr!.close(); - this.browser_process.stdout!.close(); - this.browser_process.close(); - this.browser_process_closed = true; - // Zombie processes is a thing with Windows, the firefox process on windows - // will not actually be closed using the above. - // Related Deno issue: https://github.com/denoland/deno/issues/7087 - if (this.browser === "firefox" && Deno.build.os === "windows") { - const p = Deno.run({ - cmd: ["taskkill", "/F", "/IM", "firefox.exe"], - stdout: "null", - stderr: "null", - }); - await p.status(); - p.close(); - } - await clientIsClosed; // done AFTER the above conditional because the process is still running, so the client is never closed - if (this.firefox_profile_path) { - // On windows, this block is annoying. We either get a perm denied or - // resource is in use error (classic windows). So what we're doing here is - // even if one of those errors are thrown, keep trying because what i've (ed) - // found is, it seems to need a couple seconds to realise that the dir - // isnt being used anymore. The loop shouldn't be needed for macos/unix though, so - // it will likely only run once. - while (existsSync(this.firefox_profile_path)) { - try { - Deno.removeSync(this.firefox_profile_path, { recursive: true }); - } catch (_e) { - // Just try removing again - } - } - } - if (errMsg) { - throw new Error(errMsg); - } - } - - /** - * Set a cookie for the `url`. Will be passed across 'sessions' based - * from the `url` - * - * @param name - Name of the cookie, eg X-CSRF-TOKEN - * @param value - Value to assign to the cookie name, eg "some cryptic token" - * @param url - The domain to assign the cookie to, eg "https://drash.land" - * - * @example - * ```ts - * await Sinco.setCookie("X-CSRF-TOKEN", "abc123", "https://drash.land") - * const result = await Sinco.evaluatePage(`document.cookie`) // "X-CSRF-TOKEN=abc123" - * ``` - */ - public async setCookie( - name: string, - value: string, - url: string, - ): Promise { - const res = await this.sendWebSocketMessage("Network.setCookie", { - name, - value, - url, - }) as ({ // if error response. only encountered when I (ed) tried to send a message without passing in a url prop - code: number; // eg -32602 - message: string; // eg "At least one of the url or domain needs to be specified" - } | { // if success response - success: true; - }); - if ("success" in res === false && "message" in res) { - await this.done(res.message); - } - } - - /** - * Type into an input element, by the given selector - * - * - * - * await this.type('input[name="city"]', "Stockholm") - * - * @param selector - The value for the name attribute of the input to type into - * @param value - The value to set the input to - */ - public async type(selector: string, value: string): Promise { - const command = `document.querySelector('${selector}').value = "${value}"`; - await this.evaluatePage(command); - } - - /** - * Take a screenshot of the page and save it to `filename` in `path` folder, with a `format` and `quality` (jpeg format only) - * If `selector` is passed in, it will take a screenshot of only that element - * and its children as opposed to the whole page. - * - * @param path - The path of where to save the screenshot to - * @param options - options - * @param options.filename - Name to be given to the screenshot. Optional - * @param options.selector - Screenshot the given selector instead of the full page. Optional - * @param options.format - The Screenshot format(and hence extension). Allowed values are "jpeg" and "png" - Optional - * @param options.quality - The image quality from 0 to 100, default 80. Applicable only if no format provided or format is "jpeg" - Optional - */ - public async takeScreenshot( - path: string, - options?: { - selector?: string; - fileName?: string; - format?: "jpeg" | "png"; - quality?: number; - }, - ): Promise { - if (!existsSync(path)) { - await this.done(); - throw new Error(`The provided folder path - ${path} doesn't exist`); - } - const ext = options?.format ?? "jpeg"; - const clip = (options?.selector) - ? await this.getViewport(options.selector) - : undefined; - - if (options?.quality && Math.abs(options.quality) > 100 && ext == "jpeg") { - await this.done(); - throw new Error("A quality value greater than 100 is not allowed."); - } - - //Quality should defined only if format is jpeg - const quality = (ext == "jpeg") - ? ((options?.quality) ? Math.abs(options.quality) : 80) - : undefined; - - const res = await this.sendWebSocketMessage( - "Page.captureScreenshot", - { - format: ext, - quality: quality, - clip: clip, - captureBeyondViewport: true, - }, - ) as { - data: string; - }; - - //Writing the Obtained Base64 encoded string to image file - const fName = `${path}/${ - options?.fileName?.replaceAll(/.jpeg|.jpg|.png/g, "") ?? - generateTimestamp() - }.${ext}`; - const B64str = res.data; - const u8Arr = Uint8Array.from(atob(B64str), (c) => c.charCodeAt(0)); - try { - Deno.writeFileSync(fName, u8Arr); - } catch (e) { - await this.done(); - throw new Error(e.message); - } - - return fName; - } /** * Wait for anchor navigation. Usually used when typing into an input field */ @@ -432,136 +92,23 @@ export class Client { // delete this.notification_resolvables["Page.navigatedWithinDocument"]; // } - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * This method is used internally to calculate the element Viewport (Dimensions) - * executes getBoundingClientRect of the obtained element - * @param selector - The selector for the element to capture - * @returns ViewPort object - Which contains the dimensions of the element captured - */ - private async getViewport(selector: string): Promise { - const res = await this.sendWebSocketMessage< - Protocol.Runtime.AwaitPromiseResponse - >("Runtime.evaluate", { - expression: - `JSON.stringify(document.querySelector('${selector}').getBoundingClientRect())`, - }); - if ( - "exceptionDetails" in res || - (res.result)?.subtype - ) { - await this.done(); - this.checkForErrorResult( - res, - `document.querySelector('${selector}').getBoundingClientRect()`, - ); - } - - const values = JSON.parse((res.result as { value: string }).value); - return { - x: values.x, - y: values.y, - width: values.width, - height: values.height, - scale: 2, - }; - } - - private handleSocketMessage(message: MessageResponse | NotificationResponse) { - if ("id" in message) { // message response - const resolvable = this.resolvables.get(message.id); - if (resolvable) { - if ("result" in message) { // success response - if ("errorText" in message.result!) { - const r = this.notification_resolvables.get("Page.loadEventFired"); - if (r) { - r.resolve(); - } - } - resolvable.resolve(message.result); - } - if ("error" in message) { // error response - resolvable.resolve(message.error); - } - } - } - if ("method" in message) { // Notification response - const resolvable = this.notification_resolvables.get(message.method); - if (resolvable) { - resolvable.resolve(); - } - } - } - /** - * Main method to handle sending messages/events to the websocket endpoint. + * Creates the instant and protocol to interact with * - * @param method - Any DOMAIN, see sidebar at https://chromedevtools.github.io/devtools-protocol/tot/, eg Runtime.evaluate, or DOM.getDocument - * @param params - Parameters required for the domain method + * @param buildArgs - Sub process args, should be ones to run chrome + * @param wsOptions - Hostname and port to run the websocket server on + * @param browser - Which browser we are building + * @param firefoxProfilePath - If firefox, the path to the temporary profile location * - * @returns + * @returns A client instance, ready to be used */ - private async sendWebSocketMessage( - method: string, - params?: Record, - ): Promise { - const data: { - id: number; - method: string; - params?: Record; - } = { - id: this.next_message_id++, - method: method, - }; - if (params) data.params = params; - const promise = deferred(); - this.resolvables.set(data.id, promise); - this.socket.send(JSON.stringify(data)); - const result = await promise; - this.resolvables.delete(data.id); - return result; - } - - /** - * Checks if the result is an error - * - * @param result - The DOM result response, after writing to stdin and getting by stdout of the process - * @param commandSent - The command sent to trigger the result - */ - private async checkForErrorResult( - result: Protocol.Runtime.AwaitPromiseResponse, - commandSent: string, - ): Promise { - const exceptionDetail = result.exceptionDetails; - if (!exceptionDetail) { - return; - } - if (exceptionDetail.text && !exceptionDetail.exception) { // specific for firefox - await this.done(); - throw new Error(exceptionDetail.text); - } - const errorMessage = exceptionDetail.exception!.description ?? - exceptionDetail.text; - if (errorMessage.includes("SyntaxError")) { // a syntax error - const message = errorMessage.replace("SyntaxError: ", ""); - await this.done(); - throw new SyntaxError(message + ": `" + commandSent + "`"); - } else { // any others, unsure what they'd be - await this.done(); - throw new Error(`${errorMessage}: "${commandSent}"`); - } - } - - protected static async create( + static async create( buildArgs: string[], wsOptions: { hostname: string; port: number; }, - browser: "firefox" | "chrome", + browser: Browsers, firefoxProfilePath?: string, ): Promise { const browserProcess = Deno.run({ @@ -578,7 +125,7 @@ export class Client { } break; } - const { debugUrl: wsUrl, frameId } = await Client.getWebSocketInfo( + const { debugUrl: wsUrl, frameId } = await ProtocolClass.getWebSocketInfo( wsOptions.hostname, wsOptions.port, ); @@ -586,47 +133,15 @@ export class Client { const promise = deferred(); websocket.onopen = () => promise.resolve(); await promise; - const TempClient = new Client(websocket, browserProcess, browser, frameId); - await TempClient.sendWebSocketMessage("Page.enable"); - await TempClient.sendWebSocketMessage("Runtime.enable"); - return new Client( + const protocol = new ProtocolClass( websocket, browserProcess, browser, frameId, firefoxProfilePath, ); - } - - /** - * Gets the websocket url we use to create a ws client with. - * Requires the headless chrome process to be running, as - * this is what actually starts the remote debugging url - * - * @param hostname - The hostname to fetch from - * @param port - The port for the hostname to fetch from - * - * @returns The url to connect to - */ - private static async getWebSocketInfo( - hostname: string, - port: number, - ): Promise<{ debugUrl: string; frameId: string }> { - let debugUrl = ""; - let frameId = ""; - while (debugUrl === "") { - try { - const res = await fetch(`http://${hostname}:${port}/json/list`); - const json = await res.json(); - debugUrl = json[0]["webSocketDebuggerUrl"]; - frameId = json[0]["id"]; - } catch (_err) { - // do nothing, loop again until the endpoint is ready - } - } - return { - debugUrl, - frameId, - }; + await protocol.sendWebSocketMessage("Page.enable"); + await protocol.sendWebSocketMessage("Runtime.enable"); + return new Client(protocol); } } diff --git a/src/element.ts b/src/element.ts new file mode 100644 index 00000000..708008f3 --- /dev/null +++ b/src/element.ts @@ -0,0 +1,59 @@ +import { Page } from "./page.ts"; + +/** + * A class to represent an element on the page, providing methods + * to action on that element + */ +export class Element { + /** + * The css selector for the element + */ + public selector: string; // eg "#user" or "div > #name" or "//h1" + + /** + * How we select the element + */ + public method: "document.querySelector" | "$x"; + + /** + * The page this element belongs to + */ + private page: Page; + + constructor( + method: "document.querySelector" | "$x", + selector: string, + page: Page, + ) { + this.page = page; + this.selector = selector; + this.method = method; + } + + /** + * Get the value of this element, or set the value + * + * @param newValue - If not passed, will return the value, else will set the value + * + * @returns The value if setting, else void if not + */ + public async value(newValue?: string) { + if (!newValue) { + return await this.page.evaluate( + `${this.method}('${this.selector}').value`, + ); + } + await this.page.evaluate( + `${this.method}('${this.selector}').value = \`${newValue}\``, + ); + } + + /** + * Click the element + */ + public async click(): Promise { + await this.page.evaluate( + `${this.method}('${this.selector}').click()`, + ); + } +} diff --git a/src/firefox_client.ts b/src/firefox_client.ts deleted file mode 100644 index 826b2642..00000000 --- a/src/firefox_client.ts +++ /dev/null @@ -1,96 +0,0 @@ -// https://stackoverflow.com/questions/50395719/firefox-remote-debugging-with-websockets -// FYI for reference, we can connect using websockets, but severe lack of documentation gives us NO info on how to proceed after: -/** - * $ --profile --headless --remote-debugging-port 1448 - * ```ts - * const res = await fetch("http://localhost:1448/json/list") - * const json = await res.json() - * consy url = json[json.length - 1]["webSocketDebuggerUrl"] - * const c = new WebSocket(url) - * ``` - */ - -import { BuildOptions, Client } from "./client.ts"; - -export const defaultBuildOptions = { - hostname: Deno.build.os === "windows" ? "127.0.0.1" : "localhost", - debuggerServerPort: 9293, - defaultUrl: "https://developer.mozilla.org/", -}; - -/** - * Get full path to the firefox binary on the user'ss filesystem. - * Thanks to [caspervonb](https://github.com/caspervonb/deno-web/blob/master/browser.ts) - * - * @returns the path - */ -export function getFirefoxPath(): string { - switch (Deno.build.os) { - case "darwin": - return "/Applications/Firefox.app/Contents/MacOS/firefox"; - case "linux": - return "/usr/bin/firefox"; - case "windows": - return "C:\\Program Files\\Mozilla Firefox\\firefox.exe"; - } -} - -/** - * @example - * - * const Firefox = await FirefoxClient.build() - * await Firefox. - */ -export class FirefoxClient extends Client { - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Entry point for creating a headless firefox browser. - * Creates a dev profile to be used by Firefox, creates the headless browser and sets up the connection to - * - * @param buildOptions - Any extra options you wish to provide to customise how the headless browser sub process is ran - * - hostname: Defaults to 0.0.0.0 for macos/linux, 127.0.0.1 for windows - * - port: Defaults to 9293 - * - * @returns An instance of FirefoxClient, that is now ready. - */ - public static async build( - buildOptions: BuildOptions = {}, - ): Promise { - // Setup the options to defaults if required - if (!buildOptions.hostname) { - buildOptions.hostname = defaultBuildOptions.hostname; - } - if (!buildOptions.debuggerPort) { - buildOptions.debuggerPort = defaultBuildOptions.debuggerServerPort; - } - - // Create the profile the browser will use. Create a test one so we can enable required options to enable communication with it - const tmpDirName = Deno.makeTempDirSync(); - - // Create the arguments we will use when spawning the headless browser - const args = [ - buildOptions.binaryPath || getFirefoxPath(), - "--start-debugger-server", - buildOptions.debuggerPort.toString(), - "-headless", - "--remote-debugging-port", - buildOptions.debuggerPort.toString(), - "-profile", - tmpDirName, - "about:blank", - ]; - // Create the sub process to start the browser - return await Client.create( - args, - { - hostname: buildOptions.hostname, - port: buildOptions.debuggerPort, - }, - "firefox", - tmpDirName, - ); - } -} diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 00000000..e5b34a44 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,28 @@ +export interface BuildOptions { + /** The port that the WebSocket server should run on when starting the headless client */ + debuggerPort?: number; + /** The hostname that the WebSocket server starts on when starting the headless client */ + hostname?: string; + /** The path to the binary of the browser executable, such as specifying an alternative chromium browser */ + binaryPath?: string; +} + +export interface ScreenshotOptions { + /** Name to be given to the screenshot. Optional */ + selector?: string; + /** Screenshot the given selector instead of the full page. Optional */ + fileName?: string; + /** The Screenshot format(and hence extension). Allowed values are "jpeg" and "png" - Optional */ + format?: "jpeg" | "png"; + /** The image quality from 0 to 100, default 80. Applicable only if no format provided or format is "jpeg" - Optional */ + quality?: number; +} + +export type Cookie = { + /** The name of the cookie */ + name: string; + /** The value to set the cookie to */ + value: string; + /** The domain that the cookie shoudl belong to */ + url: string; +}; diff --git a/src/page.ts b/src/page.ts new file mode 100644 index 00000000..d6ed5312 --- /dev/null +++ b/src/page.ts @@ -0,0 +1,291 @@ +import { assertEquals, deferred, Protocol } from "../deps.ts"; +import { existsSync, generateTimestamp } from "./utility.ts"; +import { Element } from "./element.ts"; +import { Protocol as ProtocolClass } from "./protocol.ts"; +import { Cookie, ScreenshotOptions } from "./interfaces.ts"; + +/** + * A representation of the page the client is on, allowing the client to action + * on it, such as setting cookies, or selecting elements, or interacting with localstorage etc + */ +export class Page { + readonly #protocol: ProtocolClass; + + constructor(protocol: ProtocolClass) { + this.#protocol = protocol; + } + + /** + * Either get all cookies for the page, or set a cookie + * + * @param newCookie - Only required if you want to set a cookie + * + * @returns All cookies for the page if no parameter is passed in, else an empty array + */ + public async cookie( + newCookie?: Cookie, + ): Promise { + if (!newCookie) { + const result = await this.#protocol.sendWebSocketMessage< + Protocol.Network.GetCookiesRequest, + Protocol.Network.GetCookiesResponse + >("Network.getCookies"); + return result.cookies; + } + await this.#protocol.sendWebSocketMessage< + Protocol.Network.SetCookieRequest, + Protocol.Network.SetCookieResponse + >("Network.setCookie", { + name: newCookie.name, + value: newCookie.value, + url: newCookie.url, + }); + return []; + } + + /** + * Either get the href/url for the page, or set the location + * + * @param newLocation - Only required if you want to set the location + * + * @example + * ```js + * const location = await page.location() // "https://drash.land" + * ``` + * + * @returns The location for the page if no parameter is passed in, else an empty string + */ + public async location(newLocation?: string): Promise { + if (!newLocation) { + const targets = await this.#protocol.sendWebSocketMessage< + null, + Protocol.Target.GetTargetsResponse + >("Target.getTargets"); + return targets.targetInfos[0].url; + } + const method = "Page.loadEventFired"; + this.#protocol.notification_resolvables.set(method, deferred()); + const notificationPromise = this.#protocol.notification_resolvables.get( + method, + ); + const res = await this.#protocol.sendWebSocketMessage< + Protocol.Page.NavigateRequest, + Protocol.Page.NavigateResponse + >( + "Page.navigate", + { + url: newLocation, + }, + ); + await notificationPromise; + if (res.errorText) { + await this.#protocol.done( + `${res.errorText}: Error for navigating to page "${newLocation}"`, + ); + } + return ""; + } + + /** + * Invoke a function or string expression on the current frame. + * + * @param pageCommand - The function to be called or the line of code to execute. + * + * @returns The result of the evaluation + */ + async evaluate( + pageCommand: (() => unknown) | string, + // As defined by the #protocol, the `value` is `any` + // deno-lint-ignore no-explicit-any + ): Promise { + if (typeof pageCommand === "string") { + const result = await this.#protocol.sendWebSocketMessage< + Protocol.Runtime.EvaluateRequest, + Protocol.Runtime.EvaluateResponse + >("Runtime.evaluate", { + expression: pageCommand, + includeCommandLineAPI: true, // supports things like $x + }); + await this.#checkForErrorResult(result, pageCommand); + return result.result.value; + } + + if (typeof pageCommand === "function") { + const { executionContextId } = await this.#protocol.sendWebSocketMessage< + Protocol.Page.CreateIsolatedWorldRequest, + Protocol.Page.CreateIsolatedWorldResponse + >( + "Page.createIsolatedWorld", + { + frameId: this.#protocol.frame_id, + }, + ); + + const res = await this.#protocol.sendWebSocketMessage< + Protocol.Runtime.CallFunctionOnRequest, + Protocol.Runtime.CallFunctionOnResponse + >( + "Runtime.callFunctionOn", + { + functionDeclaration: pageCommand.toString(), + executionContextId: executionContextId, + returnByValue: true, + awaitPromise: true, + userGesture: true, + }, + ); + await this.#checkForErrorResult(res, pageCommand.toString()); + return res.result.value; + } + } + + /** + * Wait for the page to change. Can be used with `click()` if clicking a button or anchor tag that redirects the user + */ + async waitForPageChange(): Promise { + const method = "Page.loadEventFired"; + this.#protocol.notification_resolvables.set(method, deferred()); + const notificationPromise = this.#protocol.notification_resolvables.get( + method, + ); + await notificationPromise; + this.#protocol.notification_resolvables.delete(method); + } + + /** + * Check if the given text exists on the DOM + * + * @param text - The text to check for + */ + async assertSee(text: string): Promise { + const command = `document.body.innerText.includes('${text}')`; + const exists = await this.evaluate(command); + if (exists !== true) { // We know it's going to fail, so before an assertion error is thrown, cleanupup + await this.#protocol.done(); + } + assertEquals(exists, true); + } + + /** + * Representation of the Browser's `document.querySelector` + * + * @param selector - The selector for the element + * + * @returns An element class, allowing you to take an action upon that element + */ + async querySelector(selector: string) { + const result = await this.evaluate( + `document.querySelector('${selector}')`, + ); + if (result === null) { + await this.#protocol.done( + 'The selector "' + selector + '" does not exist inside the DOM', + ); + } + return new Element("document.querySelector", selector, this); + } + + /** + * Take a screenshot of the page and save it to `filename` in `path` folder, with a `format` and `quality` (jpeg format only) + * If `selector` is passed in, it will take a screenshot of only that element + * and its children as opposed to the whole page. + * + * @param path - The path of where to save the screenshot to + * @param options + * + * @returns The path to the file relative to CWD, e.g., "Screenshots/users/user_1.png" + */ + async takeScreenshot( + path: string, + options?: ScreenshotOptions, + ): Promise { + if (!existsSync(path)) { + await this.#protocol.done(); + throw new Error(`The provided folder path - ${path} doesn't exist`); + } + const ext = options?.format ?? "jpeg"; + const rawViewportResult = options?.selector + ? await this.evaluate( + `JSON.stringify(document.querySelector('${options.selector}').getBoundingClientRect())`, + ) + : "{}"; + const jsonViewportResult = JSON.parse(rawViewportResult); + const viewPort = { + x: jsonViewportResult.x, + y: jsonViewportResult.y, + width: jsonViewportResult.width, + height: jsonViewportResult.height, + scale: 2, + }; + const clip = (options?.selector) ? viewPort : undefined; + + if (options?.quality && Math.abs(options.quality) > 100 && ext == "jpeg") { + await this.#protocol.done( + "A quality value greater than 100 is not allowed.", + ); + } + + //Quality should defined only if format is jpeg + const quality = (ext == "jpeg") + ? ((options?.quality) ? Math.abs(options.quality) : 80) + : undefined; + + const res = await this.#protocol.sendWebSocketMessage< + Protocol.Page.CaptureScreenshotRequest, + Protocol.Page.CaptureScreenshotResponse + >( + "Page.captureScreenshot", + { + format: ext, + quality: quality, + clip: clip, + }, + ) as { + data: string; + }; + + //Writing the Obtained Base64 encoded string to image file + const fName = `${path}/${ + options?.fileName?.replaceAll(/.jpeg|.jpg|.png/g, "") ?? + generateTimestamp() + }.${ext}`; + const B64str = res.data; + const u8Arr = Uint8Array.from(atob(B64str), (c) => c.charCodeAt(0)); + try { + Deno.writeFileSync(fName, u8Arr); + } catch (e) { + await this.#protocol.done(); + throw new Error(e.message); + } + + return fName; + } + + /** + * Checks if the result is an error + * + * @param result - The DOM result response, after writing to stdin and getting by stdout of the process + * @param commandSent - The command sent to trigger the result + */ + async #checkForErrorResult( + result: Protocol.Runtime.AwaitPromiseResponse, + commandSent: string, + ): Promise { + const exceptionDetail = result.exceptionDetails; + if (!exceptionDetail) { + return; + } + if (exceptionDetail.text && !exceptionDetail.exception) { // specific for firefox + await this.#protocol.done(exceptionDetail.text); + } + const errorMessage = exceptionDetail.exception!.description ?? + exceptionDetail.text; + if (errorMessage.includes("SyntaxError")) { // a syntax error + const message = errorMessage.replace("SyntaxError: ", ""); + await this.#protocol.done(); + throw new SyntaxError(message + ": `" + commandSent + "`"); + } + // any others, unsure what they'd be + await this.#protocol.done(`${errorMessage}: "${commandSent}"`); + } +} diff --git a/src/protocol.ts b/src/protocol.ts new file mode 100644 index 00000000..77a6e269 --- /dev/null +++ b/src/protocol.ts @@ -0,0 +1,222 @@ +import { Deferred, deferred } from "../deps.ts"; +import { existsSync } from "./utility.ts"; +import type { Browsers } from "./types.ts"; + +interface MessageResponse { // For when we send an event to get one back, eg running a JS expression + id: number; + result?: Record; // Present on success, OR for example if we use goTo and the url doesnt exist (in firefox) + error?: unknown; // Present on error +} + +interface NotificationResponse { // Not entirely sure when, but when we send the `Network.enable` method + method: string; + params: unknown; +} + +export class Protocol { + /** + * Our web socket connection to the remote debugging port + */ + public socket: WebSocket; + + /** + * The sub process that runs headless chrome + */ + public browser_process: Deno.Process; + + /** + * What browser we running? + */ + public browser: Browsers; + + public frame_id: string; + + /** + * A counter that acts as the message id we use to send as part of the event data through the websocket + */ + public next_message_id = 1; + + /** + * To keep hold of our promises waiting for messages from the websocket + */ + public resolvables: Map> = new Map(); + + /** + * To keep hold of promises waiting for a notification from the websocket + */ + public notification_resolvables: Map> = new Map(); + + /** + * Only if the browser is firefox, is this present. + * This is the path to the directory that firefox uses + * to write a profile + */ + public firefox_profile_path: string | undefined = undefined; + + /** + * Track if we've closed the sub process, so we dont try close it when it already has been + */ + public browser_process_closed = false; + + constructor( + socket: WebSocket, + browserProcess: Deno.Process, + browser: Browsers, + frameId: string, + firefoxProfilePath: string | undefined, + ) { + this.socket = socket; + this.browser_process = browserProcess; + this.browser = browser; + this.frame_id = frameId; + this.firefox_profile_path = firefoxProfilePath; + // Register on message listener + this.socket.onmessage = (msg) => { + const data = JSON.parse(msg.data); + if (data.method === "Page.frameStartedLoading") { + this.frame_id = data.params.frameId; + } + this.#handleSocketMessage(data); + }; + } + + /** + * Close/stop the sub process, and close the ws connection. Must be called when finished with all your testing + * + * @param errMsg - If supplied, will finally throw an error with the message after closing all processes + */ + public async done(errMsg?: string): Promise { + // Say a user calls an assertion method, and then calls done(), we make sure that if + // the subprocess is already closed, dont try close it again + if (this.browser_process_closed === true) { + return; + } + const clientIsClosed = deferred(); + this.socket.onclose = () => clientIsClosed.resolve(); + // cloing subprocess will also close the ws endpoint + this.browser_process.stderr!.close(); + this.browser_process.stdout!.close(); + this.browser_process.close(); + this.browser_process_closed = true; + // Zombie processes is a thing with Windows, the firefox process on windows + // will not actually be closed using the above. + // Related Deno issue: https://github.com/denoland/deno/issues/7087 + if (this.browser === "firefox" && Deno.build.os === "windows") { + const p = Deno.run({ + cmd: ["taskkill", "/F", "/IM", "firefox.exe"], + stdout: "null", + stderr: "null", + }); + await p.status(); + p.close(); + } + await clientIsClosed; // done AFTER the above conditional because the process is still running, so the client is never closed + if (this.firefox_profile_path) { + // On windows, this block is annoying. We either get a perm denied or + // resource is in use error (classic windows). So what we're doing here is + // even if one of those errors are thrown, keep trying because what i've (ed) + // found is, it seems to need a couple seconds to realise that the dir + // isnt being used anymore. The loop shouldn't be needed for macos/unix though, so + // it will likely only run once. + while (existsSync(this.firefox_profile_path)) { + try { + Deno.removeSync(this.firefox_profile_path, { recursive: true }); + } catch (_e) { + // Just try removing again + } + } + } + if (errMsg) { + throw new Error(errMsg); + } + } + + /** + * Main method to handle sending messages/events to the websocket endpoint. + * + * @param method - Any DOMAIN, see sidebar at https://chromedevtools.github.io/devtools-protocol/tot/, eg Runtime.evaluate, or DOM.getDocument + * @param params - Parameters required for the domain method + * + * @returns + */ + public async sendWebSocketMessage( + method: string, + params?: RequestType, + ): Promise { + const data: { + id: number; + method: string; + params?: RequestType; + } = { + id: this.next_message_id++, + method: method, + }; + if (params) data.params = params; + const promise = deferred(); + this.resolvables.set(data.id, promise); + this.socket.send(JSON.stringify(data)); + const result = await promise; + this.resolvables.delete(data.id); + return result; + } + + /** + * Gets the websocket url we use to create a ws client with. + * Requires the headless chrome process to be running, as + * this is what actually starts the remote debugging url + * + * @param hostname - The hostname to fetch from + * @param port - The port for the hostname to fetch from + * + * @returns The url to connect to + */ + public static async getWebSocketInfo( + hostname: string, + port: number, + ): Promise<{ debugUrl: string; frameId: string }> { + let debugUrl = ""; + let frameId = ""; + while (debugUrl === "") { + try { + const res = await fetch(`http://${hostname}:${port}/json/list`); + const json = await res.json(); + debugUrl = json[0]["webSocketDebuggerUrl"]; + frameId = json[0]["id"]; + } catch (_err) { + // do nothing, loop again until the endpoint is ready + } + } + return { + debugUrl, + frameId, + }; + } + + #handleSocketMessage( + message: MessageResponse | NotificationResponse, + ) { + if ("id" in message) { // message response + const resolvable = this.resolvables.get(message.id); + if (resolvable) { + if ("result" in message) { // success response + if ("errorText" in message.result!) { + const r = this.notification_resolvables.get("Page.loadEventFired"); + if (r) { + r.resolve(); + } + } + resolvable.resolve(message.result); + } + if ("error" in message) { // error response + resolvable.resolve(message.error); + } + } + } + if ("method" in message) { // Notification response + const resolvable = this.notification_resolvables.get(message.method); + if (resolvable) { + resolvable.resolve(); + } + } + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..47009571 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type Browsers = "firefox" | "chrome"; diff --git a/src/utility.ts b/src/utility.ts index 6ed6c452..ab398a53 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -20,3 +20,89 @@ export const generateTimestamp = (): string => { dt.toLocaleTimeString().replace(/:/g, "_"); return ts; }; + +/** + * Gets the full path to the chrome executable on the users filesystem + * + * @returns The path to chrome + */ +export function getChromePath(): string { + const paths = { + // deno-lint-ignore camelcase + windows_chrome_exe: + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", + // deno-lint-ignore camelcase + windows_chrome_exe_x86: + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", + darwin: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + linux: "/usr/bin/google-chrome", + }; + let chromePath = ""; + switch (Deno.build.os) { + case "darwin": + chromePath = paths.darwin; + break; + case "windows": + if (existsSync(paths.windows_chrome_exe)) { + chromePath = paths.windows_chrome_exe; + break; + } + if (existsSync(paths.windows_chrome_exe_x86)) { + chromePath = paths.windows_chrome_exe_x86; + break; + } + + throw new Error( + "Cannot find path for chrome in Windows. Submit an issue if you encounter this error.", + ); + case "linux": + chromePath = paths.linux; + break; + } + return chromePath; +} + +export function getChromeArgs(port: number, binaryPath?: string): string[] { + return [ + binaryPath || getChromePath(), + "--headless", + "--remote-debugging-port=" + port, + "--disable-gpu", + "--no-sandbox", + ]; +} + +/** + * Get full path to the firefox binary on the user'ss filesystem. + * Thanks to [caspervonb](https://github.com/caspervonb/deno-web/blob/master/browser.ts) + * + * @returns the path + */ +export function getFirefoxPath(): string { + switch (Deno.build.os) { + case "darwin": + return "/Applications/Firefox.app/Contents/MacOS/firefox"; + case "linux": + return "/usr/bin/firefox"; + case "windows": + return "C:\\Program Files\\Mozilla Firefox\\firefox.exe"; + } +} + +export function getFirefoxArgs( + tmpDirName: string, + port: number, + binaryPath?: string, +): string[] { + return [ + binaryPath || getFirefoxPath(), + "--start-debugger-server", + port.toString(), + "-headless", + "--remote-debugging-port", + port.toString(), + "-profile", + tmpDirName, + "about:blank", + ]; +} diff --git a/tests/browser_list.ts b/tests/browser_list.ts new file mode 100644 index 00000000..9d30a88d --- /dev/null +++ b/tests/browser_list.ts @@ -0,0 +1,63 @@ +import type { Browsers } from "../src/types.ts"; +import { getChromePath, getFirefoxPath } from "../src/utility.ts"; + +export const browserList: Array<{ + name: Browsers; + errors: { + page_not_exist_message: string; + page_name_not_resolved: string; + }; + cookies: Record[]; + getPath: () => string; +}> = [ + { + name: "chrome", + errors: { + page_not_exist_message: + 'NS_ERROR_UNKNOWN_HOST: Error for navigating to page "https://hellaDOCSWOWThispagesurelycantexist.biscuit"', + page_name_not_resolved: + 'net::ERR_NAME_NOT_RESOLVED: Error for navigating to page "https://hhh"', + }, + getPath: getChromePath, + cookies: [ + { + domain: "drash.land", + expires: -1, + httpOnly: false, + name: "user", + path: "/", + priority: "Medium", + sameParty: false, + secure: true, + session: true, + size: 6, + sourcePort: 443, + sourceScheme: "Secure", + value: "ed", + }, + ], + }, + { + name: "firefox", + errors: { + page_not_exist_message: + 'net::ERR_NAME_NOT_RESOLVED: Error for navigating to page "https://hellaDOCSWOWThispagesurelycantexist.biscuit"', + page_name_not_resolved: + 'NS_ERROR_UNKNOWN_HOST: Error for navigating to page "https://hhh"', + }, + getPath: getFirefoxPath, + cookies: [ + { + domain: "drash.land", + expires: -1, + httpOnly: false, + name: "user", + path: "/", + secure: true, + session: true, + size: 6, + value: "ed", + }, + ], + }, +]; diff --git a/tests/integration/assert_methods_clean_up_on_fail_test.ts b/tests/integration/assert_methods_clean_up_on_fail_test.ts index 2bf706de..52ec5adb 100644 --- a/tests/integration/assert_methods_clean_up_on_fail_test.ts +++ b/tests/integration/assert_methods_clean_up_on_fail_test.ts @@ -1,5 +1,6 @@ import { assertEquals } from "../../deps.ts"; -import { ChromeClient, FirefoxClient } from "../../mod.ts"; +import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; /** * The reason for this test is because originally, when an assertion method failed, @@ -25,68 +26,40 @@ import { ChromeClient, FirefoxClient } from "../../mod.ts"; // THIS TEST SHOULD NOT HANG, IF IT DOES, THEN THIS TEST FAILS -Deno.test("Chrome: Assertion methods cleanup when an assertion fails", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.assertUrlIs("https://chromestatus.com/features"); - let gotError = false; - let errMsg = ""; - try { - await Sinco.assertSee("Chrome Versions"); // Does not exist on the page (the `V` is lowercase, whereas here we use an uppercase) - } catch (err) { - gotError = true; - errMsg = err.message - // deno-lint-ignore no-control-regex - .replace(/\x1b/g, "") // or \x1b\[90m - .replace(/\[1m/g, "") - .replace(/\[[0-9][0-9]m/g, "") - .replace(/\n/g, ""); - } - assertEquals(gotError, true); - assertEquals( - errMsg, - "Values are not equal: [Diff] Actual / Expected- false+ true", +for (const browserItem of browserList) { + Deno.test( + browserItem.name + ": Assertion methods cleanup when an assertion fails", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + assertEquals(await page.location(), "https://chromestatus.com/features"); + let gotError = false; + let errMsg = ""; + try { + await page.assertSee("Chrome Versions"); // Does not exist on the page (the `V` is lowercase, whereas here we use an uppercase) + } catch (err) { + gotError = true; + errMsg = err.message + // deno-lint-ignore no-control-regex + .replace(/\x1b/g, "") // or \x1b\[90m + .replace(/\[1m/g, "") + .replace(/\[[0-9][0-9]m/g, "") + .replace(/\n/g, ""); + } + assertEquals(gotError, true); + assertEquals( + errMsg, + "Values are not equal: [Diff] Actual / Expected- false+ true", + ); + // Now we should be able to run tests again without it hanging + const Sinco2 = await buildFor(browserItem.name); + const page2 = await Sinco2.goTo("https://chromestatus.com"); + assertEquals(await page2.location(), "https://chromestatus.com/features"); + try { + await page2.assertSee("Chrome Versions"); + } catch (_err) { + // + } + }, ); - // Now we should be able to run tests again without it hanging - const Sinco2 = await ChromeClient.build(); - await Sinco2.goTo("https://chromestatus.com"); - await Sinco2.assertUrlIs("https://chromestatus.com/features"); - try { - await Sinco2.assertSee("Chrome Versions"); - } catch (_err) { - // - } -}); - -Deno.test("Firefox: Assertion methods cleanup when an assertion fails", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.assertUrlIs("https://chromestatus.com/features"); - let gotError = false; - let errMsg = ""; - try { - await Sinco.assertSee("Chrome Versions"); // Does not exist on the page (the `V` is lowercase, whereas here we use an uppercase) - } catch (err) { - gotError = true; - errMsg = err.message - // deno-lint-ignore no-control-regex - .replace(/\x1b/g, "") // or \x1b\[90m - .replace(/\[1m/g, "") - .replace(/\[[0-9][0-9]m/g, "") - .replace(/\n/g, ""); - } - assertEquals(gotError, true); - assertEquals( - errMsg, - "Values are not equal: [Diff] Actual / Expected- false+ true", - ); - // Now we should be able to run tests again without it hanging - const Sinco2 = await FirefoxClient.build(); - await Sinco2.goTo("https://chromestatus.com"); - await Sinco2.assertUrlIs("https://chromestatus.com/features"); - try { - await Sinco2.assertSee("Chrome Versions"); - } catch (_err) { - // - } -}); +} diff --git a/tests/integration/clicking_elements_test.ts b/tests/integration/clicking_elements_test.ts index 9f8f630f..cb54a079 100644 --- a/tests/integration/clicking_elements_test.ts +++ b/tests/integration/clicking_elements_test.ts @@ -1,19 +1,24 @@ import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; +import { assertEquals } from "../../deps.ts"; -Deno.test("Chrome: Clicking elements - Tutorial for this feature in the docs should work", async () => { - const Sinco = await buildFor("chrome"); - await Sinco.goTo("https://drash.land"); - await Sinco.click('a[href="https://discord.gg/RFsCSaHRWK"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://discord.com/invite/RFsCSaHRWK"); - await Sinco.done(); -}); - -Deno.test("Firefox: Clicking elements - Tutorial for this feature in the docs should work", async () => { - const Sinco = await buildFor("firefox"); - await Sinco.goTo("https://drash.land"); - await Sinco.click('a[href="https://discord.gg/RFsCSaHRWK"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://discord.com/invite/RFsCSaHRWK"); - await Sinco.done(); -}); +for (const browserItem of browserList) { + Deno.test( + browserItem.name + + ": Clicking elements - Tutorial for this feature in the docs should work", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const elem = await page.querySelector( + 'a[href="https://discord.gg/RFsCSaHRWK"]', + ); + await elem.click(); + await page.waitForPageChange(); + assertEquals( + await page.location(), + "https://discord.com/invite/RFsCSaHRWK", + ); + await Sinco.done(); + }, + ); +} diff --git a/tests/integration/csrf_protected_pages_test.ts b/tests/integration/csrf_protected_pages_test.ts index 678e9fb3..d8b69f54 100644 --- a/tests/integration/csrf_protected_pages_test.ts +++ b/tests/integration/csrf_protected_pages_test.ts @@ -1,4 +1,3 @@ -import { ChromeClient, FirefoxClient } from "../../mod.ts"; import { Rhum } from "../deps.ts"; /** * Other ways you can achieve this are: @@ -9,26 +8,23 @@ import { Rhum } from "../deps.ts"; const title = "CSRF Protected Pages - Tutorial for this feature in the docs should work"; -Deno.test("Chrome: " + title, async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://drash.land"); - await Sinco.setCookie("X-CSRF-TOKEN", "hi:)", "https://drash.land"); - await Sinco.goTo("https://drash.land/drash/v1.x/#/"); // Going here to ensure the cookie stays - const cookieVal = await Sinco.evaluatePage(() => { - return document.cookie; - }); - await Sinco.done(); - Rhum.asserts.assertEquals(cookieVal, "X-CSRF-TOKEN=hi:)"); -}); +import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; -Deno.test("Firefox: " + title, async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://drash.land"); - await Sinco.setCookie("X-CSRF-TOKEN", "hi:)", "https://drash.land"); - await Sinco.goTo("https://drash.land/drash/v1.x/#/"); // Going here to ensure the cookie stays - const cookieVal = await Sinco.evaluatePage(() => { - return document.cookie; +for (const browserItem of browserList) { + Deno.test("Chrome: " + title, async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + await page.cookie({ + name: "X-CSRF-TOKEN", + value: "hi:)", + url: "https://drash.land", + }); + await Sinco.goTo("https://drash.land/drash/v1.x/#/"); // Going here to ensure the cookie stays + const cookieVal = await page.evaluate(() => { + return document.cookie; + }); + await Sinco.done(); + Rhum.asserts.assertEquals(cookieVal, "X-CSRF-TOKEN=hi:)"); }); - await Sinco.done(); - Rhum.asserts.assertEquals(cookieVal, "X-CSRF-TOKEN=hi:)"); -}); +} diff --git a/tests/integration/custom_assertions_test.ts b/tests/integration/custom_assertions_test.ts index ce9c031a..b45a5aed 100644 --- a/tests/integration/custom_assertions_test.ts +++ b/tests/integration/custom_assertions_test.ts @@ -1,17 +1,17 @@ -import { ChromeClient, FirefoxClient } from "../../mod.ts"; +import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; +import { assertEquals } from "../../deps.ts"; -Deno.test("Chrome: Assertions - Tutorial for this feature in the docs should work", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://drash.land"); - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.assertSee("Develop With Confidence"); - await Sinco.done(); -}); - -Deno.test("Firefox: Assertions - Tutorial for this feature in the docs should work", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://drash.land"); - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.assertSee("Develop With Confidence"); - await Sinco.done(); -}); +for (const browserItem of browserList) { + Deno.test( + browserItem.name + + ": Assertions - Tutorial for this feature in the docs should work", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + assertEquals(await page.location(), "https://drash.land/"); + await page.assertSee("Develop With Confidence"); + await Sinco.done(); + }, + ); +} diff --git a/tests/integration/get_and_set_input_value_test.ts b/tests/integration/get_and_set_input_value_test.ts index b0d744ce..8df870c3 100644 --- a/tests/integration/get_and_set_input_value_test.ts +++ b/tests/integration/get_and_set_input_value_test.ts @@ -1,20 +1,19 @@ import { assertEquals } from "../../deps.ts"; -import { ChromeClient, FirefoxClient } from "../../mod.ts"; +import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; -Deno.test("Chrome: Get and set input value - Tutorial for this feature in the docs should work", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.type('input[placeholder="Filter"]', "hello world"); - const val = await Sinco.getInputValue('input[placeholder="Filter"]'); - assertEquals(val, "hello world"); - await Sinco.done(); -}); - -Deno.test("Firefox: Get and set input value - Tutorial for this feature in the docs should work", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.type('input[placeholder="Filter"]', "hello world"); - const val = await Sinco.getInputValue('input[placeholder="Filter"]'); - assertEquals(val, "hello world"); - await Sinco.done(); -}); +for (const browserItem of browserList) { + Deno.test( + browserItem.name + + ": Get and set input value - Tutorial for this feature in the docs should work", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const elem = await page.querySelector('input[placeholder="Filter"]'); + await elem.value("hello world"); + const val = await elem.value(); + assertEquals(val, "hello world"); + await Sinco.done(); + }, + ); +} diff --git a/tests/integration/getting_started_test.ts b/tests/integration/getting_started_test.ts index 37f94d33..ad43890a 100644 --- a/tests/integration/getting_started_test.ts +++ b/tests/integration/getting_started_test.ts @@ -1,31 +1,27 @@ import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; +import { assertEquals } from "../../deps.ts"; -Deno.test("Chrome: Tutorial for Getting Started in the docs should work", async () => { - // Setup - const Sinco = await buildFor("chrome"); // also supports firefox - await Sinco.goTo("https://drash.land"); // Go to this page +for (const browserItem of browserList) { + Deno.test( + browserItem.name + ": Tutorial for Getting Started in the docs should work", + async () => { + // Setup + const Sinco = await buildFor(browserItem.name); // also supports firefox + const page = await Sinco.goTo("https://drash.land"); // Go to this page - // Do any actions and assertions, in any order - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.click('a[href="https://discord.gg/RFsCSaHRWK"]'); // This element will take the user to Sinco's documentation - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://discord.com/invite/RFsCSaHRWK"); + // Do any actions and assertions, in any order + assertEquals(await page.location(), "https://drash.land/"); + const elem = await page.querySelector( + 'a[href="https://discord.gg/RFsCSaHRWK"]', + ); + await elem.click(); // This element will take the user to Sinco's documentation + await page.waitForPageChange(); + const location = await page.location(); - // Once finished, close to clean up any processes - await Sinco.done(); -}); - -Deno.test("Firefox: Tutorial for Getting Started in the docs should work", async () => { - // Setup - const Sinco = await buildFor("firefox"); // also supports firefox - await Sinco.goTo("https://drash.land"); // Go to this page - - // Do any actions and assertions, in any order - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.click('a[href="https://discord.gg/RFsCSaHRWK"]'); // This element will take the user to Sinco's documentation - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://discord.com/invite/RFsCSaHRWK"); - - // Once finished, close to clean up any processes - await Sinco.done(); -}); + // Once finished, close to clean up any processes + await Sinco.done(); + assertEquals(location, "https://discord.com/invite/RFsCSaHRWK"); + }, + ); +} diff --git a/tests/integration/manipulate_page_test.ts b/tests/integration/manipulate_page_test.ts index 258049ad..944bc817 100644 --- a/tests/integration/manipulate_page_test.ts +++ b/tests/integration/manipulate_page_test.ts @@ -1,95 +1,57 @@ import { assertEquals } from "../../deps.ts"; -import { ChromeClient, FirefoxClient } from "../../mod.ts"; +import { buildFor } from "../../mod.ts"; -Deno.test("Chrome: Manipulate Webpage", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://drash.land"); +import { browserList } from "../browser_list.ts"; - const updatedBody = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - const prevBody = document.body.children.length; - // deno-lint-ignore no-undef - const newEl = document.createElement("p"); - // deno-lint-ignore no-undef - document.body.appendChild(newEl); - // deno-lint-ignore no-undef - return prevBody === document.body.children.length - 1; - }); - assertEquals(updatedBody, true); +for (const browserItem of browserList) { + Deno.test(browserItem.name + ": Manipulate Webpage", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); - await Sinco.done(); -}); + const updatedBody = await page.evaluate(() => { + // deno-lint-ignore no-undef + const prevBody = document.body.children.length; + // deno-lint-ignore no-undef + const newEl = document.createElement("p"); + // deno-lint-ignore no-undef + document.body.appendChild(newEl); + // deno-lint-ignore no-undef + return prevBody === document.body.children.length - 1; + }); + assertEquals(updatedBody, true); -Deno.test("Chrome: Evaluating a script - Tutorial for this feature in the documentation works", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://drash.land"); - const pageTitle = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - return document.title; - }); - const sum = await Sinco.evaluatePage(`1 + 10`); - const oldBodyLength = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - return document.body.children.length; - }); - const newBodyLength = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - const p = document.createElement("p"); - p.textContent = "Hello world!"; - // deno-lint-ignore no-undef - document.body.appendChild(p); - // deno-lint-ignore no-undef - return document.body.children.length; + await Sinco.done(); }); - await Sinco.done(); - assertEquals(pageTitle, "Drash Land"); - assertEquals(sum, 11); - assertEquals(oldBodyLength, 3); - assertEquals(newBodyLength, 4); -}); -Deno.test("Firefox: Manipulate Webpage", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://drash.land"); - - const updatedBody = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - const prevBody = document.body.children.length; - // deno-lint-ignore no-undef - const newEl = document.createElement("p"); - // deno-lint-ignore no-undef - document.body.appendChild(newEl); - // deno-lint-ignore no-undef - return prevBody === document.body.children.length - 1; - }); - await Sinco.done(); - assertEquals(updatedBody, true); -}); - -Deno.test("Firefox: Evaluating a script - Tutorial for this feature in the documentation works", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://drash.land"); - const pageTitle = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - return document.title; - }); - const sum = await Sinco.evaluatePage(`1 + 10`); - const oldBodyLength = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - return document.body.children.length; - }); - const newBodyLength = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - const p = document.createElement("p"); - p.textContent = "Hello world!"; - // deno-lint-ignore no-undef - document.body.appendChild(p); - // deno-lint-ignore no-undef - return document.body.children.length; - }); - await Sinco.done(); - assertEquals(pageTitle, "Drash Land"); - assertEquals(sum, 11); - assertEquals(oldBodyLength, 3); - assertEquals(newBodyLength, 4); -}); + Deno.test( + browserItem.name + + ": Evaluating a script - Tutorial for this feature in the documentation works", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const pageTitle = await page.evaluate(() => { + // deno-lint-ignore no-undef + return document.title; + }); + const sum = await page.evaluate(`1 + 10`); + const oldBodyLength = await page.evaluate(() => { + // deno-lint-ignore no-undef + return document.body.children.length; + }); + const newBodyLength = await page.evaluate(() => { + // deno-lint-ignore no-undef + const p = document.createElement("p"); + p.textContent = "Hello world!"; + // deno-lint-ignore no-undef + document.body.appendChild(p); + // deno-lint-ignore no-undef + return document.body.children.length; + }); + await Sinco.done(); + assertEquals(pageTitle, "Drash Land"); + assertEquals(sum, 11); + assertEquals(oldBodyLength, 3); + assertEquals(newBodyLength, 4); + }, + ); +} diff --git a/tests/integration/screenshots_test.ts b/tests/integration/screenshots_test.ts index 015e6bdd..4a947b8d 100644 --- a/tests/integration/screenshots_test.ts +++ b/tests/integration/screenshots_test.ts @@ -1,37 +1,27 @@ import { buildFor } from "../../mod.ts"; -Deno.test("Chrome - Tutorial for taking screenshots in the docs should work", async () => { - const Sinco = await buildFor("chrome"); - await Sinco.goTo("https://drash.land"); - const screenshotsFolder = "./screenshots"; - Deno.mkdirSync(screenshotsFolder); // Ensure you create the directory your screenshots will be put within - await Sinco.takeScreenshot(screenshotsFolder); // Will take a screenshot of the whole page, and write it to `./screenshots/dd_mm_yyyy_hh_mm_ss.jpeg` - await Sinco.takeScreenshot(screenshotsFolder, { - fileName: "drash_land.png", - format: "png", - }); // Specify filename and format. Will be saved as `./screenshots/drash_land.png` - await Sinco.takeScreenshot(screenshotsFolder, { - fileName: "modules.jpeg", - selector: 'a[href="https://github.com/drashland"]', - }); // Will screenshot only the GitHub icon section, and write it to `./screenshots/dd_mm_yyyy_hh_mm_ss.jpeg` - await Sinco.done(); - Deno.removeSync("./screenshots", { recursive: true }); -}); +import { browserList } from "../browser_list.ts"; -Deno.test("Firefox - Tutorial for taking screenshots in the docs should work", async () => { - const Sinco = await buildFor("firefox"); - await Sinco.goTo("https://drash.land"); - const screenshotsFolder = "./screenshots"; - Deno.mkdirSync(screenshotsFolder); // Ensure you create the directory your screenshots will be put within - await Sinco.takeScreenshot(screenshotsFolder); // Will take a screenshot of the whole page, and write it to `./screenshots/dd_mm_yyyy_hh_mm_ss.jpeg` - await Sinco.takeScreenshot(screenshotsFolder, { - fileName: "drash_land.png", - format: "png", - }); // Specify filename and format. Will be saved as `./screenshots/drash_land.png` - await Sinco.takeScreenshot(screenshotsFolder, { - fileName: "modules.jpeg", - selector: 'a[href="https://github.com/drashland"]', - }); // Will screenshot only the GitHub icon section, and write it to `./screenshots/dd_mm_yyyy_hh_mm_ss.jpeg` - await Sinco.done(); - Deno.removeSync("./screenshots", { recursive: true }); -}); +for (const browserItem of browserList) { + Deno.test( + browserItem.name + + " - Tutorial for taking screenshots in the docs should work", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const screenshotsFolder = "./screenshots"; + Deno.mkdirSync(screenshotsFolder); // Ensure you create the directory your screenshots will be put within + await page.takeScreenshot(screenshotsFolder); // Will take a screenshot of the whole page, and write it to `./screenshots/dd_mm_yyyy_hh_mm_ss.jpeg` + await page.takeScreenshot(screenshotsFolder, { + fileName: "drash_land.png", + format: "png", + }); // Specify filename and format. Will be saved as `./screenshots/drash_land.png` + await page.takeScreenshot(screenshotsFolder, { + fileName: "modules.jpeg", + selector: 'a[href="https://github.com/drashland"]', + }); // Will screenshot only the GitHub icon section, and write it to `./screenshots/dd_mm_yyyy_hh_mm_ss.jpeg` + await Sinco.done(); + Deno.removeSync("./screenshots", { recursive: true }); + }, + ); +} diff --git a/tests/integration/visit_pages_test.ts b/tests/integration/visit_pages_test.ts index 4689edde..a9cb09b9 100644 --- a/tests/integration/visit_pages_test.ts +++ b/tests/integration/visit_pages_test.ts @@ -1,15 +1,17 @@ import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; +import { assertEquals } from "../../deps.ts"; -Deno.test("Chrome: Visit pages - Tutorial for this feature in the docs should work", async () => { - const Sinco = await buildFor("chrome"); - await Sinco.goTo("https://drash.land"); - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.done(); -}); - -Deno.test("Firfox: Visit pages - Tutorial for this feature in the docs should work", async () => { - const Sinco = await buildFor("firefox"); - await Sinco.goTo("https://drash.land"); - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.done(); -}); +for (const browserItem of browserList) { + Deno.test( + browserItem.name + + ": Visit pages - Tutorial for this feature in the docs should work", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const location = await page.location(); + await Sinco.done(); + assertEquals(location, "https://drash.land/"); + }, + ); +} diff --git a/tests/integration/waiting_test.ts b/tests/integration/waiting_test.ts index 01c66c37..406dc5d1 100644 --- a/tests/integration/waiting_test.ts +++ b/tests/integration/waiting_test.ts @@ -1,21 +1,23 @@ import { buildFor } from "../../mod.ts"; -Deno.test("Chrome: Waiting - Tutorial for this feature in the docs should work", async () => { - const Sinco = await buildFor("chrome"); - await Sinco.goTo("https://drash.land"); - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.click('a[href="https://discord.gg/RFsCSaHRWK"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://discord.com/invite/RFsCSaHRWK"); - await Sinco.done(); -}); +import { browserList } from "../browser_list.ts"; +import { assertEquals } from "../../deps.ts"; -Deno.test("Firefox: Waiting - Tutorial for this feature in the docs should work", async () => { - const Sinco = await buildFor("firefox"); - await Sinco.goTo("https://drash.land"); - await Sinco.assertUrlIs("https://drash.land/"); - await Sinco.click('a[href="https://discord.gg/RFsCSaHRWK"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://discord.com/invite/RFsCSaHRWK"); - await Sinco.done(); -}); +for (const browserItem of browserList) { + Deno.test( + browserItem.name + + ": Waiting - Tutorial for this feature in the docs should work", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const elem = await page.querySelector( + 'a[href="https://discord.gg/RFsCSaHRWK"]', + ); + await elem.click(); + await page.waitForPageChange(); + const location = await page.location(); + await Sinco.done(); + assertEquals(location, "https://discord.com/invite/RFsCSaHRWK"); + }, + ); +} diff --git a/tests/unit/Screenshots/.gitkeep b/tests/unit/Screenshots/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/chrome_client_test.ts b/tests/unit/chrome_client_test.ts deleted file mode 100644 index 559480ce..00000000 --- a/tests/unit/chrome_client_test.ts +++ /dev/null @@ -1,550 +0,0 @@ -import { existsSync } from "./../../src/utility.ts"; -import { Rhum } from "../deps.ts"; -import { deferred } from "../../deps.ts"; -import { ChromeClient } from "../../mod.ts"; -import { getChromePath } from "../../src/chrome_client.ts"; - -Rhum.testPlan("tests/unit/chrome_client_test.ts", () => { - Rhum.testSuite("build()", () => { - Rhum.testCase("Will start chrome headless as a subprocess", async () => { - const Sinco = await ChromeClient.build(); - const res = await fetch("http://localhost:9292/json/list"); - const json = await res.json(); - // Our ws client should be able to connect if the browser is running - const client = new WebSocket(json[0]["webSocketDebuggerUrl"]); - const promise = deferred(); - client.onopen = function () { - client.close(); - }; - client.onclose = function () { - promise.resolve(); - }; - await promise; - await Sinco.done(); - }); - Rhum.testCase( - "Uses the port when passed in to the parameters", - async () => { - const Sinco = await ChromeClient.build({ - debuggerPort: 9999, - }); - const res = await fetch("http://localhost:9999/json/list"); - const json = await res.json(); - // Our ws client should be able to connect if the browser is running - const client = new WebSocket(json[0]["webSocketDebuggerUrl"]); - const promise = deferred(); - client.onopen = function () { - client.close(); - }; - client.onclose = function () { - promise.resolve(); - }; - await promise; - await Sinco.done(); - }, - ); - Rhum.testCase( - "Uses the hostname when passed in to the parameters", - async () => { - // Unable to test properly, as windows doesnt like 0.0.0.0 or localhost, so the only choice is 127.0.0.1 but this is already the default - }, - ); - Rhum.testCase( - "Uses the binaryPath when passed in to the parameters", - async () => { - const Sinco = await ChromeClient.build({ - binaryPath: await getChromePath(), - }); - - const res = await fetch("http://localhost:9292/json/list"); - const json = await res.json(); - // Our ws client should be able to connect if the browser is running - const client = new WebSocket(json[0]["webSocketDebuggerUrl"]); - const promise = deferred(); - client.onopen = function () { - client.close(); - }; - client.onclose = function () { - promise.resolve(); - }; - await promise; - await Sinco.done(); - }, - ); - }); - - Rhum.testSuite("assertUrlIs()", () => { - Rhum.testCase("Works when an assertion is true", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com/features"); - await Sinco.assertUrlIs("https://chromestatus.com/features"); - await Sinco.done(); - }); - Rhum.testCase("Will fail when an assertion fails", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let originalErrMsg = ""; - try { - await Sinco.assertUrlIs("https://hella.com"); - } catch (error) { - originalErrMsg = error.message; - } - await Sinco.done(); - const msgArr = originalErrMsg.split("\n").filter((line) => { - return !!line === true && line.indexOf(" ") !== 0 && - line.indexOf("Values") < 0; - }); - Rhum.asserts.assertEquals( - msgArr[0], - "\x1b[31m\x1b[1m- https://\x1b[41m\x1b[37mchromestatus\x1b[31m\x1b[49m.com\x1b[41m\x1b[37m/\x1b[31m\x1b[49m\x1b[41m\x1b[37mfeatures\x1b[31m\x1b[49m", - ); - Rhum.asserts.assertEquals( - msgArr[1], - "\x1b[22m\x1b[39m\x1b[32m\x1b[1m+ https://\x1b[42m\x1b[37mhella\x1b[32m\x1b[49m.com", - ); - }); - }); - - Rhum.testSuite("goto()", () => { - Rhum.testCase("Successfully navigates when url is correct", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com/roadmap"); - await Sinco.assertUrlIs("https://chromestatus.com/roadmap"); - await Sinco.done(); - }); - Rhum.testCase( - "Throws an error when there was an error navving to the page", - async () => { - const Sinco = await ChromeClient.build(); - let msg = ""; - try { - await Sinco.goTo( - "https://hellaDOCSWOWThispagesurelycantexist.biscuit", - ); - } catch (err) { - msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals( - msg, - 'net::ERR_NAME_NOT_RESOLVED: Error for navigating to page "https://hellaDOCSWOWThispagesurelycantexist.biscuit"', - ); - }, - ); - }); - - Rhum.testSuite("assertSee()", () => { - Rhum.testCase( - "Assertion should work when text is present on page", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com/features"); - await Sinco.assertSee("Chrome Platform Status"); - await Sinco.done(); - }, - ); - Rhum.testCase( - "Assertion should NOT work when text is NOT present on page", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let errorMsg = ""; - try { - await Sinco.assertSee("Crumpets and tea init?"); - } catch (err) { - errorMsg = err.message; - } - await Sinco.done(); - const msgArr = errorMsg.split("\n").filter((line) => { - return !!line === true && line.indexOf(" ") !== 0 && - line.indexOf("Values") < 0; - }); - Rhum.asserts.assertEquals(msgArr[0].indexOf("- false") > -1, true); - Rhum.asserts.assertEquals(msgArr[1].indexOf("+ true") > -1, true); - }, - ); - }); - - Rhum.testSuite("click()", () => { - Rhum.testCase("It should allow clicking of elements", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.click('a[href="/roadmap"]'); - await Sinco.waitForPageChange(); - await Sinco.assertSee("Roadmap"); - await Sinco.done(); - }); - Rhum.testCase( - "It should throw an error when there is a syntax error", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.click("q;q"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: - "DOMException: Failed to execute 'querySelector' on 'Document': 'q;q' is not a valid selector.\n at :1:10: \"document.querySelector('q;q').click()\"", - }); - }, - ); - Rhum.testCase( - "It should throw an error when no element exists for the selector", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.click("a#dont-exist"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: - `TypeError: Cannot read properties of null (reading 'click')\n at :1:39: "document.querySelector('a#dont-exist').click()"`, - }); - }, - ); - }); - - Rhum.testSuite("evaluatePage()", () => { - Rhum.testCase("It should evaluate function on current frame", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://drash.land"); - const pageTitle = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - return document.title; - }); - await Sinco.done(); - Rhum.asserts.assertEquals(pageTitle, "Drash Land"); - }); - Rhum.testCase("It should evaluate string on current frame", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const parentConstructor = await Sinco.evaluatePage(`1 + 2`); - await Sinco.done(); - Rhum.asserts.assertEquals(parentConstructor, 3); - }); - }); - - Rhum.testSuite("getInputValue()", () => { - Rhum.testCase( - "It should get the value for the given input element", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.type('input[placeholder="Filter"]', "hello world"); - const val = await Sinco.getInputValue('input[placeholder="Filter"]'); - Rhum.asserts.assertEquals(val, "hello world"); - await Sinco.done(); - }, - ); - Rhum.testCase( - "It should throw an error when there is a syntax error", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.getInputValue("q;q"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: - `DOMException: Failed to execute 'querySelector' on 'Document': 'q;q' is not a valid selector.\n at :1:10: "document.querySelector('q;q').value"`, - }); - }, - ); - Rhum.testCase( - "It should throw an error when no element exists for the selector", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.getInputValue('input[name="dontexist"]'); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: - `TypeError: Cannot read properties of null (reading 'value')\n at :1:50: "document.querySelector('input[name="dontexist"]').value"`, - }); - }, - ); - Rhum.testCase( - "Should return undefined when element is not an input element", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let errMsg = ""; - try { - await Sinco.getInputValue('a[href="/roadmap"]'); - } catch (e) { - errMsg = e.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals( - errMsg, - 'a[href="/roadmap"] is either not an input element, or does not exist', - ); - }, - ); - }); - - Rhum.testSuite("type()", () => { - Rhum.testCase("It should set the value of the element", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.type('input[placeholder="Filter"]', "hello world"); - const val = await Sinco.getInputValue('input[placeholder="Filter"]'); - await Sinco.done(); - Rhum.asserts.assertEquals(val, "hello world"); - }); - Rhum.testCase( - "It should throw an error when there is a syntax error", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.type("q;q", "hello"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: - `DOMException: Failed to execute 'querySelector' on 'Document': 'q;q' is not a valid selector.\n at :1:10: "document.querySelector('q;q').value = "hello""`, - }); - }, - ); - Rhum.testCase( - "It should throw an error when no element exists for the selector", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.type("input#dont-exist", "qaloo"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: - `TypeError: Cannot set properties of null (setting 'value')\n at :1:50: "document.querySelector('input#dont-exist').value = "qaloo""`, - }); - }, - ); - }); - - Rhum.testSuite("done()", () => { - Rhum.testCase( - "Should close the sub process eg pid on users machine", - async () => { - // TODO(any): How do we do this? We could return the browser process rid and pid in the done() method, but what can we do with it? Eg checking `ps -a`, should not have the process process once we call `done()` - }, - ); - }); - - Rhum.testSuite("waitForPageChange()", () => { - Rhum.testCase("Waits for a page to change before continuing", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.assertUrlIs("https://chromestatus.com/features"); - await Sinco.click('a[href="/roadmap"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://chromestatus.com/roadmap"); - await Sinco.done(); - }); - }); - - Rhum.testSuite("takeScreenshot()", () => { - const ScreenshotsFolder = "Screenshots"; - - Rhum.beforeAll(() => { - try { - Deno.removeSync(ScreenshotsFolder, { recursive: true }); - } catch (_e) { - // - } finally { - Deno.mkdirSync(ScreenshotsFolder); - } - }); - - Rhum.testCase( - "Throws an error if provided path doesn't exist", - async () => { - let msg = ""; - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - try { - await Sinco.takeScreenshot("eieio"); - } catch (error) { - msg = error.message; - } - - Rhum.asserts.assertEquals( - msg, - `The provided folder path - eieio doesn't exist`, - ); - }, - ); - - Rhum.testCase( - "Takes a Screenshot with timestamp as filename if filename is not provided", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const fileName = await Sinco.takeScreenshot(ScreenshotsFolder); - await Sinco.done(); - - Rhum.asserts.assertEquals( - existsSync( - fileName, - ), - true, - ); - }, - ); - - Rhum.testCase( - "Takes Screenshot of only the element passed as selector and also quality(only if the image is jpeg)", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const fileName = await Sinco.takeScreenshot(ScreenshotsFolder, { - selector: "span", - quality: 50, - }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync( - fileName, - ), - true, - ); - }, - ); - - Rhum.testCase( - "Throws an error when format passed is jpeg(or default) and quality > than 100", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let msg = ""; - try { - await Sinco.takeScreenshot(ScreenshotsFolder, { quality: 999 }); - } catch (error) { - msg = error.message; - } - - await Sinco.done(); - Rhum.asserts.assertEquals( - msg, - "A quality value greater than 100 is not allowed.", - ); - }, - ); - - Rhum.testCase("Saves Screenshot with Given Filename", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.takeScreenshot(ScreenshotsFolder, { fileName: "Happy" }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync(`${ScreenshotsFolder}/Happy.jpeg`), - true, - ); - }); - - Rhum.testCase( - "Saves Screenshot with given format (jpeg | png)", - async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const fileName = await Sinco.takeScreenshot(ScreenshotsFolder, { - format: "png", - }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync( - fileName, - ), - true, - ); - }, - ); - - Rhum.testCase("Saves Screenshot with all options provided", async () => { - const Sinco = await ChromeClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.takeScreenshot(ScreenshotsFolder, { - fileName: "AllOpts", - selector: "span", - format: "jpeg", - quality: 100, - }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync(`${ScreenshotsFolder}/AllOpts.jpeg`), - true, - ); - }); - - Rhum.afterAll(() => { - Deno.removeSync(ScreenshotsFolder, { recursive: true }); - }); - }); - // Rhum.testSuite("waitForAnchorChange()", () => { - // Rhum.testCase("Waits for any anchor changes after an action", async () => { - // const Sinco = await ChromeClient.build(); - // await Sinco.goTo("https://chromestatus.com"); - // await Sinco.type('input[placeholder="Filter"]', "Gday"); - // await Sinco.waitForAnchorChange(); - // await Sinco.assertUrlIs("https://chromestatus.com/features#Gday"); - // await Sinco.done(); - // }); - // }); -}); - -Rhum.run(); diff --git a/tests/unit/client_test.ts b/tests/unit/client_test.ts new file mode 100644 index 00000000..0f32f8bd --- /dev/null +++ b/tests/unit/client_test.ts @@ -0,0 +1,109 @@ +import { assertEquals, deferred } from "../../deps.ts"; +import { buildFor } from "../../mod.ts"; +import { browserList } from "../browser_list.ts"; + +for (const browserItem of browserList) { + Deno.test( + "goTo() | Should go to the page", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const location = await page.location(); + await Sinco.done(); + assertEquals(location, "https://drash.land/"); + }, + ); + Deno.test( + "goTo() | Should error when page is invalid", + async () => { + const Sinco = await buildFor(browserItem.name); + let errMsg = ""; + try { + await Sinco.goTo("https://hhh"); + } catch (e) { + errMsg = e.message; + } + await Sinco.done(); + assertEquals(errMsg, browserItem.errors.page_name_not_resolved); + }, + ); + Deno.test( + `create() | Will start ${browserItem.name} headless as a subprocess`, + async () => { + const Sinco = await buildFor(browserItem.name); + const res = await fetch("http://localhost:9292/json/list"); + const json = await res.json(); + // Our ws client should be able to connect if the browser is running + const client = new WebSocket(json[0]["webSocketDebuggerUrl"]); + const promise = deferred(); + client.onopen = function () { + client.close(); + }; + client.onclose = function () { + promise.resolve(); + }; + await promise; + await Sinco.done(); + }, + ); + Deno.test( + "create() | Uses the port when passed in to the parameters", + async () => { + const Sinco = await buildFor(browserItem.name, { + debuggerPort: 9999, + }); + const res = await fetch("http://localhost:9999/json/list"); + const json = await res.json(); + // Our ws client should be able to connect if the browser is running + const client = new WebSocket(json[0]["webSocketDebuggerUrl"]); + const promise = deferred(); + client.onopen = function () { + client.close(); + }; + client.onclose = function () { + promise.resolve(); + }; + await promise; + await Sinco.done(); + }, + ); + Deno.test( + "create() | Uses the hostname when passed in to the parameters", + async () => { + // Unable to test properly, as windows doesnt like 0.0.0.0 or localhost, so the only choice is 127.0.0.1 but this is already the default + }, + ); + Deno.test( + "create() | Uses the binaryPath when passed in to the parameters", + async () => { + const Sinco = await buildFor(browserItem.name, { + //binaryPath: await browserItem.getPath(), + }); + + const res = await fetch("http://localhost:9292/json/list"); + const json = await res.json(); + // Our ws client should be able to connect if the browser is running + const client = new WebSocket(json[0]["webSocketDebuggerUrl"]); + const promise = deferred(); + client.onopen = function () { + client.close(); + }; + client.onclose = function () { + promise.resolve(); + }; + await promise; + await Sinco.done(); + }, + ); + + // Rhum.testSuite("waitForAnchorChange()", () => { + // Rhum.testCase("Waits for any anchor changes after an action", async () => { + // const Sinco = await ChromeClient.build(); + // await Sinco.goTo("https://chromestatus.com"); + // await Sinco.type('input[placeholder="Filter"]', "Gday"); + // await Sinco.waitForAnchorChange(); + // await Sinco.assertUrlIs("https://chromestatus.com/features#Gday"); + // await Sinco.done(); + // }); + // }); +} diff --git a/tests/unit/element_test.ts b/tests/unit/element_test.ts new file mode 100644 index 00000000..58298438 --- /dev/null +++ b/tests/unit/element_test.ts @@ -0,0 +1,54 @@ +import { buildFor } from "../../mod.ts"; +import { assertEquals } from "../../deps.ts"; +import { browserList } from "../browser_list.ts"; + +for (const browserItem of browserList) { + Deno.test("click() | It should allow clicking of elements", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const elem = await page.querySelector('a[href="/roadmap"]'); + await elem.click(); + await page.waitForPageChange(); + await page.assertSee("Roadmap"); + await Sinco.done(); + }); + + Deno.test("value | It should get the value for the given input element", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const elem = await page.querySelector('input[placeholder="Filter"]'); + await elem.value("hello world"); + const val = await elem.value(); + assertEquals(val, "hello world"); + await Sinco.done(); + }); + Deno.test( + "value | Should return empty when element is not an input element", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + let errMsg = ""; + const elem = await page.querySelector("div"); + try { + await elem.value; + } catch (e) { + errMsg = e.message; + } + await Sinco.done(); + assertEquals( + errMsg, + "", + ); + }, + ); + + Deno.test("value() | It should set the value of the element", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const elem = await page.querySelector('input[placeholder="Filter"]'); + await elem.value("hello world"); + const val = await elem.value(); + await Sinco.done(); + assertEquals(val, "hello world"); + }); +} diff --git a/tests/unit/firefox_client_test.ts b/tests/unit/firefox_client_test.ts deleted file mode 100644 index a57d351d..00000000 --- a/tests/unit/firefox_client_test.ts +++ /dev/null @@ -1,555 +0,0 @@ -import { Rhum } from "../deps.ts"; -import { FirefoxClient } from "../../mod.ts"; -import { getFirefoxPath } from "../../src/firefox_client.ts"; -import { existsSync } from "../../src/utility.ts"; -import { deferred } from "../../deps.ts"; - -Rhum.testPlan("tests/unit/firefox_client_test.ts", () => { - Rhum.testSuite("build()", () => { - Rhum.testCase("Will start firefox headless as a subprocess", async () => { - const Sinco = await FirefoxClient.build(); - const res = await fetch("http://127.0.0.1:9293/json/list"); - const json = await res.json(); - // Our ws client should be able to connect if the browser is running - const client = new WebSocket(json[1]["webSocketDebuggerUrl"]); - const promise = deferred(); - client.onopen = function () { - client.close(); - }; - client.onclose = function () { - promise.resolve(); - }; - await promise; - await Sinco.done(); - }); - Rhum.testCase( - "Uses the port when passed in to the parameters", - async () => { - const Sinco = await FirefoxClient.build({ - debuggerPort: 9999, - }); - const res = await fetch("http://localhost:9999/json/list"); - const json = await res.json(); - // Our ws client should be able to connect if the browser is running - const client = new WebSocket(json[1]["webSocketDebuggerUrl"]); - const promise = deferred(); - client.onopen = function () { - client.close(); - }; - client.onclose = function () { - promise.resolve(); - }; - await promise; - await Sinco.done(); - }, - ); - Rhum.testCase( - "Uses the hostname when passed in to the parameters", - async () => { - // Unable to test properly, as windows doesnt like 0.0.0.0 or localhost, so the only choice is 127.0.0.1 but this is already the default - }, - ); - Rhum.testCase( - "Uses the binaryPath when passed in to the parameters", - async () => { - const Sinco = await FirefoxClient.build({ - binaryPath: getFirefoxPath(), - }); - const res = await fetch("http://localhost:9293/json/list"); - const json = await res.json(); - // Our ws client should be able to connect if the browser is running - const client = new WebSocket(json[1]["webSocketDebuggerUrl"]); - const promise = deferred(); - client.onopen = function () { - client.close(); - }; - client.onclose = function () { - promise.resolve(); - }; - await promise; - await Sinco.done(); - }, - ); - Rhum.testCase( - "Should create and delete a temp path for the firefox profile", - async () => { - const Sinco = await FirefoxClient.build(); - const prop = Reflect.get(Sinco, "firefox_profile_path"); - const existsOnCreate = existsSync(prop); - await Sinco.done(); - const existsOnDestroy = existsSync(prop); - Rhum.asserts.assertEquals(existsOnCreate, true); - Rhum.asserts.assertEquals(existsOnDestroy, false); - }, - ); - }); - - Rhum.testSuite("assertUrlIs()", () => { - Rhum.testCase("Works when an assertion is true", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com/features"); - await Sinco.assertUrlIs("https://chromestatus.com/features"); - await Sinco.done(); - }); - Rhum.testCase("Will fail when an assertion fails", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let originalErrMsg = ""; - try { - await Sinco.assertUrlIs("https://hella.com"); - } catch (error) { - originalErrMsg = error.message; - } - await Sinco.done(); - const msgArr = originalErrMsg.split("\n").filter((line) => { - return !!line === true && line.indexOf(" ") !== 0 && - line.indexOf("Values") < 0; - }); - Rhum.asserts.assertEquals( - msgArr[0], - "\x1b[31m\x1b[1m- https://\x1b[41m\x1b[37mchromestatus\x1b[31m\x1b[49m.com\x1b[41m\x1b[37m/\x1b[31m\x1b[49m\x1b[41m\x1b[37mfeatures\x1b[31m\x1b[49m", - ); - Rhum.asserts.assertEquals( - msgArr[1], - "\x1b[22m\x1b[39m\x1b[32m\x1b[1m+ https://\x1b[42m\x1b[37mhella\x1b[32m\x1b[49m.com", - ); - }); - }); - - Rhum.testSuite("goto()", () => { - Rhum.testCase("Successfully navigates when url is correct", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com/roadmap"); - await Sinco.assertUrlIs("https://chromestatus.com/roadmap"); - await Sinco.done(); - }); - Rhum.testCase( - "Throws an error when there was an error navving to the page", - async () => { - const Sinco = await FirefoxClient.build(); - let msg = ""; - try { - await Sinco.goTo( - "https://hellaDOCSWOWThispagesurelycantexist.biscuit", - ); - } catch (err) { - msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals( - msg, - 'NS_ERROR_UNKNOWN_HOST: Error for navigating to page "https://hellaDOCSWOWThispagesurelycantexist.biscuit"', - ); - }, - ); - }); - - Rhum.testSuite("assertSee()", () => { - Rhum.testCase( - "Assertion should work when text is present on page", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com/features"); - await Sinco.assertSee("Chrome Platform Status"); - await Sinco.done(); - }, - ); - Rhum.testCase( - "Assertion should NOT work when text is NOT present on page", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let errorMsg = ""; - try { - await Sinco.assertSee("Crumpets and tea init?"); - } catch (err) { - errorMsg = err.message; - } - await Sinco.done(); - const msgArr = errorMsg.split("\n").filter((line) => { - return !!line === true && line.indexOf(" ") !== 0 && - line.indexOf("Values") < 0; - }); - Rhum.asserts.assertEquals(msgArr[0].indexOf("- false") > -1, true); - Rhum.asserts.assertEquals(msgArr[1].indexOf("+ true") > -1, true); - }, - ); - }); - - Rhum.testSuite("click()", () => { - Rhum.testCase("It should allow clicking of elements", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.click('a[href="/roadmap"]'); - await Sinco.waitForPageChange(); - await Sinco.assertSee("Roadmap"); - await Sinco.done(); - }); - Rhum.testCase( - "It should throw an error when there is a syntax error", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.click("q;q"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: "Document.querySelector: 'q;q' is not a valid selector", - }); - }, - ); - Rhum.testCase( - "It should throw an error when no element exists for the selector", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.click("a#dont-exist"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: `document.querySelector(...) is null`, - }); - }, - ); - }); - - Rhum.testSuite("evaluatePage()", () => { - Rhum.testCase("It should evaluate function on current frame", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://drash.land"); - const pageTitle = await Sinco.evaluatePage(() => { - // deno-lint-ignore no-undef - return document.title; - }); - await Sinco.done(); - Rhum.asserts.assertEquals(pageTitle, "Drash Land"); - }); - Rhum.testCase("It should evaluate string on current frame", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const parentConstructor = await Sinco.evaluatePage(`1 + 2`); - await Sinco.done(); - Rhum.asserts.assertEquals(parentConstructor, 3); - }); - }); - - Rhum.testSuite("getInputValue()", () => { - Rhum.testCase( - "It should get the value for the given input element", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.type('input[placeholder="Filter"]', "hello world"); - const val = await Sinco.getInputValue('input[placeholder="Filter"]'); - await Sinco.done(); - Rhum.asserts.assertEquals(val, "hello world"); - }, - ); - Rhum.testCase( - "It should throw an error when there is a syntax error", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.getInputValue("q;q"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: `Document.querySelector: 'q;q' is not a valid selector`, - }); - }, - ); - Rhum.testCase( - "It should throw an error when no element exists for the selector", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.getInputValue('input[name="dontexist"]'); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: `document.querySelector(...) is null`, - }); - }, - ); - Rhum.testCase( - "Should return undefined when element is not an input element", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let errMsg = ""; - try { - await Sinco.getInputValue('a[href="/roadmap"]'); - } catch (e) { - errMsg = e.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals( - errMsg, - 'a[href="/roadmap"] is either not an input element, or does not exist', - ); - }, - ); - }); - - Rhum.testSuite("type()", () => { - Rhum.testCase("It should set the value of the element", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.type('input[placeholder="Filter"]', "hello world"); - const val = await Sinco.getInputValue('input[placeholder="Filter"]'); - await Sinco.done(); - Rhum.asserts.assertEquals(val, "hello world"); - }); - Rhum.testCase( - "It should throw an error when there is a syntax error", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.type("q;q", "hello"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: `Document.querySelector: 'q;q' is not a valid selector`, - }); - }, - ); - Rhum.testCase( - "It should throw an error when no element exists for the selector", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const error = { - errored: false, - msg: "", - }; - try { - await Sinco.type("input#dont-exist", "qaloo"); - } catch (err) { - error.errored = true; - error.msg = err.message; - } - await Sinco.done(); - Rhum.asserts.assertEquals(error, { - errored: true, - msg: `document.querySelector(...) is null`, - }); - }, - ); - }); - - Rhum.testSuite("done()", () => { - Rhum.testCase( - "Should close the sub process eg pid on users machine", - async () => { - // TODO(any): How do we do this? We could return the browser process rid and pid in the done() method, but what can we do with it? Eg checking `ps -a`, should not have the process process once we call `done()` - }, - ); - }); - - Rhum.testSuite("waitForPageChange()", () => { - Rhum.testCase("Waits for a page to change before continuing", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.assertUrlIs("https://chromestatus.com/features"); - await Sinco.click('a[href="/roadmap"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://chromestatus.com/roadmap"); - await Sinco.done(); - }); - }); - - Rhum.testSuite("takeScreenshot()", () => { - const ScreenshotsFolder = "Screenshots"; - - Rhum.beforeAll(() => { - try { - Deno.removeSync(ScreenshotsFolder, { recursive: true }); - } catch (_e) { - // - } finally { - Deno.mkdirSync(ScreenshotsFolder); - } - }); - - Rhum.testCase( - "Throws an error if provided path doesn't exist", - async () => { - let msg = ""; - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - try { - await Sinco.takeScreenshot("eieio"); - } catch (error) { - msg = error.message; - } - - Rhum.asserts.assertEquals( - msg, - `The provided folder path - eieio doesn't exist`, - ); - }, - ); - - Rhum.testCase( - "Takes a Screenshot with timestamp as filename if filename is not provided", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const fileName = await Sinco.takeScreenshot(ScreenshotsFolder); - await Sinco.done(); - - Rhum.asserts.assertEquals( - existsSync( - fileName, - ), - true, - ); - }, - ); - - Rhum.testCase( - "Takes Screenshot of only the element passed as selector and also quality(only if the image is jpeg)", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const fileName = await Sinco.takeScreenshot(ScreenshotsFolder, { - selector: "span", - quality: 50, - }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync( - fileName, - ), - true, - ); - }, - ); - - Rhum.testCase( - "Throws an error when format passed is jpeg(or default) and quality > than 100", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - let msg = ""; - try { - await Sinco.takeScreenshot(ScreenshotsFolder, { quality: 999 }); - } catch (error) { - msg = error.message; - } - - await Sinco.done(); - Rhum.asserts.assertEquals( - msg, - "A quality value greater than 100 is not allowed.", - ); - }, - ); - - Rhum.testCase("Saves Screenshot with Given Filename", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.takeScreenshot(ScreenshotsFolder, { fileName: "Happy" }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync(`${ScreenshotsFolder}/Happy.jpeg`), - true, - ); - }); - - Rhum.testCase( - "Saves Screenshot with given format (jpeg | png)", - async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - const fileName = await Sinco.takeScreenshot(ScreenshotsFolder, { - format: "png", - }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync( - fileName, - ), - true, - ); - }, - ); - - Rhum.testCase("Saves Screenshot with all options provided", async () => { - const Sinco = await FirefoxClient.build(); - await Sinco.goTo("https://chromestatus.com"); - await Sinco.takeScreenshot(ScreenshotsFolder, { - fileName: "AllOpts", - selector: "span", - format: "jpeg", - quality: 100, - }); - await Sinco.done(); - Rhum.asserts.assertEquals( - existsSync(`${ScreenshotsFolder}/AllOpts.jpeg`), - true, - ); - }); - - Rhum.afterAll(() => { - Deno.removeSync(ScreenshotsFolder, { recursive: true }); - }); - }); - // Rhum.testSuite("waitForAnchorChange()", () => { - // Rhum.testCase("Waits for any anchor changes after an action", async () => { - // const Sinco = await FirefoxClient.build(); - // await Sinco.goTo("https://chromestatus.com"); - // await Sinco.type('input[placeholder="Filter"]', "Gday"); - // await Sinco.waitForAnchorChange(); - // await Sinco.assertUrlIs("https://chromestatus.com/features#Gday"); - // await Sinco.done(); - // }); - // }); -}); - -Rhum.run(); diff --git a/tests/unit/mod_test.ts b/tests/unit/mod_test.ts deleted file mode 100644 index e9fcd61a..00000000 --- a/tests/unit/mod_test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Rhum } from "../deps.ts"; -import { buildFor } from "../../mod.ts"; -import { assertEquals } from "../../deps.ts"; - -Rhum.testPlan("tests/unit/mod_test.ts", () => { - Rhum.testSuite("buildFor()", () => { - Rhum.testCase("Builds for firefox correctly", async () => { - const Sinco = await buildFor("firefox"); - await Sinco.goTo("https://chromestatus.com"); // Go to this page - await Sinco.assertUrlIs("https://chromestatus.com/features"); - await Sinco.type('input[placeholder="Filter"]', "Hello"); - const value = await Sinco.getInputValue('input[placeholder="Filter"]'); - assertEquals(value, "Hello"); - await Sinco.click('a[href="/roadmap"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://chromestatus.com/roadmap"); - await Sinco.assertSee("Roadmap"); - await Sinco.done(); - }); - Rhum.testCase("Builds for chrome correctly", async () => { - const Sinco = await buildFor("chrome"); - await Sinco.goTo("https://chromestatus.com"); // Go to this page - await Sinco.assertUrlIs("https://chromestatus.com/features"); - await Sinco.type('input[placeholder="Filter"]', "Hello"); - const value = await Sinco.getInputValue('input[placeholder="Filter"]'); - assertEquals(value, "Hello"); - await Sinco.click('a[href="/roadmap"]'); - await Sinco.waitForPageChange(); - await Sinco.assertUrlIs("https://chromestatus.com/roadmap"); - await Sinco.assertSee("Roadmap"); - await Sinco.done(); - }); - }); -}); - -Rhum.run(); diff --git a/tests/unit/page_test.ts b/tests/unit/page_test.ts new file mode 100644 index 00000000..13c0d538 --- /dev/null +++ b/tests/unit/page_test.ts @@ -0,0 +1,225 @@ +import { browserList } from "../browser_list.ts"; +const ScreenshotsFolder = "./tests/unit/Screenshots"; +import { buildFor } from "../../mod.ts"; +import { assertEquals } from "../../deps.ts"; +import { existsSync } from "../../src/utility.ts"; + +for (const browserItem of browserList) { + Deno.test( + "takeScreenshot() | Throws an error if provided path doesn't exist", + async () => { + let msg = ""; + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + try { + await page.takeScreenshot("eieio"); + } catch (error) { + msg = error.message; + } + + assertEquals( + msg, + `The provided folder path - eieio doesn't exist`, + ); + }, + ); + + Deno.test( + "takeScreenshot() | Takes a Screenshot with timestamp as filename if filename is not provided", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const fileName = await page.takeScreenshot(ScreenshotsFolder); + await Sinco.done(); + const exists = existsSync(fileName); + Deno.removeSync(fileName); + assertEquals( + exists, + true, + ); + }, + ); + + Deno.test( + "takeScreenshot() | Takes Screenshot of only the element passed as selector and also quality(only if the image is jpeg)", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const fileName = await page.takeScreenshot(ScreenshotsFolder, { + selector: "span", + quality: 50, + }); + await Sinco.done(); + const exists = existsSync(fileName); + Deno.removeSync(fileName); + assertEquals( + exists, + true, + ); + }, + ); + + Deno.test( + "Throws an error when format passed is jpeg(or default) and quality > than 100", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + let msg = ""; + try { + await page.takeScreenshot(ScreenshotsFolder, { quality: 999 }); + } catch (error) { + msg = error.message; + } + //await Sinco.done(); + assertEquals( + msg, + "A quality value greater than 100 is not allowed.", + ); + }, + ); + + Deno.test("Saves Screenshot with Given Filename", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const filename = await page.takeScreenshot(ScreenshotsFolder, { + fileName: "Happy", + }); + await Sinco.done(); + const exists = existsSync(filename); + Deno.removeSync(filename); + assertEquals( + exists, + true, + ); + }); + + Deno.test( + "takeScreenshot() | Saves Screenshot with given format (jpeg | png)", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const fileName = await page.takeScreenshot(ScreenshotsFolder, { + format: "png", + }); + await Sinco.done(); + const exists = existsSync(fileName); + assertEquals( + exists, + true, + ); + Deno.removeSync(fileName); + }, + ); + + Deno.test("takeScreenshot() | Saves Screenshot with all options provided", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const filename = await page.takeScreenshot(ScreenshotsFolder, { + fileName: "AllOpts", + selector: "span", + format: "jpeg", + quality: 100, + }); + await Sinco.done(); + const exists = existsSync(filename); + assertEquals( + exists, + true, + ); + Deno.removeSync(filename); + }); + + Deno.test("click() | It should allow clicking of elements", async () => { + }); + + Deno.test( + "waitForPageChange() | Waits for a page to change before continuing", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + assertEquals( + await page.location(), + "https://chromestatus.com/features", + ); + const elem = await page.querySelector('a[href="/roadmap"]'); + await elem.click(); + await page.waitForPageChange(); + assertEquals(await page.location(), "https://chromestatus.com/roadmap"); + await Sinco.done(); + }, + ); + + Deno.test( + "assertSee() | Assertion should work when text is present on page", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com/features"); + await page.assertSee("Chrome Platform Status"); + await Sinco.done(); + }, + ); + Deno.test( + "assertSee() | Assertion should NOT work when text is NOT present on page", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + let errorMsg = ""; + // test fails because page is its own instance, so page prop is true, but clients is still false + try { + await page.assertSee("Crumpets and tea init?"); + } catch (err) { + errorMsg = err.message; + } + await Sinco.done(); + const msgArr = errorMsg.split("\n").filter((line) => { + return !!line === true && line.indexOf(" ") !== 0 && + line.indexOf("Values") < 0; + }); + assertEquals(msgArr[0].indexOf("- false") > -1, true); + assertEquals(msgArr[1].indexOf("+ true") > -1, true); + }, + ); + + Deno.test( + "evaluate() | It should evaluate function on current frame", + async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + const pageTitle = await page.evaluate(() => { + // deno-lint-ignore no-undef + return document.title; + }); + await Sinco.done(); + assertEquals(pageTitle, "Drash Land"); + }, + ); + Deno.test("evaluate() | It should evaluate string on current frame", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://chromestatus.com"); + const parentConstructor = await page.evaluate(`1 + 2`); + await Sinco.done(); + assertEquals(parentConstructor, 3); + }); + + Deno.test("location() | Sets and gets the location", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://google.com"); + await page.location("https://drash.land"); + const location = await page.location(); + await Sinco.done(); + assertEquals(location, "https://drash.land/"); + }); + + Deno.test("cookie() | Sets and gets cookies", async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo("https://drash.land"); + await page.cookie({ + name: "user", + value: "ed", + "url": "https://drash.land", + }); + const cookies = await page.cookie(); + await Sinco.done(); + assertEquals(cookies, browserItem.cookies); + }); +}