Skip to content

Commit

Permalink
Finish basic mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed Jun 20, 2024
1 parent 0ae24c5 commit 27ef01b
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 114 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"@types/ws": "^8",
"prettier": "^2.7.1",
"replayio": "workspace:^",
"typescript": "^5.4.5",
"ws": "^8.17.0"
"typescript": "^5.4.5"
},
"dependencies": {
"node-fetch": "^3.3.1"
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.10",
"jest": "^28.1.3",
"msw": "2.3.0-ws.rc-6",
"next": "^14.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -51,6 +52,7 @@
"@replayio/test-utils": "workspace:^",
"debug": "^4.3.4",
"stack-utils": "^2.0.6",
"undici": "^5.28.4",
"uuid": "^8.3.2",
"ws": "^8.13.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { FullConfig } from "@playwright/test";
import type {
addOriginalSourceResult,
addSourceMapResult,
beginRecordingUploadParameters,
beginRecordingUploadResult,
endRecordingUploadResult,
existsResult,
setAccessTokenResult,
setRecordingMetadataResult,
} from "@replayio/protocol";
import { WebSocket } from "undici";

async function globalSetup(_config: FullConfig) {
(globalThis as any).WebSocket = WebSocket;

const { http, ws, HttpResponse } = await import("msw");
const { setupServer } = await import("msw/node");

const server = setupServer(
http.get("*", async ({ request }) => {
switch (request.url) {
case "https://dispatch.replay.io/": {
return new HttpResponse("", {
status: 200,
});
}
default:
throw new Error(`Unexpected GET to: ${request.url}`);
}
}),
http.post("*", async ({ request }) => {
switch (request.url) {
case "https://api.replay.io/v1/graphql": {
const body = JSON.parse(await request.text());

switch (body.name) {
case "AddTestsToShard":
// TODO: we are interested in the data that we sent out here
return new HttpResponse(JSON.stringify({}));
case "CompleteTestRunShard":
return new HttpResponse(JSON.stringify({}));
case "CreateTestRunShard":
return new HttpResponse(
JSON.stringify({
data: {
startTestRunShard: {
testRunShardId: "test-run-shard-id",
},
},
})
);
default:
throw new Error(`Unexpected graphql operation name: ${body.name}`);
}
}
case "https://webhooks.replay.io/api/metrics":
return new HttpResponse(JSON.stringify({}));
default:
throw new Error(`Unexpected POST to: ${request.url}`);
}
}),
http.put("*", async ({ request }) => {
if (request.url.startsWith("https://app.replay.io/recording/")) {
return new HttpResponse(JSON.stringify({}));
}
throw new Error(`Unexpected PUT to: ${request.url}`);
})
);

const wsHandler = ws.link("wss://dispatch.replay.io").on("connection", ({ client, server }) => {
server.connect();

client.addEventListener("message", event => {
event.preventDefault();
const data = JSON.parse(String(event.data));
switch (data.method) {
case "Authentication.setAccessToken":
case "Internal.endRecordingUpload":
case "Internal.setRecordingMetadata":
case "Recording.addOriginalSource":
case "Recording.addSourceMap":
case "Resource.exists":
client.send(
JSON.stringify({
id: data.id,
result: {} satisfies
| addOriginalSourceResult
| addSourceMapResult
| endRecordingUploadResult
| existsResult
| setAccessTokenResult
| setRecordingMetadataResult,
})
);
return;
case "Internal.beginRecordingUpload": {
const params: beginRecordingUploadParameters = data.params;
client.send(
JSON.stringify({
id: data.id,
result: {
recordingId: params.recordingId!,
uploadLink: `https://app.replay.io/recording/${params.recordingId}`,
} satisfies beginRecordingUploadResult,
})
);
return;
}
default:
throw new Error(`Unexpected protocol method: ${data.method}`);
}
});
});

server.use(wsHandler);

server.listen();
}

export default globalSetup;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { defineConfig } from "@playwright/test";
import { devices as replayDevices, replayReporter } from "@replayio/playwright";

export default defineConfig({
reporter: [["line"], replayReporter({})],
globalSetup: "../_pw-utils/network-mock-global-setup.ts",
reporter: [
["line"],
replayReporter({
upload: true,
apiKey: "MOCKED_API_KEY",
}),
],
projects: [
{
name: "replay-chromium",
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const fixturesPages = path.join(__dirname, "fixtures-app", "app");
const playwrightPath = cp.spawnSync("yarn", ["bin", "playwright"]).stdout.toString().trim();

fs.readdirSync(fixturesPages).forEach(name => {
if (!fs.statSync(path.join(fixturesPages, name)).isDirectory()) {
if (name.startsWith("_") || !fs.statSync(path.join(fixturesPages, name)).isDirectory()) {
return;
}
it(name, async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/replay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"semver": "^7.5.4",
"superstruct": "^0.15.4",
"text-table": "^0.2.0",
"ws": "^7.5.0"
"undici": "^5.28.4"
},
"devDependencies": {
"@replay-cli/tsconfig": "workspace:^",
Expand Down
58 changes: 24 additions & 34 deletions packages/replay/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import dbg from "./debug";
import WebSocket from "ws";
import { WebSocket } from "undici";
import { defer } from "./utils";
import { Agent } from "http";

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

Expand Down Expand Up @@ -45,17 +44,15 @@ class ProtocolClient {
eventListeners = new Map();
nextMessageId = 1;

constructor(address: string, callbacks: Callbacks, agent?: Agent) {
debug("Creating WebSocket for %s with %o", address, { agent });
this.socket = new WebSocket(address, {
agent: agent,
});
constructor(address: string, callbacks: Callbacks) {
debug("Creating WebSocket for %s", address);
this.socket = new ((globalThis as any).WebSocket || WebSocket)(address);
this.callbacks = callbacks;

this.socket.on("open", callbacks.onOpen);
this.socket.on("close", callbacks.onClose);
this.socket.on("error", callbacks.onError);
this.socket.on("message", message => this.onMessage(message));
this.socket.addEventListener("open", () => callbacks.onOpen(this.socket));
this.socket.addEventListener("close", () => callbacks.onClose(this.socket));
this.socket.addEventListener("error", () => callbacks.onError(this.socket));
this.socket.addEventListener("message", message => this.onMessage(message.data));
}

close() {
Expand All @@ -79,31 +76,24 @@ class ProtocolClient {
async sendCommand<T = unknown, P extends object = Record<string, unknown>>(
method: string,
params: P,
data?: any,
sessionId?: string,
callback?: (err?: Error) => void
sessionId?: string
) {
const id = this.nextMessageId++;
debug("Sending command %s: %o", method, { id, params, sessionId });
this.socket.send(
JSON.stringify({
id,
method,
params,
binary: data ? true : undefined,
sessionId,
}),
err => {
if (!err && data) {
this.socket.send(data, callback);
} else {
if (err) {
debug("Received socket error: %s", err);
}
callback?.(err);
}
}
);
try {
this.socket.send(
JSON.stringify({
id,
method,
params,
sessionId,
})
);
} catch (err) {
debug("Received socket error: %s", err);
throw err;
}

const waiter = defer<T>();
this.pendingMessages.set(id, waiter);
return waiter.promise;
Expand All @@ -113,7 +103,7 @@ class ProtocolClient {
this.eventListeners.set(method, callback);
}

onMessage(contents: WebSocket.RawData) {
onMessage(contents: any) {
const msg = JSON.parse(String(contents));
debug("Received message %o", msg);
if (msg.id) {
Expand Down
16 changes: 6 additions & 10 deletions packages/replay/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getPackument } from "query-registry";
import { compare } from "semver";
import dbg from "./debug";
import { query } from "./graphql";
import { getCurrentVersion, getHttpAgent } from "./utils";
import { getCurrentVersion } from "./utils";

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

const agent = getHttpAgent(server, agentOptions);

if (recording.status == "crashed") {
debug("Uploading crash %o", recording);
await doUploadCrash(dir, server, recording, verbose, apiKey, agent);
await doUploadCrash(dir, server, recording, verbose, apiKey);
maybeLog(verbose, `Crash report uploaded for ${recording.id}`);
if (removeAssets) {
removeRecordingAssets(recording, { directory: dir });
Expand All @@ -343,7 +340,7 @@ async function doUploadRecording(

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

async function processUploadedRecording(recordingId: string, opts: Options) {
const server = getServer(opts);
const agent = getHttpAgent(server, opts.agentOptions);
const { verbose } = opts;
let apiKey = opts.apiKey;

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

const client = new ReplayClient();
if (!(await client.initConnection(server, apiKey, verbose, agent))) {
if (!(await client.initConnection(server, apiKey, verbose))) {
maybeLog(verbose, `Processing failed: can't connect to server ${server}`);
return false;
}
Expand Down
40 changes: 18 additions & 22 deletions packages/replay/src/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,27 @@ class ReplayClient {
client: ProtocolClient | undefined;
clientReady = defer<boolean>();

async initConnection(server: string, accessToken?: string, verbose?: boolean, agent?: Agent) {
async initConnection(server: string, accessToken?: string, verbose?: boolean) {
if (!this.client) {
let { resolve } = this.clientReady;
this.client = new ProtocolClient(
server,
{
onOpen: async () => {
try {
await this.client!.setAccessToken(accessToken);
resolve(true);
} catch (err) {
maybeLog(verbose, `Error authenticating with server: ${err}`);
resolve(false);
}
},
onClose() {
resolve(false);
},
onError(e) {
maybeLog(verbose, `Error connecting to server: ${e}`);
this.client = new ProtocolClient(server, {
onOpen: async () => {
try {
await this.client!.setAccessToken(accessToken);
resolve(true);
} catch (err) {
maybeLog(verbose, `Error authenticating with server: ${err}`);
resolve(false);
},
}
},
agent
);
onClose() {
resolve(false);
},
onError(e) {
maybeLog(verbose, `Error connecting to server: ${e}`);
resolve(false);
},
});
}

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

this.client
.sendCommand("Session.ensureProcessed", { level: "basic" }, null, sessionId)
.sendCommand("Session.ensureProcessed", { level: "basic" }, sessionId)
.then(() => waiter.resolve(null));

const error = await waiter.promise;
Expand Down
Loading

0 comments on commit 27ef01b

Please sign in to comment.