Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat support existing ws endpoint #31

Merged
merged 6 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 39 additions & 9 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,35 @@ export interface BrowserOptions {
export class Browser {
#options: BrowserOptions;
#celestial: Celestial;
#process: Deno.ChildProcess;
#process: Deno.ChildProcess | null;
readonly pages: Page[] = [];

constructor(ws: WebSocket, process: Deno.ChildProcess, opts: BrowserOptions) {
constructor(
ws: WebSocket,
process: Deno.ChildProcess | null,
opts: BrowserOptions,
) {
this.#celestial = new Celestial(ws);
this.#process = process;
this.#options = opts;
}

/** Returns true if browser is connected remotely instead of using a subprocess */
get isRemoteConnection() {
return !this.#process;
}

/**
* Closes the browser and all of its pages (if any were opened). The Browser object itself is considered to be disposed and cannot be used anymore.
*/
async close() {
await this.#celestial.close();
this.#process.kill();
await this.#process.status;
this.#process?.kill();
await this.#process?.status;
// If we use a remote connection, then close all pages websockets
if (this.isRemoteConnection) {
await Promise.allSettled(this.pages.map((page) => page.close()));
}
}

/**
Expand All @@ -96,7 +109,8 @@ export class Browser {
url: "",
});
const browserWsUrl = new URL(this.#celestial.ws.url);
const wsUrl = `${browserWsUrl.origin}/devtools/page/${targetId}`;
const wsUrl =
`${browserWsUrl.origin}/devtools/page/${targetId}${browserWsUrl.search}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The browserWsUrl.search is required because some service may use this for authentication
For example browserless.io:

  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://chrome.browserless.io?token=******`,
  });

const websocket = new WebSocket(wsUrl);
await websocketReady(websocket);

Expand Down Expand Up @@ -143,13 +157,21 @@ export class Browser {
wsEndpoint() {
return this.#celestial.ws.url;
}

/**
* The browser's websocket ready state
*/
wsReadyState() {
return this.#celestial.ws.readyState;
}
lowlighter marked this conversation as resolved.
Show resolved Hide resolved
}

export interface LaunchOptions {
headless?: boolean;
path?: string;
product?: "chrome" | "firefox";
args?: string[];
browserWSEndpoint?: string;
lowlighter marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -159,17 +181,25 @@ export async function launch(opts?: LaunchOptions) {
const headless = opts?.headless ?? true;
const product = opts?.product ?? "chrome";
const args = opts?.args ?? [];
const browserWSEndpoint = opts?.browserWSEndpoint;
let path = opts?.path;

if (!path) {
path = await getBinary(product);
}

const options: BrowserOptions = {
headless,
product,
};

// Connect to endpoint directly if one was specified
if (browserWSEndpoint) {
const ws = new WebSocket(browserWSEndpoint);
await websocketReady(ws);
return new Browser(ws, null, options);
}

if (!path) {
path = await getBinary(product);
}

// Launch child process
const launch = new Deno.Command(path, {
args: [
Expand Down
16 changes: 12 additions & 4 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,19 @@ export class Page extends EventTarget {
* Close this page in the browser
*/
async close() {
const wsUrl = new URL(this.#celestial.ws.url);
const req = await fetch(`http://${wsUrl.host}/json/close/${this.#id}`);
const res = await req.text();
let success: boolean;
let res = "";
if (this.#browser.isRemoteConnection) {
await this.#celestial.close();
success = this.#browser.pages.includes(this);
} else {
const wsUrl = new URL(this.#celestial.ws.url);
const req = await fetch(`http://${wsUrl.host}/json/close/${this.#id}`);
res = await req.text();
success = res === "Target is closing";
}

if (res === "Target is closing") {
if (success) {
const index = this.#browser.pages.indexOf(this);
if (index > -1) {
this.#browser.pages.splice(index, 1);
Expand Down
36 changes: 36 additions & 0 deletions tests/existing_ws_endpoint_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { assertEquals } from "https://deno.land/[email protected]/assert/assert_equals.ts";
import { launch } from "../mod.ts";
import { assertThrows } from "https://deno.land/[email protected]/assert/assert_throws.ts";
import { assert } from "https://deno.land/[email protected]/assert/assert.ts";

Deno.test("Test existing ws endpoint", async () => {
// Spawn one browser instance and spawn another one connecting to the first one
const a = await launch();
const b = await launch({ browserWSEndpoint: a.wsEndpoint() });

// Test that second instance works without any process attached
const page = await b.newPage("http://example.com");
await page.waitForSelector("h1");
await page.close();
assert(!b.pages.includes(page));

// Close first instance and ensure that b instance is inactive too
await a.close();
assertEquals(a.wsReadyState(), WebSocket.CLOSED);
assertEquals(b.wsReadyState(), WebSocket.CLOSED);
});

Deno.test("Ensure pages are properly closed when closing existing endpoint", async () => {
// Spawn one browser instance and spawn another one connecting to the first one
const a = await launch();
const b = await launch({ browserWSEndpoint: a.wsEndpoint() });

// Ensure closing existing endpoint properly clean resources
await b.newPage("http://example.com");
await b.newPage("http://example.com");
await b.close();
assertThrows(() => b.pages[0].close(), "Page has already been closed");
assertThrows(() => b.pages[1].close(), "Page has already been closed");
assertEquals(b.wsReadyState(), WebSocket.CLOSED);
await a.close();
});
Loading