mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
## 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` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context not reported, default reasoning) via [Codex](https://openai.com/codex)
131 lines
4.1 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|