Skip to content

Commit

Permalink
refactor more
Browse files Browse the repository at this point in the history
  • Loading branch information
ebebbington committed May 15, 2024
1 parent c096860 commit 2d10639
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 160 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ jobs:
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
- name: Tests (remote)
- name: Remote tests
run: |
docker compose -f tests/integration/docker_test/docker-compose.yml up remotes -d
deno test -A tests/integration --config tsconfig.json --no-check=remote -- --remoteBrowser
deno test -A tests/integration --config tsconfig.json --no-check=remote -- --remoteBrowser
cd tests/integration/docker_test
docker-compose up -d remotes
docker exec drivers deno test -A --config tsconfig.json --no-check=remote tests/integration/remote_test.ts
console-tests:
runs-on: ubuntu-latest
Expand Down
64 changes: 59 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,64 @@

<img align="right" src="./logo.svg" alt="Drash Land - Sinco logo" height="150" style="max-height: 150px">

Sinco is a browser automation and testing tool for Deno.
Sinco is a browser automation and testing tool. What this means is, Sinco runs a
subprocess for Chrome, and will communicate to the process via the Chrome
Devtools Protocol, as the subprocess opens a WebSocket server that Sinco
connects to. This allows Sinco to spin up a new browser tab, go to certain
websites, click buttons and so much more, all programatically. All Sinco does is
runs a subprocess for Chrome, so you do not need to worry about it creating or
running any other processes.

View the full documentation at https://drash.land/sinco.
Sinco is used to run or test actions of a page in the browser. Similar to unit
and integration tests, Sinco can be used for "browser" tests.

In the event the documentation pages are not accessible, please view the raw
version of the documentation at
https://github.com/drashland/website-v2/tree/main/docs.
Some examples of what you can build are:

- Browser testing for your web application
- Web scraping
- Automating interactions with a website using code

Sinco is similar to the more well-known tools that achieve the same thing, such
as Puppeteer. What sets Sinco apart is:

- It is the first Deno browser automation tool
- It does not try to install a specific Chrome version on your computer
- It is transparent: It will use the browser and version you already have
installed.

Its maintainers have taken concepts from the following ...

- [Puppeteer](https://pptr.dev/) — following a similar API and used as
inspriration ... and mixed in their own concepts and practices such as ...

Developer UX Approachability Test-driven development Documentation-driven
development Transparency

## Documentation

### Getting Started

You use Sinco to build a subprocess (client) and interact with the page that has
been opened. This defaults to "about:blank".

```ts
import { build } from "...";
const { browser, page } = await build();
```

Be sure to always call `.close()` on the client once you've finished any actions
with it, to ensure you do not leave any hanging ops, For example, closing after
the last `browser.*` call or before assertions.

### Visiting Pages

You can do this by calling `.location()` on the page:

```ts
const { browser, page } = await build();
await page.location("https://some-url.com");
```

## Taking Screenshots

Utilise the `
35 changes: 30 additions & 5 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
import { Client } from "./src/client.ts";
import { BuildOptions, Cookie, ScreenshotOptions } from "./src/interfaces.ts";
import { Page } from "./src/page.ts";
import { getChromeArgs } from "./src/utility.ts";

export type { BuildOptions, Cookie, ScreenshotOptions };

const defaultOptions = {
hostname: "localhost",
debuggerPort: 9292,
binaryPath: undefined,
};

export async function build(
options: BuildOptions = {
hostname: "localhost",
debuggerPort: 9292,
binaryPath: undefined,
},
options: BuildOptions = defaultOptions,
): Promise<{
browser: Client;
page: Page;
}> {
if (!options.debuggerPort) options.debuggerPort = 9292;
if (!options.hostname) options.hostname = "localhost";
const buildArgs = getChromeArgs(options.debuggerPort);
const path = buildArgs.splice(0, 1)[0];
const command = new Deno.Command(path, {
args: buildArgs,
stderr: "piped",
stdout: "piped",
});
const browserProcess = command.spawn();

return await Client.create(
{
hostname: options.hostname,
port: options.debuggerPort,
},
browserProcess,
);
}

export async function connect(options: BuildOptions = defaultOptions) {
if (!options.debuggerPort) options.debuggerPort = 9292;
if (!options.hostname) options.hostname = "localhost";

return await Client.create(
{
hostname: options.hostname,
Expand Down
45 changes: 12 additions & 33 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { deferred } from "../deps.ts";
import { Page } from "./page.ts";
import { getChromeArgs } from "./utility.ts";

/**
* A way to interact with the headless browser instance.
Expand Down Expand Up @@ -31,7 +30,7 @@ export class Client {
/**
* The sub process that runs headless chrome
*/
readonly #browser_process: Deno.ChildProcess;
readonly #browser_process: Deno.ChildProcess | undefined;

#closed = false;

Expand All @@ -49,7 +48,7 @@ export class Client {
*/
constructor(
socket: WebSocket,
browserProcess: Deno.ChildProcess,
browserProcess: Deno.ChildProcess | undefined,
wsOptions: {
hostname: string;
port: number;
Expand All @@ -75,12 +74,17 @@ export class Client {
}

// Close browser process (also closes the ws endpoint, which in turn closes all sockets)
// Though if browser process isn't present (eg remote) then just close socket
const p = deferred();
this.#socket.onclose = () => p.resolve();
this.#browser_process.stderr.cancel();
this.#browser_process.stdout.cancel();
this.#browser_process.kill();
await this.#browser_process.status;
if (this.#browser_process) {
this.#browser_process.stderr.cancel();
this.#browser_process.stdout.cancel();
this.#browser_process.kill();
await this.#browser_process.status;
} else {
this.#socket.close();
}
await p;
this.#closed = true;

Expand All @@ -104,36 +108,11 @@ export class Client {
hostname: string;
port: number;
},
browserProcess: Deno.ChildProcess | undefined = undefined,
): Promise<{
browser: Client;
page: Page;
}> {
const buildArgs = getChromeArgs(wsOptions.port);
const path = buildArgs.splice(0, 1)[0];
const command = new Deno.Command(path, {
args: buildArgs,
stderr: "piped",
stdout: "piped",
});
const browserProcess = command.spawn();
// Old approach until we discovered we can always just use fetch
// // Get the main ws conn for the client - this loop is needed as the ws server isn't open until we get the listeneing on.
// // We could just loop on the fetch of the /json/list endpoint, but we could tank the computers resources if the endpoint
// // isn't up for another 10s, meaning however many fetch requests in 10s
// // Sometimes it takes a while for the "Devtools listening on ws://..." line to show on windows + firefox too
// import { TextLineStream } from "jsr:@std/streams";
// for await (
// const line of browserProcess.stderr.pipeThrough(new TextDecoderStream())
// .pipeThrough(new TextLineStream())
// ) { // Loop also needed before json endpoint is up
// const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
// if (!match) {
// continue;
// }
// browserWsUrl = line.split("on ")[1];
// break;
// }

// Wait until endpoint is ready and get a WS connection
// to the main socket
const p = deferred<WebSocket>();
Expand Down
2 changes: 0 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export interface BuildOptions {
hostname?: string;
/** The path to the binary of the browser executable, such as specifying an alternative chromium browser */
binaryPath?: string;
/** If the Browser is a remote process */
remote?: boolean;
}

export interface ScreenshotOptions {
Expand Down
4 changes: 1 addition & 3 deletions tests/integration/csrf_protected_pages_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import { assertEquals } from "../../deps.ts";

import { build } from "../../mod.ts";

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

Deno.test("csrf_protected_pages_test.ts", async (t) => {
await t.step(
`CSRF Protected Pages - Tutorial for this feature in the docs should work`,
async () => {
const { browser, page } = await build({ remote });
const { browser, page } = await build();
await page.location("https://drash.land");
await page.cookie({
name: "X-CSRF-TOKEN",
Expand Down
1 change: 1 addition & 0 deletions tests/integration/docker_test/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services:
- ../../../tsconfig.json:/var/www/docker-test/tsconfig.json
command: bash -c "tail -f /dev/null"
working_dir: /var/www/docker-test

remotes:
container_name: remotes
restart: always
Expand Down
11 changes: 5 additions & 6 deletions tests/integration/manipulate_page_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { assertEquals } from "../../deps.ts";
import { build } from "../../mod.ts";

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

Deno.test("manipulate_page_test.ts", async (t) => {
await t.step("Manipulate Webpage", async () => {
const { browser, page } = await build({ remote });
const { browser, page } = await build();
await page.location("https://drash.land");

const updatedBody = await page.evaluate(() => {
Expand All @@ -26,7 +24,7 @@ Deno.test("manipulate_page_test.ts", async (t) => {
await t.step(
"Evaluating a script - Tutorial for this feature in the documentation works",
async () => {
const { browser, page } = await build({ remote });
const { browser, page } = await build();
await page.location("https://drash.land");
const pageTitle = await page.evaluate(() => {
// deno-lint-ignore no-undef
Expand All @@ -49,8 +47,9 @@ Deno.test("manipulate_page_test.ts", async (t) => {
await browser.close();
assertEquals(pageTitle, "Drash Land");
assertEquals(sum, 11);
assertEquals(oldBodyLength, remote ? 5 : 3);
assertEquals(newBodyLength, remote ? 6 : 4);
// TODO :: Do this test but for remote as well
assertEquals(oldBodyLength, 3);
assertEquals(newBodyLength, 4);
},
);
});
79 changes: 79 additions & 0 deletions tests/integration/remote_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { assertEquals } from "../../deps.ts";
import { build, connect } from "../../mod.ts";
const serverAdd = `http://host.docker.internal:1447`;

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

Deno.test("manipulate_page_test.ts", async (t) => {
await t.step({
name: "Remote tests (various to test different aspects)",
ignore: !isRemote,
fn: async (t) => {
await t.step("Can open and close fine", async () => {
const { browser, page } = await connect({
hostname: "localhost",
debuggerPort: 9292,
});

// todo do soemthing

await browser.close();
});

await t.step("Can visit pages", async () => {
const { browser, page } = await connect({
hostname: "localhost",
debuggerPort: 9292,
});

// todo do soemthing

await browser.close();
});

await t.step("Can open and close fine", async () => {
const { browser, page } = await connect({
hostname: "localhost",
debuggerPort: 9292,
});

// todo do soemthing

await browser.close();
});

await t.step("Can visit pages", async () => {
const { browser, page } = await connect({
hostname: "localhost",
debuggerPort: 9292,
});

// todo do soemthing

await browser.close();
});

await t.step("Can evaluate", async () => {
const { browser, page } = await connect({
hostname: "localhost",
debuggerPort: 9292,
});

// todo do soemthing

await browser.close();
});

await t.step("Can click elements", async () => {
const { browser, page } = await connect({
hostname: "localhost",
debuggerPort: 9292,
});

// todo do soemthing

await browser.close();
});
},
});
});
Loading

0 comments on commit 2d10639

Please sign in to comment.