Skip to content

Commit

Permalink
First stab at a Node.js CLI wrapping the Mocha Remote client (#212)
Browse files Browse the repository at this point in the history
* Improve client errors

* Start implementing a "mocha-remote-node" CLI
  • Loading branch information
kraenhansen authored Dec 3, 2024
1 parent ac0b16d commit 3cb6471
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 12 deletions.
85 changes: 80 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"bugs": "https://github.com/kraenhansen/mocha-remote/issues",
"license": "ISC",
"devDependencies": {
"@tsconfig/node16": "^16.1.3",
"@types/chai": "^4",
"@types/debug": "^4.1.12",
"@types/mocha": "^10.0.10",
Expand Down
24 changes: 17 additions & 7 deletions packages/client/src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ export enum ClientState {

const noop = () => { /* tumbleweed */};

function errorFromEvent(event: Event, fallbackMessage: string): Error {
if ("error" in event && event.error instanceof Error) {
return event.error;
} else if ("message" in event && typeof event.message === "string") {
return new Error(event.message);
} else {
return new Error(fallbackMessage);
}
}

export class Client extends ClientEventEmitter {

public static WebSocket: typeof WebSocket;
Expand Down Expand Up @@ -187,29 +197,29 @@ export class Client extends ClientEventEmitter {
this.ws = ws;
this._state = ClientState.CONNECTING;

const errorBeforeConnection = (e: Event) => reject(e);
const handleConnectionError = (e: Event) => reject(errorFromEvent(e, "Failed to connect"));

ws.addEventListener("close", this.handleClose);

ws.addEventListener("message", this.handleMessage);

ws.addEventListener("error", event => {
const { message } = event as ErrorEvent;
this.emit(ClientEvents.ERROR, new Error(message));
const error = errorFromEvent(event, "Failed to connect");
this.emit(ClientEvents.ERROR, error);
});

ws.addEventListener("open", () => {
this.debug(`Connected to ${ws.url}`);
this._state = ClientState.CONNECTED;
// No need to track errors before connection
ws.removeEventListener("error", errorBeforeConnection);
// No need to handle connection error now that we're connected
ws.removeEventListener("error", handleConnectionError);
this.emit("connection", ws);
resolve();
});

if (!this.config.autoReconnect) {
// Attaching this since the only failed state from connecting is
ws.addEventListener("error", errorBeforeConnection);
// Attaching this since the only failed state from connecting is when we're not reconnecting
ws.addEventListener("error", handleConnectionError);
}
})
}
Expand Down
7 changes: 7 additions & 0 deletions packages/client/src/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ function toJSON(value: Record<string, unknown>): Record<string, unknown> {
Object.assign(result, {
"type": "suite",
"$$total": value.total(),
// TODO: Implement a DeserializedSuite on the client side to properly reconstruct it
"suites": [],
"tests": [],
"_beforeAll": [],
"_beforeEach": [],
"_afterAll": [],
"_afterEach": [],
});
}
return result;
Expand Down
2 changes: 2 additions & 0 deletions packages/node/mocha-remote-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import "./dist/cli.js"
14 changes: 14 additions & 0 deletions packages/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "mocha-remote-node",
"version": "1.12.3",
"type": "module",
"description": "Node.js wrapper for the Mocha Remote Client",
"bin": "./mocha-remote-node.js",
"scripts": {
"start": "tsx src/cli.ts",
"build": "tsc"
},
"dependencies": {
"glob": "^10.4.5"
}
}
70 changes: 70 additions & 0 deletions packages/node/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os from "node:os";
import { globSync } from "glob";
import { Client } from "mocha-remote-client";

/* eslint-disable no-console */

// TODO: Add support for existing Mocha runtime options for "file handling"

/*
* File Handling
* --extension File extension(s) to load
* [array] [default: ["js","cjs","mjs"]]
* --file Specify file(s) to be loaded prior to root suite
* execution [array] [default: (none)]
* --ignore, --exclude Ignore file(s) or glob pattern(s)
* [array] [default: (none)]
* --recursive Look for tests in subdirectories [boolean]
* -r, --require Require module [array] [default: (none)]
* -S, --sort Sort test files [boolean]
* -w, --watch Watch files in the current working directory for
* changes [boolean]
* --watch-files List of paths or globs to watch [array]
* --watch-ignore List of paths or globs to exclude from watching
* [array] [default: ["node_modules",".git"]]
*/

interface ConnectionRefusedError extends AggregateError {
errors: { code: "ECONNREFUSED", address: string, port: number }[];
}

function isConnectionRefusedError(error: unknown): error is ConnectionRefusedError {
return error instanceof AggregateError && error.errors.every((error: unknown) => {
return error instanceof Error &&
"code" in error && error.code === "ECONNREFUSED" &&
"port" in error && typeof error.port === "number";
});
}

const client = new Client({
title: `Node.js v${process.versions.node} on ${os.platform()}`,
autoConnect: false,
autoReconnect: false,
async tests(context) {
Object.assign(global, {
environment: { ...context, node: true },
});
// TODO: Is there a more reliable way to get the interpreter and command skipped?
const [, , patterns] = process.argv;
const testPaths = globSync(patterns, { absolute: true });
for (const testPath of testPaths) {
await import(testPath);
}
},
});

try {
await client.connect();
} catch (error) {
process.exitCode = 1;
if (isConnectionRefusedError(error)) {
const attempts = error.errors.map(error => `${error.address}:${error.port}`);
const command = "npx mocha-remote -- mocha-remote-node src/*.test.ts";
const suggestion = "Are you wrapping the mocha-remote-node CLI with the mocha-remote?";
console.error(`Connection refused (tried ${attempts.join(" / ")}).\n${suggestion}\n${command}`);
} else if (error instanceof Error) {
console.error("Mocha Remote Client failed:", error.stack);
} else {
console.error("Mocha Remote Client failed:", error);
}
}
9 changes: 9 additions & 0 deletions packages/node/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": ["@tsconfig/node16"],
"compilerOptions": {
"outDir": "dist",
"target": "es2022",
"lib": ["es2022"]
},
"include": ["src"]
}

0 comments on commit 3cb6471

Please sign in to comment.