Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Three-tier strategy. See [docs/testing-strategy.md](docs/testing-strategy.md) fo
- Use `selectFeedInSidebar(page, name)` from `fixtures.ts` — it handles opening the sidebar on mobile.

**happy-dom gotchas**:
- happy-dom is a real resource loader: a `<link rel="stylesheet">` or `<iframe src>` in a fixture opens a socket to the URL. `vitest.config.js` disables CSS/JS file loading and iframe page loading (`environmentOptions.happyDOM.settings`), and `tests/environment/no-real-network.test.ts` pins it. A disabled iframe load still prints one `NotSupportedError` line per fixture; that is happy-dom's unconditional `console.error`, not a failure. Page code that calls `fetch` on mount still needs a stub in the test (`vi.stubGlobal("fetch", ...)`), or the request goes to `localhost:3000` and dies as an `ECONNREFUSED` trace that no assertion sees.
- DOMPurify + happy-dom executes inline scripts during sanitization. Use non-callable fixtures (`var x = 1;`, not `alert(1)`).
- CSS-escaped colons (`content\\:encoded`) may work in happy-dom but fail in browsers — always use `getElementsByTagName` for XML namespace-prefixed elements.
- CDATA with namespace declarations may fail to parse. Use entity-escaped HTML (`&lt;p&gt;`) instead.
Expand Down
44 changes: 44 additions & 0 deletions tests/environment/no-real-network.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import http from "node:http";
import https from "node:https";

/**
* The unit environment must never open a socket. happy-dom is a real
* resource loader: a `<link rel="stylesheet">` or `<iframe src>` inserted
* into the document makes it fetch the URL through node:http(s), which in
* the suite meant connection attempts to localhost:3000 and to fixture
* hosts (`https://evil.com/`) that showed up as ECONNREFUSED stack traces
* in every run. Nothing awaited those fetches, so the suite stayed green
* while doing real network I/O. Pinned here so the environment setting that
* silences it cannot be dropped without this failing.
*/
describe("unit environment: no real network", () => {
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));

afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = "";
});

it("does not fetch a <link rel=stylesheet> inserted into the document", async () => {
const request = vi.spyOn(http, "request").mockImplementation(() => {
throw new Error("real network attempted");
});

document.body.innerHTML = '<link rel="stylesheet" href="/style.css">';
await settle();

expect(request).not.toHaveBeenCalled();
});

it("does not load the page of an <iframe src> inserted into the document", async () => {
const request = vi.spyOn(https, "request").mockImplementation(() => {
throw new Error("real network attempted");
});

document.body.innerHTML = '<iframe src="https://evil.com"></iframe>';
await settle();

expect(request).not.toHaveBeenCalled();
});
});
23 changes: 22 additions & 1 deletion tests/pages/billing-success.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router";
Expand Down Expand Up @@ -33,6 +33,21 @@ function renderWithRoute(path: string) {
}

describe("BillingSuccess page", () => {
// Rendering with a session_id makes the page request the license for that
// session on mount. Tests that only assert copy never stubbed fetch, so
// happy-dom opened a real socket to localhost:3000 on every run. A default
// pending stub keeps the page in its initial state, which is what those
// tests were observing anyway; tests that need a response override it.
beforeEach(() => {
vi.stubGlobal(
"fetch",
vi.fn(() => new Promise<Response>(() => {})),
);
});
afterEach(() => {
vi.unstubAllGlobals();
});

it("renders a confirmation heading", () => {
renderWithRoute("/billing/success");
expect(
Expand All @@ -48,6 +63,12 @@ describe("BillingSuccess page", () => {
expect(
screen.getByRole("button", { name: /save/i }),
).toBeInTheDocument();
// The page asks the server for this session's license rather than
// waiting for the user to paste — the input is the fallback.
const calls = (globalThis.fetch as unknown as Mock).mock.calls as Array<
[RequestInfo | URL, RequestInit?]
>;
expect(calls.some(([url]) => String(url).includes("/api/license/retrieve"))).toBe(true);
});

it("does NOT render the session id as page chrome on the polling-state happy path", async () => {
Expand Down
15 changes: 15 additions & 0 deletions vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ export default defineConfig({
},
test: {
environment: "happy-dom",
// happy-dom is a real resource loader: a <link rel="stylesheet"> or
// <iframe src> in a fixture makes it open a socket to the URL. The unit
// suite must never touch the network, and nothing awaited those loads,
// so they only showed up as ECONNREFUSED stack traces in every run.
// tests/environment/no-real-network.test.ts pins this.
environmentOptions: {
happyDOM: {
settings: {
disableCSSFileLoading: true,
disableJavaScriptFileLoading: true,
disableIframePageLoading: true,
handleDisabledFileLoadingAsSuccess: true,
},
},
},
include: ["tests/**/*.test.{js,ts,tsx}"],
setupFiles: ["tests/setup.ts"],
// Unhandled rejections FAIL the run rather than printing "Errors 1"
Expand Down