Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/elements/__tests__/jsx-preview.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { render, screen } from "@testing-library/react";
import { StrictMode } from "react";

import type { JSXPreviewProps } from "../src/jsx-preview";

import {
JSXPreview,
JSXPreviewContent,
Expand Down Expand Up @@ -277,6 +279,70 @@ describe("jSXPreview streaming mode", () => {
);
expect(screen.getByText("Text")).toBeInTheDocument();
});

it("keeps the last good preview when a streamed chunk fails to parse", () => {
// Parser reports errors synchronously during render; the resulting React
// "update while rendering" dev warning is expected here
const consoleError = vi.spyOn(console, "error").mockImplementation(vi.fn());
const onError = vi.fn();

const { rerender } = render(
<JSXPreview isStreaming jsx="<div>Good</div>" onError={onError}>
<JSXPreviewContent />
</JSXPreview>
);
expect(screen.getByText("Good")).toBeInTheDocument();

rerender(
<JSXPreview isStreaming jsx="<div>{bad syntax</div>" onError={onError}>
<JSXPreviewContent />
</JSXPreview>
);
expect(screen.getByText("Good")).toBeInTheDocument();
expect(onError).not.toHaveBeenCalled();

rerender(
<JSXPreview isStreaming jsx="<div>Recovered</div>" onError={onError}>
<JSXPreviewContent />
</JSXPreview>
);
expect(screen.getByText("Recovered")).toBeInTheDocument();

consoleError.mockRestore();
});

it("keeps the last good preview across a bad chunk in Strict Mode", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(vi.fn());

const { rerender } = render(
<StrictMode>
<JSXPreview isStreaming jsx="<div>Good</div>">
<JSXPreviewContent />
</JSXPreview>
</StrictMode>
);
expect(screen.getByText("Good")).toBeInTheDocument();

rerender(
<StrictMode>
<JSXPreview isStreaming jsx="<div>{bad syntax</div>">
<JSXPreviewContent />
</JSXPreview>
</StrictMode>
);
expect(screen.getByText("Good")).toBeInTheDocument();

rerender(
<StrictMode>
<JSXPreview isStreaming jsx="<div>Recovered</div>">
<JSXPreviewContent />
</JSXPreview>
</StrictMode>
);
expect(screen.getByText("Recovered")).toBeInTheDocument();

consoleError.mockRestore();
});
});

describe("jSXPreview integration", () => {
Expand Down
18 changes: 12 additions & 6 deletions packages/elements/src/jsx-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"use client";

import type { ComponentProps, ReactNode } from "react";
import type { TProps as JsxParserProps } from "react-jsx-parser";

import { cn } from "@repo/shadcn-ui/lib/utils";
import { AlertCircle } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import {
createContext,
memo,
Expand All @@ -13,7 +15,6 @@ import {
useRef,
useState,
} from "react";
import type { TProps as JsxParserProps } from "react-jsx-parser";
import JsxParser from "react-jsx-parser";

interface JSXPreviewContextValue {
Expand Down Expand Up @@ -209,12 +210,17 @@ export const JSXPreviewContent = memo(
const errorReportedRef = useRef<string | null>(null);
const lastGoodJsxRef = useRef("");
const [hadError, setHadError] = useState(false);

// Reset error tracking when jsx changes
useEffect(() => {
const [prevProcessedJsx, setPrevProcessedJsx] = useState(processedJsx);

// Reset error tracking before the parser renders the new string
// (react-jsx-parser reports parse errors synchronously during render,
// so a post-render effect would reset them too late and the tracking
// effect below would record the broken string as "last good")
if (processedJsx !== prevProcessedJsx) {
setPrevProcessedJsx(processedJsx);
errorReportedRef.current = null;
setHadError(false);
}, [processedJsx]);
}

const handleError = useCallback(
(err: Error) => {
Expand Down