diff --git a/e2e-tests/problems.spec.ts b/e2e-tests/problems.spec.ts index fd22175..909c37d 100644 --- a/e2e-tests/problems.spec.ts +++ b/e2e-tests/problems.spec.ts @@ -1,4 +1,4 @@ -import { test, testSkipIfWindows } from "./helpers/test_helper"; +import { test, testSkipIfWindows, Timeout } from "./helpers/test_helper"; import { expect } from "@playwright/test"; import fs from "fs"; import path from "path"; @@ -83,6 +83,72 @@ export default App; await po.snapshotMessages({ replaceDumpPath: true }); }); +testSkipIfWindows( + "problems - select specific problems and fix", + async ({ po }) => { + await po.setUp(); + await po.importApp(MINIMAL_APP); + + // Create multiple TS errors in one file + const appPath = await po.getCurrentAppPath(); + const badFilePath = path.join(appPath, "src", "bad-file.tsx"); + fs.writeFileSync( + badFilePath, + `const App = () =>
Minimal imported app
; +nonExistentFunction1(); +nonExistentFunction2(); +nonExistentFunction3(); + +export default App; +`, + ); + + await po.ensurePnpmInstall(); + + // Trigger creation of problems and open problems panel + // await po.sendPrompt("tc=create-ts-errors"); + await po.selectPreviewMode("problems"); + await po.clickRecheckProblems(); + + // Initially, all selected: button shows Fix X problems and Clear all is visible + const fixButton = po.page.getByTestId("fix-all-button"); + await expect(fixButton).toBeVisible(); + await expect(fixButton).toContainText(/Fix \d+ problems/); + + // Click first two rows to toggle off (deselect) + const rows = po.page.getByTestId("problem-row"); + const rowCount = await rows.count(); + expect(rowCount).toBeGreaterThan(2); + await rows.nth(0).click(); + await rows.nth(1).click(); + + // Button should update to reflect remaining selected + await expect(fixButton).toContainText(/Fix 1 problem/); + + // Clear all should switch to Select all when none selected + // Deselect remaining rows + for (let i = 2; i < rowCount; i++) { + await rows.nth(i).click(); + } + + const selectButton = po.page.getByRole("button", { + name: /Select all/, + }); + await expect(selectButton).toHaveText("Select all"); + + // Select all, then fix selected + await selectButton.click(); + // Unselect the second row + await rows.nth(1).click(); + await expect(fixButton).toContainText(/Fix 2 problems/); + + await fixButton.click(); + await po.waitForChatCompletion(); + await po.snapshotServerDump("last-message"); + await po.snapshotMessages({ replaceDumpPath: true }); + }, +); + testSkipIfWindows("problems - manual edit (react/vite)", async ({ po }) => { await po.setUp({ enableAutoFixProblems: true }); await po.sendPrompt("tc=1"); @@ -101,13 +167,15 @@ export default App; await po.clickTogglePreviewPanel(); await po.selectPreviewMode("problems"); - await po.clickRecheckProblems(); - await po.snapshotProblemsPane(); + const fixButton = po.page.getByTestId("fix-all-button"); + await expect(fixButton).toBeEnabled({ timeout: Timeout.LONG }); + await expect(fixButton).toContainText(/Fix 1 problem/); fs.unlinkSync(badFilePath); await po.clickRecheckProblems(); - await po.snapshotProblemsPane(); + await expect(fixButton).toBeDisabled({ timeout: Timeout.LONG }); + await expect(fixButton).toContainText(/Fix 0 problems/); }); testSkipIfWindows("problems - manual edit (next.js)", async ({ po }) => { @@ -129,11 +197,13 @@ testSkipIfWindows("problems - manual edit (next.js)", async ({ po }) => { await po.clickTogglePreviewPanel(); await po.selectPreviewMode("problems"); - await po.clickRecheckProblems(); - await po.snapshotProblemsPane(); + const fixButton = po.page.getByTestId("fix-all-button"); + await expect(fixButton).toBeEnabled({ timeout: Timeout.LONG }); + await expect(fixButton).toContainText(/Fix 1 problem/); fs.unlinkSync(badFilePath); await po.clickRecheckProblems(); - await po.snapshotProblemsPane(); + await expect(fixButton).toBeDisabled({ timeout: Timeout.LONG }); + await expect(fixButton).toContainText(/Fix 0 problems/); }); diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml deleted file mode 100644 index 77aa353..0000000 --- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml +++ /dev/null @@ -1,10 +0,0 @@ -- img -- text: 1 error -- button "Run checks": - - img -- button "Fix All": - - img -- img -- img -- text: src/bad-file.tsx 2:3 -- paragraph: Cannot find name 'nonExistentFunction'. \ No newline at end of file diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml deleted file mode 100644 index 59c7712..0000000 --- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml +++ /dev/null @@ -1,4 +0,0 @@ -- paragraph: No problems found -- img -- button "Run checks": - - img \ No newline at end of file diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml deleted file mode 100644 index 2f0fefa..0000000 --- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml +++ /dev/null @@ -1,10 +0,0 @@ -- img -- text: 1 error -- button "Run checks": - - img -- button "Fix All": - - img -- img -- img -- text: src/bad-file.tsx 2:1 -- paragraph: Cannot find name 'nonExistentFunction'. \ No newline at end of file diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml deleted file mode 100644 index 59c7712..0000000 --- a/e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml +++ /dev/null @@ -1,4 +0,0 @@ -- paragraph: No problems found -- img -- button "Run checks": - - img \ No newline at end of file diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.aria.yml b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.aria.yml new file mode 100644 index 0000000..e843206 --- /dev/null +++ b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.aria.yml @@ -0,0 +1,21 @@ +- paragraph: "Fix these 2 TypeScript compile-time errors:" +- list: + - listitem: src/bad-file.tsx:2:1 - Cannot find name 'nonExistentFunction1'. (TS2304) +- code: const App = () =>
Minimal imported app
; nonExistentFunction1(); // <-- TypeScript compiler error here nonExistentFunction2(); +- list: + - listitem: src/bad-file.tsx:4:1 - Cannot find name 'nonExistentFunction3'. (TS2304) +- code: nonExistentFunction2(); nonExistentFunction3(); // <-- TypeScript compiler error here +- paragraph: Please fix all errors in a concise way. +- img +- text: bad-file.ts +- button "Edit": + - img +- img +- text: "src/bad-file.ts Summary: Fix 2 errors and introduce a new error." +- paragraph: "[[dyad-dump-path=*]]" +- button: + - img +- img +- text: less than a minute ago +- button "Retry": + - img \ No newline at end of file diff --git a/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.txt b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.txt new file mode 100644 index 0000000..3ff5fd1 --- /dev/null +++ b/e2e-tests/snapshots/problems.spec.ts_problems---select-specific-problems-and-fix-1.txt @@ -0,0 +1,19 @@ +=== +role: user +message: Fix these 2 TypeScript compile-time errors: + +1. src/bad-file.tsx:2:1 - Cannot find name 'nonExistentFunction1'. (TS2304) +``` +const App = () =>
Minimal imported app
; +nonExistentFunction1(); // <-- TypeScript compiler error here +nonExistentFunction2(); +``` + +2. src/bad-file.tsx:4:1 - Cannot find name 'nonExistentFunction3'. (TS2304) +``` +nonExistentFunction2(); +nonExistentFunction3(); // <-- TypeScript compiler error here +``` + + +Please fix all errors in a concise way. \ No newline at end of file diff --git a/src/components/preview_panel/Problems.tsx b/src/components/preview_panel/Problems.tsx index 2f88f38..cb13cb8 100644 --- a/src/components/preview_panel/Problems.tsx +++ b/src/components/preview_panel/Problems.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useAtom, useAtomValue } from "jotai"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; @@ -12,6 +12,7 @@ import { } from "lucide-react"; import { Problem, ProblemReport } from "@/ipc/ipc_types"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { useStreamChat } from "@/hooks/useStreamChat"; import { useCheckProblems } from "@/hooks/useCheckProblems"; @@ -20,11 +21,26 @@ import { showError } from "@/lib/toast"; interface ProblemItemProps { problem: Problem; + checked: boolean; + onToggle: () => void; } -const ProblemItem = ({ problem }: ProblemItemProps) => { +const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => { return ( -
+
+ e.stopPropagation()} + className="mt-0.5" + aria-label="Select problem" + />
@@ -56,6 +72,7 @@ interface RecheckButtonProps { | "ghost" | "link"; className?: string; + onBeforeRecheck?: () => void; } const RecheckButton = ({ @@ -63,11 +80,15 @@ const RecheckButton = ({ size = "sm", variant = "outline", className = "h-7 px-3 text-xs", + onBeforeRecheck, }: RecheckButtonProps) => { const { checkProblems, isChecking } = useCheckProblems(appId); const [showingFeedback, setShowingFeedback] = useState(false); const handleRecheck = async () => { + if (onBeforeRecheck) { + onBeforeRecheck(); + } setShowingFeedback(true); const res = await checkProblems(); @@ -102,23 +123,24 @@ const RecheckButton = ({ interface ProblemsSummaryProps { problemReport: ProblemReport; appId: number; + selectedCount: number; + onClearAll: () => void; + onFixSelected: () => void; + onSelectAll: () => void; } -const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => { - const { streamMessage } = useStreamChat(); +const ProblemsSummary = ({ + problemReport, + appId, + selectedCount, + onClearAll, + onFixSelected, + onSelectAll, +}: ProblemsSummaryProps) => { const { problems } = problemReport; const totalErrors = problems.length; - const [selectedChatId] = useAtom(selectedChatIdAtom); - const handleFixAll = () => { - if (!selectedChatId) { - return; - } - streamMessage({ - prompt: createProblemFixPrompt(problemReport), - chatId: selectedChatId, - }); - }; + // Keep stream hook mounted; actual fix action is provided via onFixSelected if (problems.length === 0) { return ( @@ -148,16 +170,36 @@ const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => { )}
- + + {selectedCount === 0 ? ( + + ) : ( + + )}
@@ -175,6 +217,20 @@ export function Problems() { export function _Problems() { const selectedAppId = useAtomValue(selectedAppIdAtom); const { problemReport } = useCheckProblems(selectedAppId); + const [selectedKeys, setSelectedKeys] = useState>(new Set()); + const problemKey = (p: Problem) => + `${p.file}:${p.line}:${p.column}:${p.code}`; + const { streamMessage } = useStreamChat(); + const [selectedChatId] = useAtom(selectedChatIdAtom); + + // Whenever the problems pane is shown or the report updates, select all problems + useEffect(() => { + if (problemReport?.problems?.length) { + setSelectedKeys(new Set(problemReport.problems.map(problemKey))); + } else { + setSelectedKeys(new Set()); + } + }, [problemReport]); if (!selectedAppId) { return ( @@ -200,21 +256,65 @@ export function _Problems() {

Run checks to scan your app for TypeScript errors and other problems.

- + setSelectedKeys(new Set())} + /> ); } return (
- + + problemReport.problems.some((p) => problemKey(p) === key), + ).length + } + onClearAll={() => setSelectedKeys(new Set())} + onSelectAll={() => + setSelectedKeys( + new Set(problemReport.problems.map((p) => problemKey(p))), + ) + } + onFixSelected={() => { + if (!selectedChatId) return; + const selectedProblems = problemReport.problems.filter((p) => + selectedKeys.has(problemKey(p)), + ); + const subsetReport: ProblemReport = { problems: selectedProblems }; + streamMessage({ + prompt: createProblemFixPrompt(subsetReport), + chatId: selectedChatId, + }); + }} + />
- {problemReport.problems.map((problem, index) => ( - - ))} + {problemReport.problems.map((problem) => { + const selKey = problemKey(problem); + const checked = selectedKeys.has(selKey); + return ( + { + setSelectedKeys((prev) => { + const next = new Set(prev); + if (next.has(selKey)) { + next.delete(selKey); + } else { + next.add(selKey); + } + return next; + }); + }} + /> + ); + })}
);