Skip to content

Commit 2d10639

Browse files
committed
refactor more
1 parent c096860 commit 2d10639

File tree

12 files changed

+235
-160
lines changed

12 files changed

+235
-160
lines changed

.github/workflows/master.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ jobs:
2121
docker exec drivers deno test -A --config tsconfig.json --no-check=remote tests/integration
2222
docker exec drivers deno test -A --config tsconfig.json --no-check=remote tests/unit
2323
24-
- name: Tests (remote)
24+
- name: Remote tests
2525
run: |
26-
docker compose -f tests/integration/docker_test/docker-compose.yml up remotes -d
27-
deno test -A tests/integration --config tsconfig.json --no-check=remote -- --remoteBrowser
28-
deno test -A tests/integration --config tsconfig.json --no-check=remote -- --remoteBrowser
26+
cd tests/integration/docker_test
27+
docker-compose up -d remotes
28+
docker exec drivers deno test -A --config tsconfig.json --no-check=remote tests/integration/remote_test.ts
2929
3030
console-tests:
3131
runs-on: ubuntu-latest

README.md

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,64 @@
66

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

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

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

13-
In the event the documentation pages are not accessible, please view the raw
14-
version of the documentation at
15-
https://github.com/drashland/website-v2/tree/main/docs.
20+
Some examples of what you can build are:
21+
22+
- Browser testing for your web application
23+
- Web scraping
24+
- Automating interactions with a website using code
25+
26+
Sinco is similar to the more well-known tools that achieve the same thing, such
27+
as Puppeteer. What sets Sinco apart is:
28+
29+
- It is the first Deno browser automation tool
30+
- It does not try to install a specific Chrome version on your computer
31+
- It is transparent: It will use the browser and version you already have
32+
installed.
33+
34+
Its maintainers have taken concepts from the following ...
35+
36+
- [Puppeteer](https://pptr.dev/) — following a similar API and used as
37+
inspriration ... and mixed in their own concepts and practices such as ...
38+
39+
Developer UX Approachability Test-driven development Documentation-driven
40+
development Transparency
41+
42+
## Documentation
43+
44+
### Getting Started
45+
46+
You use Sinco to build a subprocess (client) and interact with the page that has
47+
been opened. This defaults to "about:blank".
48+
49+
```ts
50+
import { build } from "...";
51+
const { browser, page } = await build();
52+
```
53+
54+
Be sure to always call `.close()` on the client once you've finished any actions
55+
with it, to ensure you do not leave any hanging ops, For example, closing after
56+
the last `browser.*` call or before assertions.
57+
58+
### Visiting Pages
59+
60+
You can do this by calling `.location()` on the page:
61+
62+
```ts
63+
const { browser, page } = await build();
64+
await page.location("https://some-url.com");
65+
```
66+
67+
## Taking Screenshots
68+
69+
Utilise the `

mod.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
11
import { Client } from "./src/client.ts";
22
import { BuildOptions, Cookie, ScreenshotOptions } from "./src/interfaces.ts";
33
import { Page } from "./src/page.ts";
4+
import { getChromeArgs } from "./src/utility.ts";
45

56
export type { BuildOptions, Cookie, ScreenshotOptions };
67

8+
const defaultOptions = {
9+
hostname: "localhost",
10+
debuggerPort: 9292,
11+
binaryPath: undefined,
12+
};
13+
714
export async function build(
8-
options: BuildOptions = {
9-
hostname: "localhost",
10-
debuggerPort: 9292,
11-
binaryPath: undefined,
12-
},
15+
options: BuildOptions = defaultOptions,
1316
): Promise<{
1417
browser: Client;
1518
page: Page;
1619
}> {
1720
if (!options.debuggerPort) options.debuggerPort = 9292;
1821
if (!options.hostname) options.hostname = "localhost";
22+
const buildArgs = getChromeArgs(options.debuggerPort);
23+
const path = buildArgs.splice(0, 1)[0];
24+
const command = new Deno.Command(path, {
25+
args: buildArgs,
26+
stderr: "piped",
27+
stdout: "piped",
28+
});
29+
const browserProcess = command.spawn();
30+
31+
return await Client.create(
32+
{
33+
hostname: options.hostname,
34+
port: options.debuggerPort,
35+
},
36+
browserProcess,
37+
);
38+
}
39+
40+
export async function connect(options: BuildOptions = defaultOptions) {
41+
if (!options.debuggerPort) options.debuggerPort = 9292;
42+
if (!options.hostname) options.hostname = "localhost";
43+
1944
return await Client.create(
2045
{
2146
hostname: options.hostname,

src/client.ts

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { deferred } from "../deps.ts";
22
import { Page } from "./page.ts";
3-
import { getChromeArgs } from "./utility.ts";
43

54
/**
65
* A way to interact with the headless browser instance.
@@ -31,7 +30,7 @@ export class Client {
3130
/**
3231
* The sub process that runs headless chrome
3332
*/
34-
readonly #browser_process: Deno.ChildProcess;
33+
readonly #browser_process: Deno.ChildProcess | undefined;
3534

3635
#closed = false;
3736

@@ -49,7 +48,7 @@ export class Client {
4948
*/
5049
constructor(
5150
socket: WebSocket,
52-
browserProcess: Deno.ChildProcess,
51+
browserProcess: Deno.ChildProcess | undefined,
5352
wsOptions: {
5453
hostname: string;
5554
port: number;
@@ -75,12 +74,17 @@ export class Client {
7574
}
7675

7776
// Close browser process (also closes the ws endpoint, which in turn closes all sockets)
77+
// Though if browser process isn't present (eg remote) then just close socket
7878
const p = deferred();
7979
this.#socket.onclose = () => p.resolve();
80-
this.#browser_process.stderr.cancel();
81-
this.#browser_process.stdout.cancel();
82-
this.#browser_process.kill();
83-
await this.#browser_process.status;
80+
if (this.#browser_process) {
81+
this.#browser_process.stderr.cancel();
82+
this.#browser_process.stdout.cancel();
83+
this.#browser_process.kill();
84+
await this.#browser_process.status;
85+
} else {
86+
this.#socket.close();
87+
}
8488
await p;
8589
this.#closed = true;
8690

@@ -104,36 +108,11 @@ export class Client {
104108
hostname: string;
105109
port: number;
106110
},
111+
browserProcess: Deno.ChildProcess | undefined = undefined,
107112
): Promise<{
108113
browser: Client;
109114
page: Page;
110115
}> {
111-
const buildArgs = getChromeArgs(wsOptions.port);
112-
const path = buildArgs.splice(0, 1)[0];
113-
const command = new Deno.Command(path, {
114-
args: buildArgs,
115-
stderr: "piped",
116-
stdout: "piped",
117-
});
118-
const browserProcess = command.spawn();
119-
// Old approach until we discovered we can always just use fetch
120-
// // 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.
121-
// // We could just loop on the fetch of the /json/list endpoint, but we could tank the computers resources if the endpoint
122-
// // isn't up for another 10s, meaning however many fetch requests in 10s
123-
// // Sometimes it takes a while for the "Devtools listening on ws://..." line to show on windows + firefox too
124-
// import { TextLineStream } from "jsr:@std/streams";
125-
// for await (
126-
// const line of browserProcess.stderr.pipeThrough(new TextDecoderStream())
127-
// .pipeThrough(new TextLineStream())
128-
// ) { // Loop also needed before json endpoint is up
129-
// const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
130-
// if (!match) {
131-
// continue;
132-
// }
133-
// browserWsUrl = line.split("on ")[1];
134-
// break;
135-
// }
136-
137116
// Wait until endpoint is ready and get a WS connection
138117
// to the main socket
139118
const p = deferred<WebSocket>();

src/interfaces.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ export interface BuildOptions {
55
hostname?: string;
66
/** The path to the binary of the browser executable, such as specifying an alternative chromium browser */
77
binaryPath?: string;
8-
/** If the Browser is a remote process */
9-
remote?: boolean;
108
}
119

1210
export interface ScreenshotOptions {

tests/integration/csrf_protected_pages_test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ import { assertEquals } from "../../deps.ts";
77

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

10-
const remote = Deno.args.includes("--remoteBrowser");
11-
1210
Deno.test("csrf_protected_pages_test.ts", async (t) => {
1311
await t.step(
1412
`CSRF Protected Pages - Tutorial for this feature in the docs should work`,
1513
async () => {
16-
const { browser, page } = await build({ remote });
14+
const { browser, page } = await build();
1715
await page.location("https://drash.land");
1816
await page.cookie({
1917
name: "X-CSRF-TOKEN",

tests/integration/docker_test/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ services:
1313
- ../../../tsconfig.json:/var/www/docker-test/tsconfig.json
1414
command: bash -c "tail -f /dev/null"
1515
working_dir: /var/www/docker-test
16+
1617
remotes:
1718
container_name: remotes
1819
restart: always

tests/integration/manipulate_page_test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { assertEquals } from "../../deps.ts";
22
import { build } from "../../mod.ts";
33

4-
const remote = Deno.args.includes("--remoteBrowser");
5-
64
Deno.test("manipulate_page_test.ts", async (t) => {
75
await t.step("Manipulate Webpage", async () => {
8-
const { browser, page } = await build({ remote });
6+
const { browser, page } = await build();
97
await page.location("https://drash.land");
108

119
const updatedBody = await page.evaluate(() => {
@@ -26,7 +24,7 @@ Deno.test("manipulate_page_test.ts", async (t) => {
2624
await t.step(
2725
"Evaluating a script - Tutorial for this feature in the documentation works",
2826
async () => {
29-
const { browser, page } = await build({ remote });
27+
const { browser, page } = await build();
3028
await page.location("https://drash.land");
3129
const pageTitle = await page.evaluate(() => {
3230
// deno-lint-ignore no-undef
@@ -49,8 +47,9 @@ Deno.test("manipulate_page_test.ts", async (t) => {
4947
await browser.close();
5048
assertEquals(pageTitle, "Drash Land");
5149
assertEquals(sum, 11);
52-
assertEquals(oldBodyLength, remote ? 5 : 3);
53-
assertEquals(newBodyLength, remote ? 6 : 4);
50+
// TODO :: Do this test but for remote as well
51+
assertEquals(oldBodyLength, 3);
52+
assertEquals(newBodyLength, 4);
5453
},
5554
);
5655
});

tests/integration/remote_test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { assertEquals } from "../../deps.ts";
2+
import { build, connect } from "../../mod.ts";
3+
const serverAdd = `http://host.docker.internal:1447`;
4+
5+
const isRemote = Deno.args.includes("--remote");
6+
7+
Deno.test("manipulate_page_test.ts", async (t) => {
8+
await t.step({
9+
name: "Remote tests (various to test different aspects)",
10+
ignore: !isRemote,
11+
fn: async (t) => {
12+
await t.step("Can open and close fine", async () => {
13+
const { browser, page } = await connect({
14+
hostname: "localhost",
15+
debuggerPort: 9292,
16+
});
17+
18+
// todo do soemthing
19+
20+
await browser.close();
21+
});
22+
23+
await t.step("Can visit pages", async () => {
24+
const { browser, page } = await connect({
25+
hostname: "localhost",
26+
debuggerPort: 9292,
27+
});
28+
29+
// todo do soemthing
30+
31+
await browser.close();
32+
});
33+
34+
await t.step("Can open and close fine", async () => {
35+
const { browser, page } = await connect({
36+
hostname: "localhost",
37+
debuggerPort: 9292,
38+
});
39+
40+
// todo do soemthing
41+
42+
await browser.close();
43+
});
44+
45+
await t.step("Can visit pages", async () => {
46+
const { browser, page } = await connect({
47+
hostname: "localhost",
48+
debuggerPort: 9292,
49+
});
50+
51+
// todo do soemthing
52+
53+
await browser.close();
54+
});
55+
56+
await t.step("Can evaluate", async () => {
57+
const { browser, page } = await connect({
58+
hostname: "localhost",
59+
debuggerPort: 9292,
60+
});
61+
62+
// todo do soemthing
63+
64+
await browser.close();
65+
});
66+
67+
await t.step("Can click elements", async () => {
68+
const { browser, page } = await connect({
69+
hostname: "localhost",
70+
debuggerPort: 9292,
71+
});
72+
73+
// todo do soemthing
74+
75+
await browser.close();
76+
});
77+
},
78+
});
79+
});

0 commit comments

Comments
 (0)