Skip to content

Commit 27ef01b

Browse files
committed
Finish basic mocking
1 parent 0ae24c5 commit 27ef01b

File tree

13 files changed

+443
-114
lines changed

13 files changed

+443
-114
lines changed

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
"@types/ws": "^8",
3333
"prettier": "^2.7.1",
3434
"replayio": "workspace:^",
35-
"typescript": "^5.4.5",
36-
"ws": "^8.17.0"
35+
"typescript": "^5.4.5"
3736
},
3837
"dependencies": {
3938
"node-fetch": "^3.3.1"

packages/playwright/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@types/uuid": "^8.3.4",
3131
"@types/ws": "^8.5.10",
3232
"jest": "^28.1.3",
33+
"msw": "2.3.0-ws.rc-6",
3334
"next": "^14.2.4",
3435
"react": "^18.3.1",
3536
"react-dom": "^18.3.1",
@@ -51,6 +52,7 @@
5152
"@replayio/test-utils": "workspace:^",
5253
"debug": "^4.3.4",
5354
"stack-utils": "^2.0.6",
55+
"undici": "^5.28.4",
5456
"uuid": "^8.3.2",
5557
"ws": "^8.13.0"
5658
},
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { FullConfig } from "@playwright/test";
2+
import type {
3+
addOriginalSourceResult,
4+
addSourceMapResult,
5+
beginRecordingUploadParameters,
6+
beginRecordingUploadResult,
7+
endRecordingUploadResult,
8+
existsResult,
9+
setAccessTokenResult,
10+
setRecordingMetadataResult,
11+
} from "@replayio/protocol";
12+
import { WebSocket } from "undici";
13+
14+
async function globalSetup(_config: FullConfig) {
15+
(globalThis as any).WebSocket = WebSocket;
16+
17+
const { http, ws, HttpResponse } = await import("msw");
18+
const { setupServer } = await import("msw/node");
19+
20+
const server = setupServer(
21+
http.get("*", async ({ request }) => {
22+
switch (request.url) {
23+
case "https://dispatch.replay.io/": {
24+
return new HttpResponse("", {
25+
status: 200,
26+
});
27+
}
28+
default:
29+
throw new Error(`Unexpected GET to: ${request.url}`);
30+
}
31+
}),
32+
http.post("*", async ({ request }) => {
33+
switch (request.url) {
34+
case "https://api.replay.io/v1/graphql": {
35+
const body = JSON.parse(await request.text());
36+
37+
switch (body.name) {
38+
case "AddTestsToShard":
39+
// TODO: we are interested in the data that we sent out here
40+
return new HttpResponse(JSON.stringify({}));
41+
case "CompleteTestRunShard":
42+
return new HttpResponse(JSON.stringify({}));
43+
case "CreateTestRunShard":
44+
return new HttpResponse(
45+
JSON.stringify({
46+
data: {
47+
startTestRunShard: {
48+
testRunShardId: "test-run-shard-id",
49+
},
50+
},
51+
})
52+
);
53+
default:
54+
throw new Error(`Unexpected graphql operation name: ${body.name}`);
55+
}
56+
}
57+
case "https://webhooks.replay.io/api/metrics":
58+
return new HttpResponse(JSON.stringify({}));
59+
default:
60+
throw new Error(`Unexpected POST to: ${request.url}`);
61+
}
62+
}),
63+
http.put("*", async ({ request }) => {
64+
if (request.url.startsWith("https://app.replay.io/recording/")) {
65+
return new HttpResponse(JSON.stringify({}));
66+
}
67+
throw new Error(`Unexpected PUT to: ${request.url}`);
68+
})
69+
);
70+
71+
const wsHandler = ws.link("wss://dispatch.replay.io").on("connection", ({ client, server }) => {
72+
server.connect();
73+
74+
client.addEventListener("message", event => {
75+
event.preventDefault();
76+
const data = JSON.parse(String(event.data));
77+
switch (data.method) {
78+
case "Authentication.setAccessToken":
79+
case "Internal.endRecordingUpload":
80+
case "Internal.setRecordingMetadata":
81+
case "Recording.addOriginalSource":
82+
case "Recording.addSourceMap":
83+
case "Resource.exists":
84+
client.send(
85+
JSON.stringify({
86+
id: data.id,
87+
result: {} satisfies
88+
| addOriginalSourceResult
89+
| addSourceMapResult
90+
| endRecordingUploadResult
91+
| existsResult
92+
| setAccessTokenResult
93+
| setRecordingMetadataResult,
94+
})
95+
);
96+
return;
97+
case "Internal.beginRecordingUpload": {
98+
const params: beginRecordingUploadParameters = data.params;
99+
client.send(
100+
JSON.stringify({
101+
id: data.id,
102+
result: {
103+
recordingId: params.recordingId!,
104+
uploadLink: `https://app.replay.io/recording/${params.recordingId}`,
105+
} satisfies beginRecordingUploadResult,
106+
})
107+
);
108+
return;
109+
}
110+
default:
111+
throw new Error(`Unexpected protocol method: ${data.method}`);
112+
}
113+
});
114+
});
115+
116+
server.use(wsHandler);
117+
118+
server.listen();
119+
}
120+
121+
export default globalSetup;

packages/playwright/tests/fixtures-app/app/basic/playwright.config.mjs renamed to packages/playwright/tests/fixtures-app/app/basic/playwright.config.mts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { defineConfig } from "@playwright/test";
22
import { devices as replayDevices, replayReporter } from "@replayio/playwright";
33

44
export default defineConfig({
5-
reporter: [["line"], replayReporter({})],
5+
globalSetup: "../_pw-utils/network-mock-global-setup.ts",
6+
reporter: [
7+
["line"],
8+
replayReporter({
9+
upload: true,
10+
apiKey: "MOCKED_API_KEY",
11+
}),
12+
],
613
projects: [
714
{
815
name: "replay-chromium",

packages/playwright/tests/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const fixturesPages = path.join(__dirname, "fixtures-app", "app");
66
const playwrightPath = cp.spawnSync("yarn", ["bin", "playwright"]).stdout.toString().trim();
77

88
fs.readdirSync(fixturesPages).forEach(name => {
9-
if (!fs.statSync(path.join(fixturesPages, name)).isDirectory()) {
9+
if (name.startsWith("_") || !fs.statSync(path.join(fixturesPages, name)).isDirectory()) {
1010
return;
1111
}
1212
it(name, async () => {

packages/replay/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"semver": "^7.5.4",
4949
"superstruct": "^0.15.4",
5050
"text-table": "^0.2.0",
51-
"ws": "^7.5.0"
51+
"undici": "^5.28.4"
5252
},
5353
"devDependencies": {
5454
"@replay-cli/tsconfig": "workspace:^",

packages/replay/src/client.ts

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import dbg from "./debug";
2-
import WebSocket from "ws";
2+
import { WebSocket } from "undici";
33
import { defer } from "./utils";
4-
import { Agent } from "http";
54

65
const debug = dbg("replay:protocol");
76

@@ -45,17 +44,15 @@ class ProtocolClient {
4544
eventListeners = new Map();
4645
nextMessageId = 1;
4746

48-
constructor(address: string, callbacks: Callbacks, agent?: Agent) {
49-
debug("Creating WebSocket for %s with %o", address, { agent });
50-
this.socket = new WebSocket(address, {
51-
agent: agent,
52-
});
47+
constructor(address: string, callbacks: Callbacks) {
48+
debug("Creating WebSocket for %s", address);
49+
this.socket = new ((globalThis as any).WebSocket || WebSocket)(address);
5350
this.callbacks = callbacks;
5451

55-
this.socket.on("open", callbacks.onOpen);
56-
this.socket.on("close", callbacks.onClose);
57-
this.socket.on("error", callbacks.onError);
58-
this.socket.on("message", message => this.onMessage(message));
52+
this.socket.addEventListener("open", () => callbacks.onOpen(this.socket));
53+
this.socket.addEventListener("close", () => callbacks.onClose(this.socket));
54+
this.socket.addEventListener("error", () => callbacks.onError(this.socket));
55+
this.socket.addEventListener("message", message => this.onMessage(message.data));
5956
}
6057

6158
close() {
@@ -79,31 +76,24 @@ class ProtocolClient {
7976
async sendCommand<T = unknown, P extends object = Record<string, unknown>>(
8077
method: string,
8178
params: P,
82-
data?: any,
83-
sessionId?: string,
84-
callback?: (err?: Error) => void
79+
sessionId?: string
8580
) {
8681
const id = this.nextMessageId++;
8782
debug("Sending command %s: %o", method, { id, params, sessionId });
88-
this.socket.send(
89-
JSON.stringify({
90-
id,
91-
method,
92-
params,
93-
binary: data ? true : undefined,
94-
sessionId,
95-
}),
96-
err => {
97-
if (!err && data) {
98-
this.socket.send(data, callback);
99-
} else {
100-
if (err) {
101-
debug("Received socket error: %s", err);
102-
}
103-
callback?.(err);
104-
}
105-
}
106-
);
83+
try {
84+
this.socket.send(
85+
JSON.stringify({
86+
id,
87+
method,
88+
params,
89+
sessionId,
90+
})
91+
);
92+
} catch (err) {
93+
debug("Received socket error: %s", err);
94+
throw err;
95+
}
96+
10797
const waiter = defer<T>();
10898
this.pendingMessages.set(id, waiter);
10999
return waiter.promise;
@@ -113,7 +103,7 @@ class ProtocolClient {
113103
this.eventListeners.set(method, callback);
114104
}
115105

116-
onMessage(contents: WebSocket.RawData) {
106+
onMessage(contents: any) {
117107
const msg = JSON.parse(String(contents));
118108
debug("Received message %o", msg);
119109
if (msg.id) {

packages/replay/src/main.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getPackument } from "query-registry";
44
import { compare } from "semver";
55
import dbg from "./debug";
66
import { query } from "./graphql";
7-
import { getCurrentVersion, getHttpAgent } from "./utils";
7+
import { getCurrentVersion } from "./utils";
88

99
// requiring v4 explicitly because it's the last version with commonjs support.
1010
// Should be upgraded to the latest when converting this code to es modules.
@@ -145,12 +145,11 @@ async function doUploadCrash(
145145
server: string,
146146
recording: RecordingEntry,
147147
verbose?: boolean,
148-
apiKey?: string,
149-
agent?: Agent
148+
apiKey?: string
150149
) {
151150
const client = new ReplayClient();
152151
maybeLog(verbose, `Starting crash data upload for ${recording.id}...`);
153-
if (!(await client.initConnection(server, apiKey, verbose, agent))) {
152+
if (!(await client.initConnection(server, apiKey, verbose))) {
154153
maybeLog(verbose, `Crash data upload failed: can't connect to server ${server}`);
155154
return null;
156155
}
@@ -327,11 +326,9 @@ async function doUploadRecording(
327326
apiKey = await readToken({ directory: dir });
328327
}
329328

330-
const agent = getHttpAgent(server, agentOptions);
331-
332329
if (recording.status == "crashed") {
333330
debug("Uploading crash %o", recording);
334-
await doUploadCrash(dir, server, recording, verbose, apiKey, agent);
331+
await doUploadCrash(dir, server, recording, verbose, apiKey);
335332
maybeLog(verbose, `Crash report uploaded for ${recording.id}`);
336333
if (removeAssets) {
337334
removeRecordingAssets(recording, { directory: dir });
@@ -343,7 +340,7 @@ async function doUploadRecording(
343340

344341
debug("Uploading recording %o", recording);
345342
const client = new ReplayClient();
346-
if (!(await client.initConnection(server, apiKey, verbose, agent))) {
343+
if (!(await client.initConnection(server, apiKey, verbose))) {
347344
handleUploadingError(`Cannot connect to server ${server}`, strict, verbose);
348345
return null;
349346
}
@@ -468,7 +465,6 @@ async function uploadRecording(id: string, opts: UploadOptions = {}) {
468465

469466
async function processUploadedRecording(recordingId: string, opts: Options) {
470467
const server = getServer(opts);
471-
const agent = getHttpAgent(server, opts.agentOptions);
472468
const { verbose } = opts;
473469
let apiKey = opts.apiKey;
474470

@@ -479,7 +475,7 @@ async function processUploadedRecording(recordingId: string, opts: Options) {
479475
}
480476

481477
const client = new ReplayClient();
482-
if (!(await client.initConnection(server, apiKey, verbose, agent))) {
478+
if (!(await client.initConnection(server, apiKey, verbose))) {
483479
maybeLog(verbose, `Processing failed: can't connect to server ${server}`);
484480
return false;
485481
}

packages/replay/src/upload.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,27 @@ class ReplayClient {
2121
client: ProtocolClient | undefined;
2222
clientReady = defer<boolean>();
2323

24-
async initConnection(server: string, accessToken?: string, verbose?: boolean, agent?: Agent) {
24+
async initConnection(server: string, accessToken?: string, verbose?: boolean) {
2525
if (!this.client) {
2626
let { resolve } = this.clientReady;
27-
this.client = new ProtocolClient(
28-
server,
29-
{
30-
onOpen: async () => {
31-
try {
32-
await this.client!.setAccessToken(accessToken);
33-
resolve(true);
34-
} catch (err) {
35-
maybeLog(verbose, `Error authenticating with server: ${err}`);
36-
resolve(false);
37-
}
38-
},
39-
onClose() {
40-
resolve(false);
41-
},
42-
onError(e) {
43-
maybeLog(verbose, `Error connecting to server: ${e}`);
27+
this.client = new ProtocolClient(server, {
28+
onOpen: async () => {
29+
try {
30+
await this.client!.setAccessToken(accessToken);
31+
resolve(true);
32+
} catch (err) {
33+
maybeLog(verbose, `Error authenticating with server: ${err}`);
4434
resolve(false);
45-
},
35+
}
4636
},
47-
agent
48-
);
37+
onClose() {
38+
resolve(false);
39+
},
40+
onError(e) {
41+
maybeLog(verbose, `Error connecting to server: ${e}`);
42+
resolve(false);
43+
},
44+
});
4945
}
5046

5147
return this.clientReady.promise;
@@ -151,7 +147,7 @@ class ReplayClient {
151147
this.client.setEventListener("Session.unprocessedRegions", () => {});
152148

153149
this.client
154-
.sendCommand("Session.ensureProcessed", { level: "basic" }, null, sessionId)
150+
.sendCommand("Session.ensureProcessed", { level: "basic" }, sessionId)
155151
.then(() => waiter.resolve(null));
156152

157153
const error = await waiter.promise;

0 commit comments

Comments
 (0)