From 66f326de5ed7615c49dac3a172a64f72eec9b51d Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 12 May 2026 13:55:10 -0400 Subject: [PATCH] fix(example-apps): clear eslint issues in dashproof-lab and dashmint-lab dashproof-lab: - Add "coverage" to globalIgnores so Vitest coverage HTML reporter scripts stop triggering unused-disable warnings (matches dashnote). - Replace HistoryPanel's prop-watching useEffect with the React-recommended "reset state during render" pattern, fixing react-hooks/set-state-in-effect. Initialize prevRequestToken to undefined so the panel still fires on its first render (the parent mounts it fresh with the token already bumped). - Add regression tests covering the parent-dispatched chain deep-link and the fire-once-per-token invariant. dashmint-lab: - Rename the Playwright fixture-API second-arg callback parameter from `use` to `provide` (matching dashproof-lab's convention) so that eslint-plugin-react-hooks v6 stops flagging `await use(page)` as a misplaced React Hook call. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dashmint-lab/test/e2e/fixtures.ts | 4 +- example-apps/dashproof-lab/eslint.config.js | 2 +- .../src/components/HistoryPanel.tsx | 31 ++++--- .../dashproof-lab/test/HistoryPanel.test.tsx | 90 +++++++++++++++++++ 4 files changed, 114 insertions(+), 13 deletions(-) diff --git a/example-apps/dashmint-lab/test/e2e/fixtures.ts b/example-apps/dashmint-lab/test/e2e/fixtures.ts index 0d4ae90..15b745e 100644 --- a/example-apps/dashmint-lab/test/e2e/fixtures.ts +++ b/example-apps/dashmint-lab/test/e2e/fixtures.ts @@ -21,14 +21,14 @@ interface AppFixture { export { base as rawTest }; export const test = base.extend({ - page: async ({ page }, use) => { + page: async ({ page }, provide) => { await page.goto("/"); // Wait until the SDK has connected (sidebar shows "Connected") so any // Collection query the spec triggers has a live SDK to talk to. await expect(page.getByText("Connected").first()).toBeVisible({ timeout: 60_000, }); - await use(page); + await provide(page); }, }); diff --git a/example-apps/dashproof-lab/eslint.config.js b/example-apps/dashproof-lab/eslint.config.js index 09dc42a..48b1004 100644 --- a/example-apps/dashproof-lab/eslint.config.js +++ b/example-apps/dashproof-lab/eslint.config.js @@ -6,7 +6,7 @@ import tseslint from "typescript-eslint"; import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(["dist", "playwright-report", "test-results"]), + globalIgnores(["coverage", "dist", "playwright-report", "test-results"]), { files: ["**/*.{ts,tsx}"], extends: [ diff --git a/example-apps/dashproof-lab/src/components/HistoryPanel.tsx b/example-apps/dashproof-lab/src/components/HistoryPanel.tsx index 3624cb5..eff8f2b 100644 --- a/example-apps/dashproof-lab/src/components/HistoryPanel.tsx +++ b/example-apps/dashproof-lab/src/components/HistoryPanel.tsx @@ -53,6 +53,27 @@ export function HistoryPanel({ const [errorState, setErrorState] = useState(null); const [toast, setToast] = useState(null); const toastTimer = useRef | null>(null); + const [prevRequestToken, setPrevRequestToken] = useState( + undefined, + ); + + // Reset state in response to a new parent-issued request (monotonic + // requestToken). React docs recommend doing this during render rather than + // in an effect — see https://react.dev/learn/you-might-not-need-an-effect. + // The sentinel-undefined initial value ensures the reset fires on first + // render too (the parent mounts this panel fresh with the token already + // bumped). + if (prevRequestToken !== requestToken) { + setPrevRequestToken(requestToken); + const trimmed = requestedChainId?.trim(); + if (trimmed) { + setMode("chain"); + setChainInput(trimmed); + setActiveChainId(trimmed); + setAnchors([]); + setErrorState(null); + } + } const effectiveMode = session.status === "authenticated" ? mode : "chain"; const canQueryOwner = @@ -79,16 +100,6 @@ export function HistoryPanel({ [], ); - useEffect(() => { - const trimmed = requestedChainId?.trim(); - if (!trimmed) return; - setMode("chain"); - setChainInput(trimmed); - setActiveChainId(trimmed); - setAnchors([]); - setErrorState(null); - }, [requestedChainId, requestToken]); - useEffect(() => { if (canQueryOwner) { const sdk = session.sdk; diff --git a/example-apps/dashproof-lab/test/HistoryPanel.test.tsx b/example-apps/dashproof-lab/test/HistoryPanel.test.tsx index 767302c..738fc48 100644 --- a/example-apps/dashproof-lab/test/HistoryPanel.test.tsx +++ b/example-apps/dashproof-lab/test/HistoryPanel.test.tsx @@ -125,6 +125,96 @@ describe("HistoryPanel", () => { expect(chainSections).toHaveLength(1); }); + it("loads the parent-requested chainId when requestToken bumps", async () => { + // Regression for the render-time prop-reset that replaced the previous + // setState-in-useEffect. Parent dispatches a new chainId by bumping + // requestToken; the panel should switch to chain mode, populate the + // input, and load that chain. + mockUseSession.mockReturnValue({ + status: "readonly", + sdk: {}, + identityId: null, + log: vi.fn(), + }); + mockListAnchorsByChain.mockResolvedValue([ + { + id: "anchor-req", + ownerId: "owner-x", + createdAt: 1710000000000, + entryHash: Uint8Array.from([3]), + entryHashHex: "03", + chainId: "requested-chain", + filename: "requested.txt", + }, + ]); + + const { rerender } = render( + , + ); + + rerender( + , + ); + + await waitFor(() => { + expect(mockListAnchorsByChain).toHaveBeenCalledWith( + expect.objectContaining({ chainId: "requested-chain" }), + ); + }); + await screen.findByText("requested.txt"); + expect(screen.getByDisplayValue("requested-chain")).toBeTruthy(); + }); + + it("does not re-fire the parent-requested reset when requestToken is unchanged", async () => { + // The render-time guard (prevRequestToken !== requestToken) must + // fire-once-per-token; otherwise unrelated re-renders would clobber any + // edit the user made to the chain input after the initial dispatch. + mockUseSession.mockReturnValue({ + status: "readonly", + sdk: {}, + identityId: null, + log: vi.fn(), + }); + mockListAnchorsByChain.mockResolvedValue([]); + + const { rerender } = render( + , + ); + + await waitFor(() => { + expect(screen.getByDisplayValue("requested-chain")).toBeTruthy(); + }); + + // User edits the chain input after the parent's dispatch. + fireEvent.change(screen.getByPlaceholderText("invoice-2026-04"), { + target: { value: "user-edited" }, + }); + expect(screen.getByDisplayValue("user-edited")).toBeTruthy(); + + // Unrelated re-render with the same requestToken — must NOT reset the + // input back to requestedChainId. + rerender( + , + ); + + expect(screen.getByDisplayValue("user-edited")).toBeTruthy(); + }); + it("loads chain history in read-only mode", async () => { mockUseSession.mockReturnValue({ status: "readonly",