diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index b70ee0c6..62c9d0ab 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -18,8 +18,8 @@ jobs: run: | 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 + docker exec drivers deno test -A --config tsconfig.json --no-check=remote tests/integration + docker exec drivers deno test -A --config tsconfig.json --no-check=remote tests/unit tests: @@ -38,11 +38,11 @@ jobs: - name: Run Integration Tests run: | - deno test -A tests/integration --config tsconfig.json + deno test -A tests/integration --config tsconfig.json --no-check=remote - name: Run Unit Tests run: | - deno test -A --config tsconfig.json tests/unit + deno test -A --config tsconfig.json tests/unit --no-check=remote linter: # Only one OS is required since fmt is cross platform diff --git a/deps.ts b/deps.ts index 9451a33c..a3c3a060 100644 --- a/deps.ts +++ b/deps.ts @@ -1,6 +1,10 @@ import type Protocol from "https://unpkg.com/devtools-protocol@0.0.818844/types/protocol.d.ts"; export { Protocol }; -export { assertEquals } from "https://deno.land/std@0.118.0/testing/asserts.ts"; +export { + assertEquals, + AssertionError, + assertThrows, +} from "https://deno.land/std@0.118.0/testing/asserts.ts"; export { readLines } from "https://deno.land/std@0.118.0/io/mod.ts"; export { deferred } from "https://deno.land/std@0.118.0/async/deferred.ts"; export type { Deferred } from "https://deno.land/std@0.118.0/async/deferred.ts"; diff --git a/src/client.ts b/src/client.ts index a66bea03..7c05ed87 100644 --- a/src/client.ts +++ b/src/client.ts @@ -142,6 +142,7 @@ export class Client { ); await protocol.sendWebSocketMessage("Page.enable"); await protocol.sendWebSocketMessage("Runtime.enable"); + await protocol.sendWebSocketMessage("Log.enable"); return new Client(protocol); } } diff --git a/src/page.ts b/src/page.ts index d6ed5312..848b9221 100644 --- a/src/page.ts +++ b/src/page.ts @@ -1,4 +1,4 @@ -import { assertEquals, deferred, Protocol } from "../deps.ts"; +import { assertEquals, AssertionError, deferred, Protocol } from "../deps.ts"; import { existsSync, generateTimestamp } from "./utility.ts"; import { Element } from "./element.ts"; import { Protocol as ProtocolClass } from "./protocol.ts"; @@ -185,6 +185,59 @@ export class Page { return new Element("document.querySelector", selector, this); } + /** + * Assert that there are no errors in the developer console, such as: + * - 404's (favicon for example) + * - Issues with JavaScript files + * - etc + * + * @param exceptions - A list of strings that if matched, will be ignored such as ["favicon.ico"] if you want/need to ignore a 404 error for this file + * + * @throws AssertionError + */ + public async assertNoConsoleErrors(exceptions: string[] = []) { + const forMessages = deferred(); + let notifCount = 0; + // deno-lint-ignore no-this-alias + const self = this; + const interval = setInterval(function () { + const notifs = self.#protocol.console_errors; + // If stored notifs is greater than what we've got, then + // more notifs are being sent to us, so wait again + if (notifs.length > notifCount) { + notifCount = notifs.length; + return; + } + // Otherwise, we have not gotten anymore notifs in the last .5s + clearInterval(interval); + forMessages.resolve(); + }, 1000); + await forMessages; + const errorNotifs = this.#protocol.console_errors; + const filteredNotifs = !exceptions.length + ? errorNotifs + : errorNotifs.filter((notif) => { + const notifCanBeIgnored = exceptions.find((exception) => { + if (notif.includes(exception)) { + return true; + } + return false; + }); + if (notifCanBeIgnored) { + return false; + } + return true; + }); + if (!filteredNotifs.length) { + return; + } + await this.#protocol.done(); + throw new AssertionError( + "Expected console to show no errors. Instead got:\n" + + filteredNotifs.join("\n"), + ); + } + /** * 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 diff --git a/src/protocol.ts b/src/protocol.ts index 77a6e269..f79a22b2 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1,6 +1,7 @@ import { Deferred, deferred } from "../deps.ts"; import { existsSync } from "./utility.ts"; import type { Browsers } from "./types.ts"; +import { Protocol as ProtocolTypes } from "../deps.ts"; interface MessageResponse { // For when we send an event to get one back, eg running a JS expression id: number; @@ -10,7 +11,7 @@ interface MessageResponse { // For when we send an event to get one back, eg run interface NotificationResponse { // Not entirely sure when, but when we send the `Network.enable` method method: string; - params: unknown; + params: Record; } export class Protocol { @@ -58,6 +59,11 @@ export class Protocol { */ public browser_process_closed = false; + /** + * Map of notifications, where the key is the method and the value is an array of the events + */ + public console_errors: string[] = []; + constructor( socket: WebSocket, browserProcess: Deno.Process, @@ -197,22 +203,42 @@ export class Protocol { ) { 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(); - } + if (!resolvable) { + return; + } + 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); } + resolvable.resolve(message.result); + } + if ("error" in message) { // error response + resolvable.resolve(message.error); } } if ("method" in message) { // Notification response + // Store certain methods for if we need to query them later + if (message.method === "Runtime.exceptionThrown") { + const params = message + .params as unknown as ProtocolTypes.Runtime.ExceptionThrownEvent; + const errorMessage = params.exceptionDetails.exception?.description; + if (errorMessage) { + this.console_errors.push(errorMessage); + } + } + if (message.method === "Log.entryAdded") { + const params = message + .params as unknown as ProtocolTypes.Log.EntryAddedEvent; + if (params.entry.level === "error") { + const errorMessage = params.entry.text; + if (errorMessage) { + this.console_errors.push(errorMessage); + } + } + } const resolvable = this.notification_resolvables.get(message.method); if (resolvable) { resolvable.resolve(); diff --git a/src/utility.ts b/src/utility.ts index ab398a53..a2ee1933 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -65,8 +65,8 @@ export function getChromePath(): string { export function getChromeArgs(port: number, binaryPath?: string): string[] { return [ binaryPath || getChromePath(), - "--headless", "--remote-debugging-port=" + port, + "--headless", "--disable-gpu", "--no-sandbox", ]; diff --git a/tests/deps.ts b/tests/deps.ts index 5a196bcd..6346aed6 100644 --- a/tests/deps.ts +++ b/tests/deps.ts @@ -1 +1,2 @@ export { Rhum } from "https://deno.land/x/rhum@v1.1.12/mod.ts"; +export * as Drash from "https://deno.land/x/drash@v2.2.0/mod.ts"; diff --git a/tests/server.ts b/tests/server.ts new file mode 100644 index 00000000..f0277a97 --- /dev/null +++ b/tests/server.ts @@ -0,0 +1,24 @@ +import { Drash } from "./deps.ts"; + +class HomeResource extends Drash.Resource { + public paths = ["/"]; + public GET(_request: Drash.Request, response: Drash.Response) { + response.html( + "", + ); + } +} +class JSResource extends Drash.Resource { + public paths = ["/.*\.js"]; + public GET(_request: Drash.Request, response: Drash.Response) { + response.text("callUser()"); + response.headers.set("content-type", "application/javascript"); + } +} + +export const server = new Drash.Server({ + resources: [HomeResource, JSResource], + protocol: "http", + port: 1447, + hostname: "localhost", +}); diff --git a/tests/unit/page_test.ts b/tests/unit/page_test.ts index 13c0d538..f76110db 100644 --- a/tests/unit/page_test.ts +++ b/tests/unit/page_test.ts @@ -3,6 +3,7 @@ const ScreenshotsFolder = "./tests/unit/Screenshots"; import { buildFor } from "../../mod.ts"; import { assertEquals } from "../../deps.ts"; import { existsSync } from "../../src/utility.ts"; +import { server } from "../server.ts"; for (const browserItem of browserList) { Deno.test( @@ -222,4 +223,80 @@ for (const browserItem of browserList) { await Sinco.done(); assertEquals(cookies, browserItem.cookies); }); + + Deno.test(`[${browserItem.name}] assertNoConsoleErrors() | Should throw when errors`, async () => { + server.run(); + const Sinco = await buildFor(browserItem.name); + // I (ed) knows this page shows errors, but if we ever need to change it in the future, + // can always spin up a drash web app and add errors in the js to produce console errors + const page = await Sinco.goTo( + server.address, + ); + let errMsg = ""; + try { + await page.assertNoConsoleErrors(); + } catch (e) { + errMsg = e.message; + } + await Sinco.done(); + await server.close(); + try { + assertEquals( + errMsg, + `Expected console to show no errors. Instead got: +ReferenceError: callUser is not defined + at http://localhost:1447/index.js:1:1 +Failed to load resource: the server responded with a status of 404 (Not Found)`, + ); + } catch (_e) { + assertEquals( + errMsg, + `Expected console to show no errors. Instead got: +Failed to load resource: the server responded with a status of 404 (Not Found) +ReferenceError: callUser is not defined + at http://localhost:1447/index.js:1:1 +Failed to load resource: the server responded with a status of 404 (Not Found)`, + ); + } + }); + + Deno.test(`[${browserItem.name}] assertNoConsoleErrors() | Should not throw when no errors`, async () => { + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo( + "https://drash.land", + ); + await page.assertNoConsoleErrors(); + await Sinco.done(); + }); + + Deno.test(`[${browserItem.name}] assertNoConsoleErrors() | Should exclude messages`, async () => { + server.run(); + const Sinco = await buildFor(browserItem.name); + const page = await Sinco.goTo( + server.address, + ); + let errMsg = ""; + try { + await page.assertNoConsoleErrors(["callUser"]); + } catch (e) { + errMsg = e.message; + } + await server.close(); + await Sinco.done(); + try { + assertEquals( + errMsg, + `Expected console to show no errors. Instead got: +Failed to load resource: the server responded with a status of 404 (Not Found) +Failed to load resource: the server responded with a status of 404 (Not Found)`, + ); + } catch (_e) { + assertEquals( + errMsg, + `Expected console to show no errors. Instead got: +Failed to load resource: the server responded with a status of 404 (Not Found)`, + ); + } + }); + break; }