From 3b76aad2e7876f81d4e08cc1fbb91080c86954e3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 5 Feb 2026 22:05:39 +0000
Subject: [PATCH] Address review feedback: update comment, move regex, add edge
case tests
Co-authored-by: andrew-polk <5847219+andrew-polk@users.noreply.github.com>
---
.../stringWithOptionalLink.spec.tsx | 84 ++++++++++++++++++-
.../stringWithOptionalLink.tsx | 6 +-
2 files changed, 87 insertions(+), 3 deletions(-)
diff --git a/src/BloomBrowserUI/react_components/stringWithOptionalLink.spec.tsx b/src/BloomBrowserUI/react_components/stringWithOptionalLink.spec.tsx
index f0c50fbc79f7..1b7e17afd14f 100644
--- a/src/BloomBrowserUI/react_components/stringWithOptionalLink.spec.tsx
+++ b/src/BloomBrowserUI/react_components/stringWithOptionalLink.spec.tsx
@@ -1,5 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
-import * as React from "react";
import * as ReactDOM from "react-dom";
import { renderToStaticMarkup } from "react-dom/server";
import { act } from "react-dom/test-utils";
@@ -106,4 +105,87 @@ describe("StringWithOptionalLink", () => {
);
expect(postMock).not.toHaveBeenCalled();
});
+
+ it("renders a single span for messages with no links", () => {
+ const markup = renderToStaticMarkup(
+ ,
+ );
+ const temp = document.createElement("div");
+ temp.innerHTML = markup;
+
+ const spans = temp.querySelectorAll("span");
+ const anchors = temp.querySelectorAll("a");
+
+ expect(spans.length).toBe(1);
+ expect(spans[0].textContent).toBe("This is plain text");
+ expect(anchors.length).toBe(0);
+ });
+
+ it("handles messages starting with a link", () => {
+ const markup = renderToStaticMarkup(
+ Link then text"}
+ />,
+ );
+ const temp = document.createElement("div");
+ temp.innerHTML = markup;
+
+ const anchors = temp.querySelectorAll("a");
+ const spans = temp.querySelectorAll("span");
+
+ expect(anchors.length).toBe(1);
+ expect(anchors[0].textContent).toBe("Link");
+ expect(spans.length).toBe(1);
+ expect(spans[0].textContent).toBe(" then text");
+ });
+
+ it("handles messages ending with a link", () => {
+ const markup = renderToStaticMarkup(
+ Link"}
+ />,
+ );
+ const temp = document.createElement("div");
+ temp.innerHTML = markup;
+
+ const spans = temp.querySelectorAll("span");
+ const anchors = temp.querySelectorAll("a");
+
+ expect(spans.length).toBe(1);
+ expect(spans[0].textContent).toBe("Text before ");
+ expect(anchors.length).toBe(1);
+ expect(anchors[0].textContent).toBe("Link");
+ });
+
+ it("handles consecutive links with no text between them", () => {
+ const markup = renderToStaticMarkup(
+ FirstSecond"
+ }
+ />,
+ );
+ const temp = document.createElement("div");
+ temp.innerHTML = markup;
+
+ const anchors = temp.querySelectorAll("a");
+ const spans = temp.querySelectorAll("span");
+
+ expect(anchors.length).toBe(2);
+ expect(anchors[0].textContent).toBe("First");
+ expect(anchors[1].textContent).toBe("Second");
+ expect(spans.length).toBe(0);
+ });
+
+ it("renders a single span for empty string", () => {
+ const markup = renderToStaticMarkup(
+ ,
+ );
+ const temp = document.createElement("div");
+ temp.innerHTML = markup;
+
+ const spans = temp.querySelectorAll("span");
+ expect(spans.length).toBe(1);
+ expect(spans[0].textContent).toBe("");
+ });
});
diff --git a/src/BloomBrowserUI/react_components/stringWithOptionalLink.tsx b/src/BloomBrowserUI/react_components/stringWithOptionalLink.tsx
index a0bc8d7ed1a5..5affe6bc2f2f 100644
--- a/src/BloomBrowserUI/react_components/stringWithOptionalLink.tsx
+++ b/src/BloomBrowserUI/react_components/stringWithOptionalLink.tsx
@@ -6,11 +6,13 @@ import { post, postString } from "../utils/bloomApi";
// (Currently the href must use single quotes.)
// If there is no link, the result is a single span, the message.
// If there are links, we interleave spans and anchor tags for each segment.
-// Currently the href is assumed to be something to send to our API,
-// but NOT to actually navigate to. We could support more options as needed.
+// For internal links, the href is treated as something to send to our API rather than
+// something for the browser to navigate to directly. External http/mailto hrefs are
+// sent to the 'link' API endpoint, which opens them in an external browser.
export const StringWithOptionalLink: React.FunctionComponent<{
message: string;
}> = (props) => {
+ // Create a fresh regex for each render to avoid lastIndex issues
const linkRegex = /]*?href='([^>']+)'[^>]*>(.*?)<\/a>/g;
const elements: React.ReactNode[] = [];
let lastIndex = 0;