fabro/apps/fabro-web/app/routes/insights-editor.test.tsx
Bryan Helmkamp 62f0b3e7d1
refactor(web): improve React Doctor score (#405)
## Summary

Improves the web UI's React Doctor audit score by separating reusable
helpers from React component modules, tightening effect/state ownership,
and extracting real component boundaries in the install wizard, stage
activity view, run-files diff browser, RunDetail route, and Runs
workspace. The branch removes the previously deferred RunDetail and Runs
giant-component diagnostics without changing RunDetail UX, route
contracts, action ordering, or Runs workspace behavior.

| Metric | Main baseline | Initial PR | Current PR |
|--------|---------------|------------|------------|
| React Doctor score | 63 | 71 | 99 |
| React Doctor errors | 123 | 0 | 0 |
| React Doctor warnings | 241 | 163 | 3 |
| React Doctor diagnostics | 364 | 163 | 3 |

## Changes

- Moves exported helper logic out of component files so Fast
Refresh/component-export rules no longer dominate the audit.
- Adds a targeted React Doctor config exception for React Router route
modules, where non-component exports like route metadata are
intentional.
- Refactors low-risk state/effect patterns: keyed interview question
state, reducer-backed editable run title state, event-owned preview
opening, route-keyed insights editor initialization, refresh timer
ownership, and selection/derived list cleanup.
- Reworks `InstallApp` around an install reducer, a controller hook for
install lifecycle state, and focused wizard step components for LLM,
server, object-store, sandbox, and GitHub setup.
- Moves `RunStages` selected-stage activity into a keyed boundary for
panel/debug detail state while preserving stage activity filters across
navigation.
- Extracts the `RunFiles` loaded diff-browser view from route/query
coordination so the route owns data/URL state and the loaded view owns
rendering.
- Splits `RunDetail` into route-local header, actions, tab shell, docked
controls, model, and lifecycle-toast modules; the actions menu now uses
grouped descriptors instead of a large boolean/callback prop matrix.
- Extracts Runs workspace preference ownership into
`useRunsWorkspacePreferences` and moves toolbar rendering into
`RunsToolbar`, leaving the route focused on data, DnD state, filtering,
and view selection.
- Guards `InsightsEditor` query execution with a latest-run id and
timeout cleanup so stale or unmounted mock query runs cannot overwrite
newer results.
- Adds regression coverage for archived-run deletion from RunDetail and
stale-result handling in InsightsEditor.
- Improves semantic/accessibility coverage with labeled controls, native
meter/section semantics, decorative status dots, and clearer unavailable
copy.
- Removes dead UI code and applies local suppressions only where the
rule is a documented false positive or an intentional imperative
integration boundary.

## Remaining React Doctor warnings

Current score is 99 with 0 errors and 3 warnings. The remaining warnings
are intentionally left for separate judgment rather than mechanical
churn:

- `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and
`CreateSecretForm` need reducers only if they encode real coupled
transitions, not simple field setters.

## Verification

- `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22
pass`, `0 fail`
- `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx
app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail`
- `cd apps/fabro-web && bunx react-doctor@latest --full --json >
/tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors,
`3` warnings
- Earlier branch verification also included `cd apps/fabro-web && bun
run build`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context not reported, default reasoning) via
[Codex](https://openai.com/codex)
2026-05-25 22:41:37 -04:00

131 lines
4.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import TestRenderer, { act } from "react-test-renderer";
import { createMemoryRouter, RouterProvider } from "react-router";
import { setupReactTestEnv } from "../lib/test-utils";
import InsightsEditor from "./insights-editor";
let teardownReactEnv: (() => void) | undefined;
let originalResizeObserver: typeof ResizeObserver | undefined;
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
function textFromNode(
node: ReturnType<TestRenderer.ReactTestRenderer["toJSON"]>,
): string {
if (!node) return "";
if (typeof node === "string") return node;
if (Array.isArray(node)) return node.map(textFromNode).join(" ");
return (node.children ?? []).map(textFromNode).join(" ");
}
function textFromTestNode(node: TestRenderer.ReactTestInstance): string {
return node.children.map((child) => {
if (typeof child === "string") return child;
if (typeof child === "number") return String(child);
return textFromTestNode(child);
}).join("");
}
function buttonByText(renderer: TestRenderer.ReactTestRenderer, text: string) {
return renderer.root.findAll(
(node) => node.type === "button" && textFromTestNode(node).includes(text),
)[0];
}
async function renderInsightsEditor() {
const router = createMemoryRouter(
[{ path: "/insights", element: <InsightsEditor /> }],
{ initialEntries: ["/insights"] },
);
let renderer!: TestRenderer.ReactTestRenderer;
await act(async () => {
renderer = TestRenderer.create(<RouterProvider router={router} />);
});
mountedRenderers.push(renderer);
return renderer;
}
beforeEach(() => {
teardownReactEnv = setupReactTestEnv();
originalResizeObserver = globalThis.ResizeObserver;
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
} as typeof ResizeObserver;
});
afterEach(() => {
for (const renderer of mountedRenderers.splice(0)) {
act(() => renderer.unmount());
}
if (originalResizeObserver) {
globalThis.ResizeObserver = originalResizeObserver;
} else {
delete (globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver;
}
teardownReactEnv?.();
teardownReactEnv = undefined;
});
describe("InsightsEditor", () => {
test("keeps the newest query result when an earlier run finishes later", async () => {
const renderer = await renderInsightsEditor();
const originalSetTimeout = globalThis.setTimeout;
const timers: Array<() => void> = [];
globalThis.setTimeout = ((callback: TimerHandler) => {
if (typeof callback === "function") {
timers.push(callback as () => void);
}
return timers.length as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout;
try {
await act(async () => {
buttonByText(renderer, "Table")!.props.onClick();
});
const textarea = renderer.root.findByProps({ "aria-label": "SQL query" });
await act(async () => {
textarea.props.onChange({ target: { value: "SELECT * FROM runs" } });
});
await act(async () => {
buttonByText(renderer, "Run")!.props.onClick();
});
const updatedTextarea = renderer.root.findByProps({ "aria-label": "SQL query" });
await act(async () => {
updatedTextarea.props.onChange({
target: { value: "SELECT failed FROM runs" },
});
});
const latestTextarea = renderer.root.findByProps({ "aria-label": "SQL query" });
await act(async () => {
latestTextarea.props.onKeyDown({
key: "Enter",
metaKey: true,
ctrlKey: false,
preventDefault: () => undefined,
});
});
expect(timers).toHaveLength(2);
await act(async () => {
timers[1]!();
});
expect(textFromNode(renderer.toJSON())).toContain("failure_rate");
await act(async () => {
timers[0]!();
});
const text = textFromNode(renderer.toJSON());
expect(text).toContain("failure_rate");
expect(text).not.toContain("total_additions");
} finally {
globalThis.setTimeout = originalSetTimeout;
}
});
});