From a832bd805cc0441e47d233d847beea38e1cae356 Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Sun, 10 May 2026 21:18:55 +0000 Subject: [PATCH 1/2] fix(canvas): extractMessageText uses only first direct text field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: `extractMessageText` in ConversationTraceModal joined text from ALL result.parts[].text + result.parts[].root.text entries, concatenating "Direct text\nRoot text" when only "Direct text" was expected. Fix: scan all parts for the first direct `text` field and return it. Only fall back to `parts[0].root.text` when no direct text exists. Subsequent parts' root.text fields are ignored when a direct text was found in an earlier part — matching the test contract. Fixes: ConversationTraceModal.test.tsx "prefers parts[].text over parts[].root.text" (test was failing with concat output). Co-Authored-By: Claude Opus 4.7 --- .../src/components/ConversationTraceModal.tsx | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/canvas/src/components/ConversationTraceModal.tsx b/canvas/src/components/ConversationTraceModal.tsx index 63afe664..c585781a 100644 --- a/canvas/src/components/ConversationTraceModal.tsx +++ b/canvas/src/components/ConversationTraceModal.tsx @@ -31,17 +31,25 @@ export function extractMessageText(body: Record | null): string if (text) return text; // Response: result.parts[].text or result.parts[].root.text + // Use the first part that has a direct text field; within that part, + // prefer direct text over root.text. Subsequent parts' root.text fields + // are ignored when a direct text exists in an earlier part. const result = body.result as Record | undefined; const rParts = (result?.parts || []) as Array>; - const rText = rParts - .map((p) => { - if (p.text) return p.text as string; - const root = p.root as Record | undefined; - return (root?.text as string) || ""; - }) - .filter(Boolean) - .join("\n"); - if (rText) return rText; + const firstPartWithText = rParts.find( + (p) => typeof p.text === "string" && (p.text as string) !== "" + ); + if (firstPartWithText) { + return firstPartWithText.text as string; + } + // No direct text found; use root.text from the first part (if present). + const firstPart = rParts[0]; + if (firstPart) { + const root = firstPart.root as Record | undefined; + if (typeof root?.text === "string" && root.text !== "") { + return root.text as string; + } + } if (typeof body.result === "string") return body.result; } catch { /* ignore */ } -- 2.45.2 From 9279f9292b3f3fd7e83f95498659e95762b68edb Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Sun, 10 May 2026 21:22:44 +0000 Subject: [PATCH 2/2] fix(canvas/test): Spinner tests use getAttribute for SVG className SVG elements in jsdom expose className as SVGAnimatedString (an object), not a plain string. Calling toContain() on an object produces a confusing "expected [] to include" error. Fix: use getAttribute("class") instead. Also adds afterEach(cleanup) for DOM isolation between tests. Co-Authored-By: Claude Opus 4.7 --- .../src/components/__tests__/Spinner.test.tsx | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/canvas/src/components/__tests__/Spinner.test.tsx b/canvas/src/components/__tests__/Spinner.test.tsx index 610f3a03..ae85b060 100644 --- a/canvas/src/components/__tests__/Spinner.test.tsx +++ b/canvas/src/components/__tests__/Spinner.test.tsx @@ -3,52 +3,56 @@ * Tests for Spinner component. * * Covers: sm/md/lg size classes, aria-hidden, motion-safe animate-spin class. + * + * NOTE: SVG elements use SVGAnimatedString for className (not a plain string), + * so we use getAttribute("class") instead of className for assertions. */ import React from "react"; -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { render, cleanup } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; import { Spinner } from "../Spinner"; +afterEach(cleanup); + +function getSvgClass(r: ReturnType): string { + const svg = r.container.querySelector("svg"); + if (!svg) throw new Error("No SVG found"); + return svg.getAttribute("class") ?? ""; +} + describe("Spinner — size variants", () => { it("renders with sm size class", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg).toBeTruthy(); - expect(svg?.className).toContain("w-3"); - expect(svg?.className).toContain("h-3"); + const r = render(); + expect(getSvgClass(r)).toContain("w-3"); + expect(getSvgClass(r)).toContain("h-3"); }); it("renders with md size class (default)", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.className).toContain("w-4"); - expect(svg?.className).toContain("h-4"); + const r = render(); + expect(getSvgClass(r)).toContain("w-4"); + expect(getSvgClass(r)).toContain("h-4"); }); it("renders with lg size class", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.className).toContain("w-5"); - expect(svg?.className).toContain("h-5"); + const r = render(); + expect(getSvgClass(r)).toContain("w-5"); + expect(getSvgClass(r)).toContain("h-5"); }); it("defaults to md size when no size prop given", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.className).toContain("w-4"); - expect(svg?.className).toContain("h-4"); + const r = render(); + expect(getSvgClass(r)).toContain("w-4"); + expect(getSvgClass(r)).toContain("h-4"); }); it("has aria-hidden=true so screen readers skip it", () => { - const { container } = render(); - const svg = container.querySelector("svg"); + const r = render(); + const svg = r.container.querySelector("svg"); expect(svg?.getAttribute("aria-hidden")).toBe("true"); }); it("includes the motion-safe:animate-spin class for CSS animation", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.className).toContain("motion-safe:animate-spin"); + expect(getSvgClass(render())).toContain("motion-safe:animate-spin"); }); it("renders exactly one SVG element", () => { -- 2.45.2