mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: prevent site storybook tests from hanging after completion (#23936)
The vitest process hung after all 2132 story tests passed because leftover refetchInterval polls kept the Node.js event loop alive. Components that set per-query refetchInterval override the QueryClient default, causing HTTP requests through vite's proxy to localhost:3000 (no backend) that never resolve cleanly. Three fixes: - preview.tsx: disable all automatic refetching defaults and cancel in-flight queries on story unmount via useEffect cleanup - storybook.tsx: save/restore the original window.WebSocket in the withWebSocket decorator, clear pending timers in close() - vite.config.mts: add explicit testTimeout, hookTimeout, bail, and retry settings to the storybook vitest project Also fix 5 story files that imported from @testing-library/react instead of storybook/test.
This commit is contained in:
committed by
GitHub
parent
10f0786966
commit
a1ef3043bb
@@ -856,6 +856,7 @@ pre-push:
|
|||||||
start=$$(date +%s)
|
start=$$(date +%s)
|
||||||
logdir=$$(mktemp -d "$${TMPDIR:-/tmp}/coder-pre-push.XXXXXX")
|
logdir=$$(mktemp -d "$${TMPDIR:-/tmp}/coder-pre-push.XXXXXX")
|
||||||
echo "$(BOLD)pre-push$(RESET) ($$logdir)"
|
echo "$(BOLD)pre-push$(RESET) ($$logdir)"
|
||||||
|
test -d site/node_modules/.cache/storybook || (cd site/ && pnpm exec node scripts/warmup-storybook-cache.mjs)
|
||||||
echo "test + build site:"
|
echo "test + build site:"
|
||||||
$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir \
|
$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir \
|
||||||
test \
|
test \
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Warm vite's transform cache for storybook story files.
|
||||||
|
// Only needed on cold cache (first run after pnpm install).
|
||||||
|
import { createServer } from "vite";
|
||||||
|
import { readdirSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = join(__dirname, "..");
|
||||||
|
|
||||||
|
const server = await createServer({
|
||||||
|
configFile: join(root, "vite.config.mts"),
|
||||||
|
root,
|
||||||
|
});
|
||||||
|
await server.listen();
|
||||||
|
|
||||||
|
const stories = readdirSync(join(root, "src"), { recursive: true })
|
||||||
|
.filter((f) => String(f).endsWith(".stories.tsx"))
|
||||||
|
.map((f) => `/src/${f}`);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
stories.map((f) =>
|
||||||
|
server.environments.client.warmupRequest(f).catch(() => {}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.close();
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { within } from "@testing-library/react";
|
|
||||||
import { action } from "storybook/actions";
|
import { action } from "storybook/actions";
|
||||||
import { userEvent } from "storybook/test";
|
import { userEvent, within } from "storybook/test";
|
||||||
import { DeleteDialog } from "./DeleteDialog";
|
import { DeleteDialog } from "./DeleteDialog";
|
||||||
|
|
||||||
const meta: Meta<typeof DeleteDialog> = {
|
const meta: Meta<typeof DeleteDialog> = {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { within } from "@testing-library/react";
|
|
||||||
import type { ErrorResponse } from "react-router";
|
import type { ErrorResponse } from "react-router";
|
||||||
import { expect, userEvent } from "storybook/test";
|
import { expect, userEvent, within } from "storybook/test";
|
||||||
import { GlobalErrorBoundaryInner } from "./GlobalErrorBoundary";
|
import { GlobalErrorBoundaryInner } from "./GlobalErrorBoundary";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
|
|||||||
original={compareWith}
|
original={compareWith}
|
||||||
modified={value}
|
modified={value}
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
// Let the editor handle model cleanup. Without this,
|
||||||
|
// @monaco-editor/react disposes models before the
|
||||||
|
// DiffEditorWidget and throws an error.
|
||||||
|
keepCurrentOriginalModel
|
||||||
|
keepCurrentModifiedModel
|
||||||
onMount={handleDiffEditorMount}
|
onMount={handleDiffEditorMount}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { waitFor } from "@testing-library/react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { expect, spyOn, userEvent, within } from "storybook/test";
|
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
|
||||||
import { API } from "#/api/api";
|
import { API } from "#/api/api";
|
||||||
import { MockUsers } from "#/pages/UsersPage/storybookData/users";
|
import { MockUsers } from "#/pages/UsersPage/storybookData/users";
|
||||||
import { MockUserOwner } from "#/testHelpers/entities";
|
import { MockUserOwner } from "#/testHelpers/entities";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { within } from "@testing-library/react";
|
|
||||||
import { action } from "storybook/actions";
|
import { action } from "storybook/actions";
|
||||||
import { userEvent } from "storybook/test";
|
import { userEvent, within } from "storybook/test";
|
||||||
import { chromatic } from "#/testHelpers/chromatic";
|
import { chromatic } from "#/testHelpers/chromatic";
|
||||||
import {
|
import {
|
||||||
MockDefaultOrganization,
|
MockDefaultOrganization,
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { within } from "@testing-library/react";
|
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { userEvent } from "storybook/test";
|
import { userEvent, within } from "storybook/test";
|
||||||
import { TemplateInsightsControls } from "./TemplateInsightsPage";
|
import { TemplateInsightsControls } from "./TemplateInsightsPage";
|
||||||
|
|
||||||
const meta: Meta<typeof TemplateInsightsControls> = {
|
const meta: Meta<typeof TemplateInsightsControls> = {
|
||||||
|
|||||||
+91
-53
@@ -90,63 +90,68 @@ export default defineConfig({
|
|||||||
"Set-Cookie":
|
"Set-Cookie":
|
||||||
"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax",
|
"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax",
|
||||||
},
|
},
|
||||||
proxy: {
|
// The proxy targets localhost:3000 (coderd). During tests no
|
||||||
"//": {
|
// coderd is running, and the proxy's retry sockets keep the
|
||||||
changeOrigin: true,
|
// Node process alive after vitest finishes.
|
||||||
target: process.env.CODER_HOST || "http://localhost:3000",
|
proxy: process.env.VITEST
|
||||||
secure: process.env.NODE_ENV === "production",
|
? undefined
|
||||||
rewrite: (path) => path.replace(/\/+/g, "/"),
|
: {
|
||||||
},
|
"//": {
|
||||||
"/api": {
|
changeOrigin: true,
|
||||||
ws: true,
|
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||||
changeOrigin: true,
|
secure: process.env.NODE_ENV === "production",
|
||||||
target: process.env.CODER_HOST || "http://localhost:3000",
|
rewrite: (path) => path.replace(/\/+/g, "/"),
|
||||||
secure: process.env.NODE_ENV === "production",
|
},
|
||||||
configure: (proxy) => {
|
"/api": {
|
||||||
if (process.env.CODER_SESSION_TOKEN) {
|
ws: true,
|
||||||
proxy.on("proxyReq", (proxyReq) => {
|
changeOrigin: true,
|
||||||
proxyReq.setHeader(
|
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||||
"Coder-Session-Token",
|
secure: process.env.NODE_ENV === "production",
|
||||||
process.env.CODER_SESSION_TOKEN!,
|
configure: (proxy) => {
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Vite does not catch socket errors, and stops the webserver.
|
|
||||||
// As /logs endpoint can return HTTP 4xx status, we need to embrace
|
|
||||||
// Vite with a custom error handler to prevent from quitting.
|
|
||||||
proxy.on("proxyReqWs", (proxyReq, _req, socket) => {
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
proxyReq.setHeader(
|
|
||||||
"origin",
|
|
||||||
process.env.CODER_HOST || "http://localhost:3000",
|
|
||||||
);
|
|
||||||
if (process.env.CODER_SESSION_TOKEN) {
|
if (process.env.CODER_SESSION_TOKEN) {
|
||||||
proxyReq.setHeader(
|
proxy.on("proxyReq", (proxyReq) => {
|
||||||
"Coder-Session-Token",
|
proxyReq.setHeader(
|
||||||
process.env.CODER_SESSION_TOKEN!,
|
"Coder-Session-Token",
|
||||||
);
|
process.env.CODER_SESSION_TOKEN!,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
// Vite does not catch socket errors, and stops the webserver.
|
||||||
|
// As /logs endpoint can return HTTP 4xx status, we need to embrace
|
||||||
|
// Vite with a custom error handler to prevent from quitting.
|
||||||
|
proxy.on("proxyReqWs", (proxyReq, _req, socket) => {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
proxyReq.setHeader(
|
||||||
|
"origin",
|
||||||
|
process.env.CODER_HOST || "http://localhost:3000",
|
||||||
|
);
|
||||||
|
if (process.env.CODER_SESSION_TOKEN) {
|
||||||
|
proxyReq.setHeader(
|
||||||
|
"Coder-Session-Token",
|
||||||
|
process.env.CODER_SESSION_TOKEN!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
socket.on("error", (error) => {
|
socket.on("error", (error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/swagger": {
|
||||||
|
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
"/healthz": {
|
||||||
|
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
"/serviceWorker.js": {
|
||||||
|
target: process.env.CODER_HOST || "http://localhost:3000",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
"/swagger": {
|
|
||||||
target: process.env.CODER_HOST || "http://localhost:3000",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
"/healthz": {
|
|
||||||
target: process.env.CODER_HOST || "http://localhost:3000",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
"/serviceWorker.js": {
|
|
||||||
target: process.env.CODER_HOST || "http://localhost:3000",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
allowedHosts: [".coder", ".dev.coder.com"],
|
allowedHosts: [".coder", ".dev.coder.com"],
|
||||||
},
|
},
|
||||||
// Pre-bundle deps that Vite tends to discover late (deep MUI
|
// Pre-bundle deps that Vite tends to discover late (deep MUI
|
||||||
@@ -204,6 +209,9 @@ export default defineConfig({
|
|||||||
"@mui/system/createTheme",
|
"@mui/system/createTheme",
|
||||||
"@mui/system/useTheme",
|
"@mui/system/useTheme",
|
||||||
"@mui/x-tree-view",
|
"@mui/x-tree-view",
|
||||||
|
// Discovered at runtime without this entry, triggering
|
||||||
|
// a mid-run dep re-optimization that breaks imports.
|
||||||
|
"@tanstack/react-query-devtools",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -217,6 +225,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
silent: "passed-only",
|
silent: "passed-only",
|
||||||
|
// Rolldown's native threads do not terminate on close,
|
||||||
|
// so vitest always hits this timeout. Keep it short.
|
||||||
|
teardownTimeout: 1000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
extends: true,
|
extends: true,
|
||||||
@@ -243,6 +254,27 @@ export default defineConfig({
|
|||||||
storybookTest({
|
storybookTest({
|
||||||
configDir: path.join(__dirname, ".storybook"),
|
configDir: path.join(__dirname, ".storybook"),
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
name: "storybook-test-setup",
|
||||||
|
// Return 502 for API routes. The proxy is disabled
|
||||||
|
// during tests (see above), so without this vite
|
||||||
|
// serves its HTML fallback for unmatched routes.
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use((req, res, next) => {
|
||||||
|
const url = req.url ?? "";
|
||||||
|
if (
|
||||||
|
url.startsWith("/api/") ||
|
||||||
|
url.startsWith("/swagger/") ||
|
||||||
|
url.startsWith("/healthz")
|
||||||
|
) {
|
||||||
|
res.statusCode = 502;
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
test: {
|
test: {
|
||||||
name: "storybook",
|
name: "storybook",
|
||||||
@@ -253,6 +285,12 @@ export default defineConfig({
|
|||||||
instances: [{ browser: "chromium" }],
|
instances: [{ browser: "chromium" }],
|
||||||
},
|
},
|
||||||
setupFiles: [".storybook/vitest.setup.ts"],
|
setupFiles: [".storybook/vitest.setup.ts"],
|
||||||
|
// Stop early on systemic failures.
|
||||||
|
bail: 5,
|
||||||
|
// Cap concurrent browser iframes. The default
|
||||||
|
// (os.availableParallelism, 96 on dev workspaces)
|
||||||
|
// overwhelms vite's transform pipeline on cold cache.
|
||||||
|
maxWorkers: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user