test: Extract code block detection into shared utilities (#11222)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa
2026-01-07 14:55:56 -03:00
committed by GitHub
parent 17872f672b
commit 7e77cd1f8e
12 changed files with 297 additions and 85 deletions

View File

@ -3,6 +3,7 @@ import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax/browser";
import remarkGfm from "remark-gfm";
import type { ContentType } from "@/types/chat";
import { extractLanguage, isCodeBlock } from "@/utils/codeBlockUtils";
import ForwardedIconComponent from "../../common/genericIconComponent";
import SimplifiedCodeTabComponent from "../codeTabsComponent";
import DurationDisplay from "./DurationDisplay";
@ -74,7 +75,6 @@ export default function ContentDisplay({
return <>{props.children}</>;
},
code: ({ node, className, children, ...props }) => {
const inline = !(props as any).hasOwnProperty("data-language");
let content = children as string;
if (
Array.isArray(children) &&
@ -90,14 +90,16 @@ export default function ContentDisplay({
}
}
const match = /language-(\w+)/.exec(className || "");
if (isCodeBlock(className, props, content)) {
return (
<SimplifiedCodeTabComponent
language={extractLanguage(className)}
code={String(content).replace(/\n$/, "")}
/>
);
}
return !inline ? (
<SimplifiedCodeTabComponent
language={(match && match[1]) || ""}
code={String(content).replace(/\n$/, "")}
/>
) : (
return (
<code className={className} {...props}>
{content}
</code>
@ -171,16 +173,16 @@ export default function ContentDisplay({
return <ul className="max-w-full">{props.children}</ul>;
},
code: ({ node, className, children, ...props }) => {
const inline = !(props as any).hasOwnProperty(
"data-language",
);
const match = /language-(\w+)/.exec(className || "");
return !inline ? (
<SimplifiedCodeTabComponent
language={(match && match[1]) || ""}
code={String(children).replace(/\n$/, "")}
/>
) : (
const content = String(children);
if (isCodeBlock(className, props, content)) {
return (
<SimplifiedCodeTabComponent
language={extractLanguage(className)}
code={content.replace(/\n$/, "")}
/>
);
}
return (
<code className={className} {...props}>
{children}
</code>

View File

@ -3,6 +3,7 @@ import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import { TextShimmer } from "@/components/ui/TextShimmer";
import { extractLanguage, isCodeBlock } from "@/utils/codeBlockUtils";
import { cn } from "@/utils/utils";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent";
import LogoIcon from "./chat-logo-icon";
@ -121,9 +122,6 @@ export const ErrorView = ({
children,
...props
}) => {
const inline = !(
props as any
).hasOwnProperty("data-language");
let content = children as string;
if (
Array.isArray(children) &&
@ -141,19 +139,23 @@ export const ErrorView = ({
}
}
const match = /language-(\w+)/.exec(
className || "",
);
if (
isCodeBlock(className, props, content)
) {
return (
<CodeTabsComponent
language={extractLanguage(
className,
)}
code={String(content).replace(
/\n$/,
"",
)}
/>
);
}
return !inline ? (
<CodeTabsComponent
language={(match && match[1]) || ""}
code={String(content).replace(
/\n$/,
"",
)}
/>
) : (
return (
<code
className={className}
{...props}

View File

@ -3,6 +3,7 @@ import rehypeMathjax from "rehype-mathjax/browser";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import { EMPTY_OUTPUT_SEND_MESSAGE } from "@/constants/constants";
import { extractLanguage, isCodeBlock } from "@/utils/codeBlockUtils";
import { preprocessChatMessage } from "@/utils/markdownUtils";
import { cn } from "@/utils/utils";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent";
@ -67,7 +68,6 @@ export const MarkdownField = ({
);
},
code: ({ node, className, children, ...props }) => {
const inline = !(props as any).hasOwnProperty("data-language");
let content = children as string;
if (
Array.isArray(children) &&
@ -88,14 +88,16 @@ export const MarkdownField = ({
}
}
const match = /language-(\w+)/.exec(className || "");
if (isCodeBlock(className, props, content)) {
return (
<CodeTabsComponent
language={extractLanguage(className)}
code={String(content).replace(/\n$/, "")}
/>
);
}
return !inline ? (
<CodeTabsComponent
language={(match && match[1]) || ""}
code={String(content).replace(/\n$/, "")}
/>
) : (
return (
<code className={className} {...props}>
{content}
</code>

View File

@ -0,0 +1,172 @@
import { extractLanguage, isCodeBlock } from "../codeBlockUtils";
describe("codeBlockUtils", () => {
describe("isCodeBlock", () => {
describe("should return true (block code)", () => {
it("should_return_true_when_className_has_language_identifier", () => {
const result = isCodeBlock("language-python", {}, "print('hello')");
expect(result).toBe(true);
});
it("should_return_true_when_className_has_language_javascript", () => {
const result = isCodeBlock("language-javascript", {}, "const x = 1");
expect(result).toBe(true);
});
it("should_return_true_when_className_has_language_with_other_classes", () => {
const result = isCodeBlock(
"hljs language-typescript some-other-class",
{},
"const x: number = 1",
);
expect(result).toBe(true);
});
it("should_return_true_when_props_has_data_language", () => {
const result = isCodeBlock(
undefined,
{ "data-language": "python" },
"print('hello')",
);
expect(result).toBe(true);
});
it("should_return_true_when_props_has_data_language_empty_string", () => {
const result = isCodeBlock(undefined, { "data-language": "" }, "code");
expect(result).toBe(true);
});
it("should_return_true_when_content_has_newlines", () => {
const result = isCodeBlock(undefined, {}, "line1\nline2\nline3");
expect(result).toBe(true);
});
it("should_return_true_when_content_has_single_newline", () => {
const result = isCodeBlock(undefined, {}, "line1\nline2");
expect(result).toBe(true);
});
it("should_return_true_when_multiple_conditions_are_met", () => {
const result = isCodeBlock(
"language-python",
{ "data-language": "python" },
"def hello():\n print('world')",
);
expect(result).toBe(true);
});
});
describe("should return false (inline code)", () => {
it("should_return_false_when_no_language_class_no_data_language_no_newlines", () => {
const result = isCodeBlock(undefined, {}, "inline code");
expect(result).toBe(false);
});
it("should_return_false_when_className_is_empty_string", () => {
const result = isCodeBlock("", {}, "simple code");
expect(result).toBe(false);
});
it("should_return_false_when_className_has_no_language_prefix", () => {
const result = isCodeBlock("hljs some-class", {}, "code");
expect(result).toBe(false);
});
it("should_return_false_when_props_is_undefined", () => {
const result = isCodeBlock(undefined, undefined, "inline");
expect(result).toBe(false);
});
it("should_return_false_when_props_is_empty_object", () => {
const result = isCodeBlock(undefined, {}, "x = 1");
expect(result).toBe(false);
});
it("should_return_false_when_content_is_single_line", () => {
const result = isCodeBlock(undefined, {}, "print('hello')");
expect(result).toBe(false);
});
it("should_return_false_when_content_is_empty", () => {
const result = isCodeBlock(undefined, {}, "");
expect(result).toBe(false);
});
});
describe("edge cases", () => {
it("should_handle_language_class_with_numbers", () => {
const result = isCodeBlock("language-es2020", {}, "code");
expect(result).toBe(true);
});
it("should_handle_content_with_carriage_return_only", () => {
// \r alone should not be treated as newline for code block
const result = isCodeBlock(undefined, {}, "line1\rline2");
expect(result).toBe(false);
});
it("should_handle_content_with_crlf", () => {
const result = isCodeBlock(undefined, {}, "line1\r\nline2");
expect(result).toBe(true);
});
it("should_handle_content_with_trailing_newline_only", () => {
const result = isCodeBlock(undefined, {}, "single line\n");
expect(result).toBe(true);
});
it("should_handle_content_with_leading_newline_only", () => {
const result = isCodeBlock(undefined, {}, "\nsingle line");
expect(result).toBe(true);
});
it("should_handle_null_props_gracefully", () => {
// TypeScript would prevent this, but testing runtime safety
const result = isCodeBlock(undefined, null as any, "code");
expect(result).toBe(false);
});
});
});
describe("extractLanguage", () => {
it("should_extract_python_from_language_class", () => {
const result = extractLanguage("language-python");
expect(result).toBe("python");
});
it("should_extract_javascript_from_language_class", () => {
const result = extractLanguage("language-javascript");
expect(result).toBe("javascript");
});
it("should_extract_language_when_mixed_with_other_classes", () => {
const result = extractLanguage("hljs language-typescript highlight");
expect(result).toBe("typescript");
});
it("should_return_empty_string_when_no_language_class", () => {
const result = extractLanguage("hljs some-class");
expect(result).toBe("");
});
it("should_return_empty_string_when_className_is_undefined", () => {
const result = extractLanguage(undefined);
expect(result).toBe("");
});
it("should_return_empty_string_when_className_is_empty", () => {
const result = extractLanguage("");
expect(result).toBe("");
});
it("should_handle_language_with_numbers", () => {
const result = extractLanguage("language-es6");
expect(result).toBe("es6");
});
it("should_extract_first_language_when_multiple_present", () => {
const result = extractLanguage("language-python language-javascript");
expect(result).toBe("python");
});
});
});

View File

@ -0,0 +1,30 @@
/**
* Determines if a code element should be rendered as a block (with copy button)
* or as inline code.
*
* A code element is considered a block if any of the following conditions are met:
* 1. It has a language class (e.g., "language-python")
* 2. It has the "data-language" attribute (from some markdown parsers)
* 3. The content contains newlines (multi-line code)
*/
export function isCodeBlock(
className: string | undefined,
props: Record<string, unknown> | undefined,
content: string,
): boolean {
const languageMatch = /language-(\w+)/.exec(className ?? "");
const hasLanguageClass = !!languageMatch;
const hasDataLanguage = "data-language" in (props ?? {});
const hasNewlines = content.includes("\n");
return hasLanguageClass || hasDataLanguage || hasNewlines;
}
/**
* Extracts the language identifier from a className.
* Returns empty string if no language is found.
*/
export function extractLanguage(className: string | undefined): string {
const match = /language-(\w+)/.exec(className ?? "");
return match?.[1] ?? "";
}

View File

@ -1,4 +1,3 @@
import { type Page } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { expect, test } from "../../fixtures";
@ -6,7 +5,6 @@ import { addFlowToTestOnEmptyLangflow } from "../../utils/add-flow-to-test-on-em
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { selectGptModel } from "../../utils/select-gpt-model";
test(
"user must be able to freeze a path",
@ -37,11 +35,13 @@ test(
await initialGPTsetup(page);
// Use unique prompts to avoid OpenAI caching returning identical responses
const timestamp = Date.now();
await page
.getByTestId("textarea_str_input_value")
.first()
.fill(
"say a random number between 1 and 300000 and a random animal that lives in the sea",
`say a random number between 1 and 300000 and a random animal that lives in the sea. Request ID: ${timestamp}-1`,
);
await adjustScreenView(page);
@ -66,8 +66,13 @@ test(
await page.getByText("Close").last().click();
// Change model to force different output
await selectGptModel(page);
// Change the prompt to ensure different output (avoid OpenAI caching)
await page
.getByTestId("textarea_str_input_value")
.first()
.fill(
`say a random number between 1 and 300000 and a random animal that lives in the sea. Request ID: ${timestamp}-2`,
);
await page.waitForSelector('[data-testid="button_run_chat output"]', {
timeout: 3000,
@ -133,27 +138,3 @@ test(
expect(secondRandomTextGeneratedByAI).toEqual(thirdRandomTextGeneratedByAI);
},
);
async function _moveSlider(
page: Page,
side: "left" | "right",
advanced: boolean = false,
) {
const thumbSelector = `slider_thumb${advanced ? "_advanced" : ""}`;
const trackSelector = `slider_track${advanced ? "_advanced" : ""}`;
await page.getByTestId(thumbSelector).click();
const trackBoundingBox = await page.getByTestId(trackSelector).boundingBox();
if (trackBoundingBox) {
const moveDistance =
trackBoundingBox.width * 0.1 * (side === "left" ? -1 : 1);
const centerX = trackBoundingBox.x + trackBoundingBox.width / 2;
const centerY = trackBoundingBox.y + trackBoundingBox.height / 2;
await page.mouse.move(centerX + moveDistance, centerY);
await page.mouse.down();
await page.mouse.up();
}
}

View File

@ -4,6 +4,7 @@ import { expect, test } from "../../fixtures";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
import { selectGptModel } from "../../utils/select-gpt-model";
withEventDeliveryModes(
"Research Translation Loop.spec",
@ -31,25 +32,13 @@ withEventDeliveryModes(
await initialGPTsetup(page, {
skipAdjustScreenView: true,
skipAddNewApiKeys: true,
skipSelectGptModel: true,
});
// TODO: Uncomment this when we have a way to test Anthropic
// await page.getByTestId("dropdown_str_provider").click();
// await page.getByTestId("Anthropic-1-option").click();
await page
.getByTestId("popover-anchor-input-api_key")
.last()
.fill(process.env.OPENAI_API_KEY ?? "");
await page.waitForSelector('[data-testid="dropdown_str_model_name"]', {
timeout: 5000,
});
await page.getByTestId("dropdown_str_model_name").click();
await page.keyboard.press("Enter");
await selectGptModel(page);
await page.getByTestId("playground-btn-flow-io").click();

View File

@ -59,6 +59,10 @@ test(
await initialGPTsetup(page);
await adjustScreenView(page);
await page
.getByTestId("popover-anchor-input-api_key")
.fill(process.env.OPENAI_API_KEY || "");
await page
.getByTestId("handle-chatinput-noshownode-chat message-source")
.click();

View File

@ -0,0 +1,20 @@
import type { Page } from "@playwright/test";
export const addOpenAiInputKey = async (page: Page) => {
const numberOfOpenAiFields = await page
.getByTestId("popover-anchor-input-openai_api_key")
.count();
for (let i = 0; i < numberOfOpenAiFields; i++) {
const openAiInput = page
.getByTestId("popover-anchor-input-openai_api_key")
.nth(i);
const inputValue = await openAiInput.inputValue();
if (!inputValue) {
await openAiInput.fill(process.env.OPENAI_API_KEY!);
}
await page.waitForTimeout(500);
}
};

View File

@ -9,7 +9,7 @@ export async function adjustScreenView(
} = {},
) {
await page.waitForSelector('[data-testid="canvas_controls_dropdown"]', {
timeout: 5000,
timeout: 30000,
});
let fitViewButton = await page.getByTestId("fit_view").count();

View File

@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import { adjustScreenView } from "./adjust-screen-view";
import { selectGptModel } from "./select-gpt-model";
import { updateOldComponents } from "./update-old-components";
import { addOpenAiInputKey } from "./add-open-ai-input-key";
export async function initialGPTsetup(
page: Page,
@ -9,6 +10,7 @@ export async function initialGPTsetup(
skipAdjustScreenView?: boolean;
skipUpdateOldComponents?: boolean;
skipSelectGptModel?: boolean;
skipAddOpenAiInputKey?: boolean;
},
) {
if (!options?.skipAdjustScreenView) {
@ -20,4 +22,7 @@ export async function initialGPTsetup(
if (!options?.skipSelectGptModel) {
await selectGptModel(page);
}
if (!options?.skipAddOpenAiInputKey) {
await addOpenAiInputKey(page);
}
}

View File

@ -9,11 +9,14 @@ export const selectGptModel = async (page: Page) => {
const gptOMiniOption = await page.getByTestId("gpt-4o-mini-option").count();
await page.waitForTimeout(500);
if (gptOMiniOption === 0) {
await page.getByTestId("manage-model-providers").click();
await page.waitForSelector("text=Model providers", { timeout: 30000 });
await page.getByTestId("provider-item-OpenAI").click();
await page.waitForTimeout(500);
const checkExistingKey = await page.getByTestId("input-end-icon").count();
if (checkExistingKey === 0) {
@ -26,6 +29,8 @@ export const selectGptModel = async (page: Page) => {
await page.getByTestId("llm-toggle-gpt-4o-mini").click();
await page.getByText("Close").last().click();
} else {
await page.waitForTimeout(500);
const isChecked = await page
.getByTestId("llm-toggle-gpt-4o-mini")
.isChecked();
@ -36,7 +41,7 @@ export const selectGptModel = async (page: Page) => {
await page.getByTestId("model_model").nth(i).click();
}
}
await page.waitForTimeout(500);
await page.getByTestId("gpt-4o-mini-option").click();
}
};