Skip to content

Commit

Permalink
Finally Completed All Merging
Browse files Browse the repository at this point in the history
more stabilizations

another commit

commit

feat: Support waiting for requests (drashland#126)

* feat: Support waiting for requests

* docblocks

* rm logging

* fmt

feat: Support setting files on inputs (drashland#118)

* feat: Support setting files on inputs

* add tests

* fix tests

* fix again

feat: Support arugments for evaluate() function callbacks (drashland#122)

* feat: Support arugments for evaluate() function callbacks

* lint

feat: Support dialogs (drashland#124)

* feat: Support dialogs

* rm logging

* fix(page): add period to end of error message

* Update server.ts

* Update server.ts

* Update page_test.ts

Co-Authored-By: Eric Crooks <eric.crooks@gmail.com>
  • Loading branch information
SnoCold and crookse committed Jun 4, 2022
1 parent 136b327 commit 9a2ad8b
Show file tree
Hide file tree
Showing 13 changed files with 649 additions and 155 deletions.
71 changes: 71 additions & 0 deletions src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,77 @@ export class Element {
this.#protocol = protocol;
}

/**
* Sets a file for a file input
*
* @param path - The remote path of the file to attach
*
* @example
* ```js
* import { resolve } from "https://deno.land/std@0.136.0/path/mod.ts";
* const fileInput = await page.querySelector("input[type='file']");
* await fileInput.file(resolve("./logo.png"));
* ```
*/
public async file(path: string): Promise<void> {
return await this.files(path);
}

/**
* Sets many files for a file input
*
* @param files - The list of remote files to attach
*
* @example
* ```js
* import { resolve } from "https://deno.land/std@0.136.0/path/mod.ts";
* const fileInput = await page.querySelector("input[type='file']");
* await fileInput.files(resolve("./logo.png"));
* ```
*/
public async files(...files: string[]) {
if (files.length > 1) {
const isMultiple = await this.#page.evaluate(
`${this.#method}('${this.#selector}').hasAttribute('multiple')`,
);
if (!isMultiple) {
throw new Error(
"Trying to set files on a file input without the 'multiple' attribute",
);
}
}

const name = await this.#page.evaluate(
`${this.#method}('${this.#selector}').nodeName`,
);
if (name !== "INPUT") {
throw new Error("Trying to set a file on an element that isnt an input");
}
const type = await this.#page.evaluate(
`${this.#method}('${this.#selector}').type`,
);
if (type !== "file") {
throw new Error(
'Trying to set a file on an input that is not of type "file"',
);
}

const { node } = await this.#protocol.send<
ProtocolTypes.DOM.DescribeNodeRequest,
ProtocolTypes.DOM.DescribeNodeResponse
>("DOM.describeNode", {
objectId: this.#objectId,
});
await this.#protocol.send<ProtocolTypes.DOM.SetFileInputFilesRequest, null>(
"DOM.setFileInputFiles",
{
files: files,
objectId: this.#objectId,
backendNodeId: node.backendNodeId,
},
);
}

/**
* Get the value of this element, or set the value
*
Expand Down
156 changes: 153 additions & 3 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Element } from "./element.ts";
import { Protocol as ProtocolClass } from "./protocol.ts";
import { Cookie, ScreenshotOptions } from "./interfaces.ts";
import { Client } from "./client.ts";
import type { Deferred } from "../deps.ts";

/**
* A representation of the page the client is on, allowing the client to action
Expand Down Expand Up @@ -45,6 +46,67 @@ export class Page {
return this.#protocol.socket;
}

/**
* Tells Sinco you are expecting a dialog, so Sinco can listen for the event,
* and when `.dialog()` is called, Sinco can accept or decline it at the right time
*
* @example
* ```js
* // Note that if `.click()` produces a dialog, do not await it.
* await page.expectDialog();
* await elem.click();
* await page.dialog(true, "my username is Sinco");
* ```
*/
public expectDialog() {
this.#protocol.notifications.set(
"Page.javascriptDialogOpening",
deferred(),
);
}

/**
* Interact with a dialog.
*
* Will throw if `.expectDialog()` was not called before.
* This is so Sino doesn't try to accept/decline a dialog before
* it opens.
*
* @example
* ```js
* // Note that if `.click()` produces a dialog, do not await it.
* await page.expectDialog();
* elem.click();
* await page.dialog(true, "my username is Sinco");
* ```
*
* @param accept - Whether to accept or dismiss the dialog
* @param promptText - The text to enter into the dialog prompt before accepting. Used only if this is a prompt dialog.
*/
public async dialog(accept: boolean, promptText?: string) {
const p = this.#protocol.notifications.get("Page.javascriptDialogOpening");
if (!p) {
throw new Error(
`Trying to accept or decline a dialog without you expecting one. ".expectDialog()" was not called beforehand.`,
);
}
await p;
const method = "Page.javascriptDialogClosed";
this.#protocol.notifications.set(method, deferred());
const body: Protocol.Page.HandleJavaScriptDialogRequest = {
accept,
};
if (promptText) {
body.promptText = promptText;
}
await this.#protocol.send<
Protocol.Page.HandleJavaScriptDialogRequest,
null
>("Page.handleJavaScriptDialog", body);
const closedPromise = this.#protocol.notifications.get(method);
await closedPromise;
}

/**
* Closes the page. After, you will not be able to interact with it
*/
Expand Down Expand Up @@ -94,6 +156,47 @@ export class Page {
return [];
}

/**
* Tell Sinco that you will be expecting to wait for a request
*/
public expectWaitForRequest() {
const requestWillBeSendMethod = "Network.requestWillBeSent";
this.#protocol.notifications.set(requestWillBeSendMethod, deferred());
}

/**
* Wait for a request to finish loading.
*
* Can be used to wait for:
* - Clicking a button that (via JS) will send a HTTO request via axios/fetch etc
* - Submitting an inline form
* - ... and many others
*/
public async waitForRequest() {
const params = await this.#protocol.notifications.get(
"Network.requestWillBeSent",
) as {
requestId: string;
};
if (!params) {
throw new Error(
`Unable to wait for a request because \`.expectWaitForRequest()\` was not called.`,
);
}
const { requestId } = params;
const method = "Network.loadingFinished";
this.#protocol.notifications.set(method, {
params: {
requestId,
},
promise: deferred(),
});
const result = this.#protocol.notifications.get(method) as unknown as {
promise: Deferred<never>;
};
await result.promise;
}

/**
* Either get the href/url for the page, or set the location
*
Expand Down Expand Up @@ -140,32 +243,79 @@ export class Page {
return "";
}

// deno-lint-ignore no-explicit-any
public async evaluate(command: string): Promise<any>;
public async evaluate(
// deno-lint-ignore no-explicit-any
pageFunction: (...args: any[]) => any | Promise<any>,
...args: unknown[]
// deno-lint-ignore no-explicit-any
): Promise<any>;
/**
* Invoke a function or string expression on the current frame.
*
* @param pageCommand - The function to be called or the line of code to execute.
* @param args - Only if pageCommand is a function. Arguments to pass to the command so you can use data that was out of scope
*
* @example
* ```js
* const user = { name: "Sinco" };
* const result1 = await page.evaluate((user: { name: string }) => {
* // Now we're able to use `user` and any other bits of data!
* return user.name;
* }, user) // "Sinco"
* const result2 = await page.evaluate((user: { name: string }, window: Window, answer: "yes") => {
* // Query dom
* // ...
*
* return {
* ...user,
* window,
* answer
* };
* }, user, window, "yes") // { name: "Sinco", window: ..., answer: "yes" }
* ```
*
* @returns The result of the evaluation
*/
async evaluate(
pageCommand: (() => unknown) | string,
// deno-lint-ignore no-explicit-any
pageCommand: ((...args: any[]) => unknown) | string,
...args: unknown[]
// As defined by the #protocol, the `value` is `any`
// deno-lint-ignore no-explicit-any
): Promise<any> {
function convertArgument(
this: Page,
arg: unknown,
): Protocol.Runtime.CallArgument {
if (typeof arg === "bigint") {
return { unserializableValue: `${arg.toString()}n` };
}
if (Object.is(arg, -0)) return { unserializableValue: "-0" };
if (Object.is(arg, Infinity)) return { unserializableValue: "Infinity" };
if (Object.is(arg, -Infinity)) {
return { unserializableValue: "-Infinity" };
}
if (Object.is(arg, NaN)) return { unserializableValue: "NaN" };
return { value: arg };
}

if (typeof pageCommand === "string") {
const result = await this.#protocol.send<
Protocol.Runtime.EvaluateRequest,
Protocol.Runtime.EvaluateResponse
>("Runtime.evaluate", {
expression: pageCommand,
returnByValue: true,
includeCommandLineAPI: true, // supports things like $x
});
await this.#checkForEvaluateErrorResult(result, pageCommand);
return result.result.value;
}

if (typeof pageCommand === "function") {
const a = await this.#protocol.send<
const { executionContextId } = await this.#protocol.send<
Protocol.Page.CreateIsolatedWorldRequest,
Protocol.Page.CreateIsolatedWorldResponse
>(
Expand All @@ -174,7 +324,6 @@ export class Page {
frameId: this.#frame_id,
},
);
const { executionContextId } = a;

const res = await this.#protocol.send<
Protocol.Runtime.CallFunctionOnRequest,
Expand All @@ -187,6 +336,7 @@ export class Page {
returnByValue: true,
awaitPromise: true,
userGesture: true,
arguments: args.map(convertArgument.bind(this)),
},
);
await this.#checkForEvaluateErrorResult(res, pageCommand.toString());
Expand Down
35 changes: 32 additions & 3 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export class Protocol {
*/
public notifications: Map<
string,
Deferred<Record<string, unknown>>
Deferred<Record<string, unknown>> | {
params: Record<string, unknown>;
promise: Deferred<Record<string, unknown>>;
}
> = new Map();

/**
Expand Down Expand Up @@ -126,9 +129,35 @@ export class Protocol {
}

const resolvable = this.notifications.get(message.method);
if (resolvable) {
if (!resolvable) {
return;
}
if ("resolve" in resolvable && "reject" in resolvable) {
resolvable.resolve(message.params);
}
if ("params" in resolvable && "promise" in resolvable) {
let allMatch = false;
Object.keys(resolvable.params).forEach((paramName) => {
if (
allMatch === true &&
(message.params[paramName] as string | number).toString() !==
(resolvable.params[paramName] as string | number).toString()
) {
allMatch = false;
return;
}
if (
(message.params[paramName] as string | number).toString() ===
(resolvable.params[paramName] as string | number).toString()
) {
allMatch = true;
}
});
if (allMatch) {
resolvable.promise.resolve(message.params);
}
return;
}
}
}

Expand All @@ -151,7 +180,7 @@ export class Protocol {
if (getFrameId) {
protocol.notifications.set("Runtime.executionContextCreated", deferred());
}
for (const method of ["Page", "Log", "Runtime"]) {
for (const method of ["Page", "Log", "Runtime", "Network"]) {
await protocol.send(`${method}.enable`);
}
if (getFrameId) {
Expand Down
2 changes: 2 additions & 0 deletions tests/deps.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * as Drash from "https://deno.land/x/drash@v2.5.4/mod.ts";
export { resolve } from "https://deno.land/std@0.136.0/path/mod.ts";
export { delay } from "https://deno.land/std@0.126.0/async/delay.ts";
4 changes: 2 additions & 2 deletions tests/integration/get_and_set_input_value_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assertEquals } from "../../deps.ts";
import { buildFor } from "../../mod.ts";
import { browserList } from "../browser_list.ts";
import { fetcher } from "../../src/utility.ts";
import { waiter } from "../../src/utility.ts";

const remote = Deno.args.includes("--remoteBrowser");

Expand All @@ -10,7 +10,7 @@ for (const browserItem of browserList) {
await t.step(
"Get and set input value - Tutorial for this feature in the docs should work",
async () => {
remote && await fetcher();
remote && await waiter();
const { browser, page } = await buildFor(browserItem.name, { remote });
await page.location("https://chromestatus.com");
const elem = await page.querySelector('input[placeholder="Filter"]');
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/getting_started_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildFor } from "../../mod.ts";
import { browserList } from "../browser_list.ts";
import { assertEquals } from "../../deps.ts";
import { fetcher } from "../../src/utility.ts";
import { waiter } from "../../src/utility.ts";

const remote = Deno.args.includes("--remoteBrowser");

Expand All @@ -11,7 +11,7 @@ for (const browserItem of browserList) {
"Tutorial for Getting Started in the docs should work",
async () => {
// Setup
remote && await fetcher();
remote && await waiter();
const { browser, page } = await buildFor(browserItem.name, { remote }); // also supports firefox
await page.location("https://drash.land"); // Go to this page

Expand Down
Loading

0 comments on commit 9a2ad8b

Please sign in to comment.