diff --git a/playwright-tests/manual-edits.spec.ts b/playwright-tests/manual-edits.spec.ts new file mode 100644 index 0000000..19134f6 --- /dev/null +++ b/playwright-tests/manual-edits.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from "@playwright/test" + +test("Manual edits test", async ({ page }) => { + await page.goto("http://127.0.0.1:5177/editor?snippet_id=snippet_5") + await page.waitForLoadState("networkidle") + + const loginButton = page.getByRole("button", { name: "Fake testuser Login" }) + await loginButton.waitFor({ state: "visible" }) + await loginButton.click() + + const combobox = page.getByRole("combobox") + await combobox.waitFor({ state: "visible" }) + await combobox.click() + + const fileOption = page.getByText("manual-edits.json") + await fileOption.waitFor({ state: "visible" }) + await fileOption.click() + + // Wait for file content to load + await page.waitForLoadState("networkidle") + + const emptyPlacementsText = page.getByText('"pcb_placements": []') + await emptyPlacementsText.waitFor({ state: "visible" }) + await emptyPlacementsText.click() + + await page.keyboard.press("Control+End") + await page.keyboard.down("Shift") + await page.keyboard.press("Control+Home") + await page.keyboard.up("Shift") + + const pcbPlacementsData = `{ + "pcb_placements": [ + { + "selector": "U1", + "center": { + "x": -26.03275345576554, + "y": 23.735745797903878 + }, + "relative_to": "group_center", + "_edit_event_id": "0.5072961258141278" + } + ], + "edit_events": [], + "manual_trace_hints": [] + }` + + await page.keyboard.type(pcbPlacementsData) + + // Wait for Run button and click + const runButton = page.getByRole("button", { name: "Run", exact: true }) + await runButton.waitFor({ state: "visible" }) + await runButton.click() + + // Wait for any animations/processing after Run + await page.waitForLoadState("networkidle") + + // Wait for Save button and click + const saveButton = page.getByRole("button", { name: "Save" }) + await saveButton.waitFor({ state: "visible", timeout: 10000 }) + await saveButton.click() + + await page.waitForTimeout(1000) + + await expect(page).toHaveScreenshot("editor-manual-edits.png") + + // Navigate to view page + await page.goto("http://127.0.0.1:5177/testuser/a555timer-square-wave") + await page.waitForLoadState("networkidle") + + const filesLink = page.getByRole("link", { name: "Files" }) + await filesLink.waitFor({ state: "visible" }) + await filesLink.click() + + const fileLink = page.getByText("manual-edits.json") + await fileLink.waitFor({ state: "visible" }) + await fileLink.click() + + await page.waitForLoadState("networkidle") + await expect(page).toHaveScreenshot("manual-edits-view.png") +}) diff --git a/playwright-tests/snapshots/manual-edits.spec.ts-editor-manual-edits.png b/playwright-tests/snapshots/manual-edits.spec.ts-editor-manual-edits.png new file mode 100644 index 0000000..fb2c9c0 Binary files /dev/null and b/playwright-tests/snapshots/manual-edits.spec.ts-editor-manual-edits.png differ diff --git a/playwright-tests/snapshots/manual-edits.spec.ts-manual-edits-view.png b/playwright-tests/snapshots/manual-edits.spec.ts-manual-edits-view.png new file mode 100644 index 0000000..7e5ee32 Binary files /dev/null and b/playwright-tests/snapshots/manual-edits.spec.ts-manual-edits-view.png differ diff --git a/src/components/CodeAndPreview.tsx b/src/components/CodeAndPreview.tsx index ffb43cd..7724d6d 100644 --- a/src/components/CodeAndPreview.tsx +++ b/src/components/CodeAndPreview.tsx @@ -26,6 +26,7 @@ export function CodeAndPreview({ snippet }: Props) { const axios = useAxios() const isLoggedIn = useGlobalStore((s) => Boolean(s.session)) const urlParams = useUrlParams() + const { toast } = useToast() const templateFromUrl = useMemo( () => getSnippetTemplate(urlParams.template), [], @@ -41,12 +42,22 @@ export function CodeAndPreview({ snippet }: Props) { ) }, []) - // Initialize with template or snippet's manual edits if available - const [manualEditsFileContent, setManualEditsFileContent] = useState( - snippet?.manual_edits_json ?? - JSON.stringify(manualEditsTemplate, null, 2) ?? - "", - ) + // Initialize manualEditsFileContent with proper validation + const [manualEditsFileContent, setManualEditsFileContent] = useState(() => { + try { + const initialContent = + snippet?.manual_edits_json ?? + JSON.stringify(manualEditsTemplate, null, 2) + // Validate that it's parseable JSON + JSON.parse(initialContent) + return initialContent + } catch (e) { + console.warn( + "Invalid initial manual edits content, using default template", + ) + return JSON.stringify(manualEditsTemplate, null, 2) + } + }) const [code, setCode] = useState(defaultCode ?? "") const [dts, setDts] = useState("") const [showPreview, setShowPreview] = useState(true) @@ -62,20 +73,38 @@ export function CodeAndPreview({ snippet }: Props) { } }, [Boolean(snippet)]) - const { toast } = useToast() - + // Update manual edits when snippet changes, with validation useEffect(() => { if (snippet?.manual_edits_json) { - setManualEditsFileContent(snippet.manual_edits_json) + try { + JSON.parse(snippet.manual_edits_json) + setManualEditsFileContent(snippet.manual_edits_json) + } catch (e) { + console.warn("Invalid manual edits JSON from snippet") + toast({ + title: "Warning", + description: + "Invalid manual edits format in snippet. Using default template.", + variant: "destructive", + }) + setManualEditsFileContent(JSON.stringify(manualEditsTemplate, null, 2)) + } } - }, [Boolean(snippet?.manual_edits_json)]) + }, [snippet?.manual_edits_json]) - const userImports = useMemo( - () => ({ - "./manual-edits.json": JSON.parse(manualEditsFileContent), - }), - [manualEditsFileContent], - ) + // Safely parse userImports with error handling + const userImports = useMemo(() => { + try { + return { + "./manual-edits.json": JSON.parse(manualEditsFileContent), + } + } catch (e) { + console.warn("Error parsing manual edits for imports, using empty object") + return { + "./manual-edits.json": {}, + } + } + }, [manualEditsFileContent]) const { message, @@ -101,6 +130,14 @@ export function CodeAndPreview({ snippet }: Props) { const updateSnippetMutation = useMutation({ mutationFn: async () => { if (!snippet) throw new Error("No snippet to update") + + // Validate manual edits before sending + try { + JSON.parse(manualEditsFileContent) + } catch (e) { + throw new Error("Invalid manual edits JSON") + } + const response = await axios.post("/snippets/update", { snippet_id: snippet.snippet_id, code: code, @@ -125,7 +162,10 @@ export function CodeAndPreview({ snippet }: Props) { console.error("Error saving snippet:", error) toast({ title: "Error", - description: "Failed to save the snippet. Please try again.", + description: + error instanceof Error + ? error.message + : "Failed to save the snippet. Please try again.", variant: "destructive", }) },