mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
4e04921205
Co-authored-by: Aiden Bai <aiden@million.dev>
166 lines
5.9 KiB
TypeScript
166 lines
5.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
|
|
|
|
vi.mock("../src/cli/utils/record-metric.js", () => ({
|
|
recordCount: vi.fn(),
|
|
}));
|
|
|
|
import { GitBaseBranchInvalid, ReactDoctorError } from "@react-doctor/core";
|
|
import { METRIC } from "../src/cli/utils/constants.js";
|
|
import { buildErrorIssueUrl, handleError, handleUserError } from "../src/cli/utils/handle-error.js";
|
|
import { recordCount } from "../src/cli/utils/record-metric.js";
|
|
|
|
const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
|
|
const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
|
|
|
|
interface EnvironmentSnapshot {
|
|
[environmentVariableName: string]: string | undefined;
|
|
}
|
|
|
|
describe("handleError", () => {
|
|
let savedEnvironment: EnvironmentSnapshot;
|
|
let savedExitCode: number | string | undefined;
|
|
|
|
beforeEach(() => {
|
|
savedExitCode = process.exitCode;
|
|
savedEnvironment = {
|
|
[OTLP_ENDPOINT_ENVIRONMENT_VARIABLE]: process.env[OTLP_ENDPOINT_ENVIRONMENT_VARIABLE],
|
|
[OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE]: process.env[OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE],
|
|
};
|
|
process.env[OTLP_ENDPOINT_ENVIRONMENT_VARIABLE] = "https://otel.example.test";
|
|
process.env[OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE] = "Bearer secret-token";
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.exitCode = savedExitCode;
|
|
for (const [environmentVariableName, value] of Object.entries(savedEnvironment)) {
|
|
if (value === undefined) {
|
|
delete process.env[environmentVariableName];
|
|
} else {
|
|
process.env[environmentVariableName] = value;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("builds a prefilled GitHub issue URL with redacted OTel context", () => {
|
|
const issueUrl = new URL(buildErrorIssueUrl(new Error("boom")));
|
|
const body = issueUrl.searchParams.get("body") ?? "";
|
|
|
|
expect(issueUrl.origin + issueUrl.pathname).toBe(
|
|
"https://github.com/millionco/react-doctor/issues/new",
|
|
);
|
|
expect(issueUrl.searchParams.get("title")).toBe("CLI error: boom");
|
|
expect(issueUrl.searchParams.get("labels")).toBe("bug");
|
|
expect(body).toContain("```text\nboom\n```");
|
|
expect(body).toContain("REACT_DOCTOR_OTLP_ENDPOINT configured: yes");
|
|
expect(body).toContain("REACT_DOCTOR_OTLP_AUTH_HEADER configured: yes (value redacted)");
|
|
expect(body).toContain("OTLP exporter enabled: yes");
|
|
expect(body).toContain("trace/span link, if exported:");
|
|
expect(body).not.toContain("secret-token");
|
|
});
|
|
|
|
it("adds the Sentry reference to the issue body when an event id is provided", () => {
|
|
const body =
|
|
new URL(buildErrorIssueUrl(new Error("boom"), "evt-abc123")).searchParams.get("body") ?? "";
|
|
expect(body).toContain("Sentry reference: evt-abc123");
|
|
});
|
|
|
|
it("omits the Sentry reference line when no event id is provided", () => {
|
|
const body = new URL(buildErrorIssueUrl(new Error("boom"))).searchParams.get("body") ?? "";
|
|
expect(body).not.toContain("Sentry reference:");
|
|
});
|
|
|
|
it("prints the reference id so the user can quote it when reporting", () => {
|
|
const errorMessages: string[] = [];
|
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((...messages) => {
|
|
errorMessages.push(messages.join(" "));
|
|
});
|
|
|
|
try {
|
|
handleError(new Error("boom"), { shouldExit: false, sentryEventId: "evt-xyz789" });
|
|
} finally {
|
|
consoleErrorSpy.mockRestore();
|
|
}
|
|
|
|
expect(errorMessages.join("\n")).toContain(
|
|
"Reference (mention this when reporting): evt-xyz789",
|
|
);
|
|
});
|
|
|
|
it("suggests Discord when printing an error", () => {
|
|
const errorMessages: string[] = [];
|
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((...messages) => {
|
|
errorMessages.push(messages.join(" "));
|
|
});
|
|
|
|
try {
|
|
handleError(new Error("boom"), { shouldExit: false });
|
|
} finally {
|
|
consoleErrorSpy.mockRestore();
|
|
}
|
|
|
|
expect(errorMessages.join("\n")).toContain(
|
|
"You can also ask for help in Discord: https://react.doctor/discord",
|
|
);
|
|
expect(process.exitCode).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("handleUserError", () => {
|
|
let savedExitCode: number | string | undefined;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
savedExitCode = process.exitCode;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.exitCode = savedExitCode;
|
|
});
|
|
|
|
it("prints just the message — no crash framing, issue link, Discord, or Sentry reference", () => {
|
|
const errorMessages: string[] = [];
|
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((...messages) => {
|
|
errorMessages.push(messages.join(" "));
|
|
});
|
|
|
|
const userError = new ReactDoctorError({
|
|
reason: new GitBaseBranchInvalid({
|
|
detail: 'Diff range "7694215..c4de712" has an invalid endpoint "..".',
|
|
}),
|
|
});
|
|
try {
|
|
handleUserError(userError, { shouldExit: false });
|
|
} finally {
|
|
consoleErrorSpy.mockRestore();
|
|
}
|
|
|
|
const output = errorMessages.join("\n");
|
|
expect(output).toContain('Diff range "7694215..c4de712" has an invalid endpoint');
|
|
expect(output).not.toContain("Something went wrong");
|
|
expect(output).not.toContain("open this prefilled issue");
|
|
expect(output).not.toContain("Discord");
|
|
expect(output).not.toContain("Reference (mention this when reporting)");
|
|
expect(process.exitCode).toBe(1);
|
|
});
|
|
|
|
it("records incomplete npx installs as environment errors", () => {
|
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
const cacheError = Object.assign(new Error("Cannot find module './meta/unevaluated.json'"), {
|
|
code: "MODULE_NOT_FOUND",
|
|
requireStack: [
|
|
"/home/user/.npm/_npx/81e833f6d16d6127/node_modules/ajv/dist/refs/json-schema-2020-12/index.js",
|
|
],
|
|
});
|
|
|
|
try {
|
|
handleUserError(cacheError, { shouldExit: false });
|
|
} finally {
|
|
consoleErrorSpy.mockRestore();
|
|
}
|
|
|
|
expect(recordCount).toHaveBeenCalledWith(METRIC.cliEnvironmentError, 1, {
|
|
code: "NPX_CACHE_CORRUPTION",
|
|
});
|
|
});
|
|
});
|