Skip to content

Commit 211b395

Browse files
committed
test: implement unit and e2e tests on repl instrumentation
1 parent 941dbb5 commit 211b395

13 files changed

Lines changed: 786 additions & 47 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>REPL fixture</title>
6+
</head>
7+
<body>
8+
<h1 id="title">REPL fixture page</h1>
9+
<button id="action" type="button">Click</button>
10+
<div id="message">Initial fixture text</div>
11+
12+
<script>
13+
document.getElementById("action").addEventListener("click", () => {
14+
document.getElementById("message").textContent = "Clicked from fixture";
15+
});
16+
</script>
17+
</body>
18+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use strict";
2+
3+
module.exports = {
4+
gridUrl: "local",
5+
headless: "new",
6+
sets: {
7+
default: { files: ["tests/**/*.test.[jt]s"] },
8+
},
9+
browsers: {
10+
chrome: {
11+
desiredCapabilities: {
12+
browserName: "chrome",
13+
"goog:chromeOptions": {
14+
args: ["--no-sandbox", "--disable-dev-shm-usage"],
15+
},
16+
},
17+
},
18+
},
19+
system: {
20+
workers: 1,
21+
mochaOpts: {
22+
timeout: 60000,
23+
},
24+
},
25+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use strict";
2+
3+
const assert = require("node:assert/strict");
4+
const path = require("node:path");
5+
const { pathToFileURL } = require("node:url");
6+
7+
// Used by REPL e2e assertions through generated context.
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
9+
const rootValue = 1000;
10+
const pageUrl = pathToFileURL(path.join(__dirname, "..", "page.html")).href;
11+
12+
// `localValue` is used by REPL e2e assertions through generated context.
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14+
it("opens repl before test body", async ({ browser, localValue = 234 }) => {
15+
await browser.url(pageUrl);
16+
17+
assert.equal(await browser.$("#title").getText(), "REPL fixture page");
18+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use strict";
2+
3+
const path = require("node:path");
4+
const { pathToFileURL } = require("node:url");
5+
6+
const pageUrl = pathToFileURL(path.join(__dirname, "..", "page.html")).href;
7+
8+
it("opens repl after failed test", async ({ browser }) => {
9+
await browser.url(pageUrl);
10+
await browser.$("#action").click();
11+
12+
throw new Error("Intentional failure for REPL on fail e2e");
13+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import assert from "node:assert/strict";
2+
import path from "node:path";
3+
import { pathToFileURL } from "node:url";
4+
5+
type TestContext = {
6+
browser: any;
7+
};
8+
9+
// Bound here so REPL e2e assertions can read it through generated context.
10+
const rootValue: number = 1000;
11+
void rootValue;
12+
13+
const pageUrl: string = pathToFileURL(path.join(__dirname, "..", "page.html")).href;
14+
15+
// @ts-expect-error Testplane passes a context object into Mocha test callbacks.
16+
it("opens repl from test code", async ({ browser }: TestContext): Promise<void> => {
17+
// Bound here so REPL e2e assertions can read it through generated context.
18+
const localValue: number = 234;
19+
void localValue;
20+
21+
await browser.url(pageUrl);
22+
await browser.switchToRepl();
23+
const anotherValue = 12222;
24+
console.log(anotherValue);
25+
await browser.$("#action").click();
26+
27+
assert.equal(await browser.$("#message").getText(), "Clicked from fixture");
28+
});

test/e2e/repl/repl-client.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use strict";
2+
3+
const net = require("node:net");
4+
5+
const HOST = "127.0.0.1";
6+
const PROMPT = "> ";
7+
const SERVER_CLOSED_MESSAGE = "The server was closed after the REPL was exited";
8+
const TIMEOUT = 30000;
9+
10+
exports.runReplCommand = async (port, code) => {
11+
const socket = await connect(port);
12+
13+
return new Promise((resolve, reject) => {
14+
let output = "";
15+
16+
const timeout = setTimeout(() => {
17+
socket.destroy();
18+
reject(new Error(`Timed out after ${TIMEOUT}ms waiting for REPL result`));
19+
}, TIMEOUT);
20+
21+
socket.setEncoding("utf8");
22+
socket.write(`${code}\n`);
23+
socket.on("data", chunk => {
24+
output += chunk;
25+
26+
if (output.endsWith(PROMPT)) {
27+
clearTimeout(timeout);
28+
socket.end();
29+
resolve(extractResult(output));
30+
}
31+
});
32+
socket.on("error", err => {
33+
clearTimeout(timeout);
34+
reject(err);
35+
});
36+
});
37+
};
38+
39+
exports.exitRepl = async port => {
40+
const socket = await connect(port);
41+
42+
return new Promise((resolve, reject) => {
43+
let output = "";
44+
let isDone = false;
45+
const timeout = setTimeout(() => {
46+
socket.destroy();
47+
reject(new Error(`Timed out after ${TIMEOUT}ms waiting for REPL exit`));
48+
}, TIMEOUT);
49+
const done = () => {
50+
if (isDone) {
51+
return;
52+
}
53+
54+
isDone = true;
55+
clearTimeout(timeout);
56+
socket.destroy();
57+
resolve();
58+
};
59+
60+
socket.setEncoding("utf8");
61+
socket.write(".exit\n");
62+
socket.on("data", chunk => {
63+
output += chunk;
64+
65+
if (output.includes(SERVER_CLOSED_MESSAGE)) {
66+
done();
67+
}
68+
});
69+
socket.on("close", done);
70+
socket.on("error", err => {
71+
if (isDone) {
72+
return;
73+
}
74+
75+
isDone = true;
76+
clearTimeout(timeout);
77+
reject(err);
78+
});
79+
});
80+
};
81+
82+
function connect(port) {
83+
const startTime = Date.now();
84+
85+
return new Promise((resolve, reject) => {
86+
const tryConnect = () => {
87+
const socket = net.createConnection({ host: HOST, port });
88+
89+
socket.once("connect", () => resolve(socket));
90+
socket.once("error", err => {
91+
socket.destroy();
92+
93+
if (Date.now() - startTime >= TIMEOUT) {
94+
reject(err);
95+
return;
96+
}
97+
98+
setTimeout(tryConnect, 100);
99+
});
100+
};
101+
102+
tryConnect();
103+
});
104+
}
105+
106+
function extractResult(rawOutput) {
107+
let result = rawOutput;
108+
109+
if (result.startsWith(PROMPT)) {
110+
result = result.slice(PROMPT.length);
111+
}
112+
113+
if (result.endsWith(PROMPT)) {
114+
result = result.slice(0, -PROMPT.length);
115+
}
116+
117+
return result.replace(/\n$/, "");
118+
}
119+
120+
if (require.main === module) {
121+
const [, , portArg, ...codeParts] = process.argv;
122+
const port = Number(portArg);
123+
124+
if (!Number.isInteger(port) || codeParts.length === 0) {
125+
console.error("Usage: node repl-client.js <port> <code>");
126+
process.exit(1);
127+
}
128+
129+
exports
130+
.runReplCommand(port, codeParts.join(" "))
131+
.then(result => process.stdout.write(result))
132+
.catch(err => {
133+
console.error(err.message);
134+
process.exit(1);
135+
});
136+
}

0 commit comments

Comments
 (0)