-
Converted
-
- {convertedFileChip.iconKind === 'image' && 'πΌοΈ'}
- {convertedFileChip.iconKind === 'audio' && 'π΅'}
- {convertedFileChip.iconKind === 'video' && 'π₯'}
- {convertedFileChip.iconKind === 'file' && 'π'}
-
-
- {convertedFileChip.name}
-
-
-
-
- Open
-
-
-
}
- onClick={onClearConvertedFileChip}
- data-testid="clear-converted-file-chip"
- />
+
+
+
Converted
+
+ {convertedFileChip.iconKind === 'image' && 'πΌοΈ'}
+ {convertedFileChip.iconKind === 'audio' && 'π΅'}
+ {convertedFileChip.iconKind === 'video' && 'π₯'}
+ {convertedFileChip.iconKind === 'file' && 'π'}
+
+
+ {convertedFileChip.name}
+
+
+
+
+ Open
+
+
+
}
+ onClick={onClearConvertedFileChip}
+ data-testid="clear-converted-file-chip"
+ />
+
+ {convertedFileChip.iconKind === 'image' && (
+

+ )}
+ {convertedFileChip.iconKind === 'audio' && (
+
+ )}
+ {convertedFileChip.iconKind === 'video' && (
+
+ )}
)}
>
From 8eff901a0b679998f00c5de07c4e877d67943b30 Mon Sep 17 00:00:00 2001
From: jbolor21 <86250273+jbolor21@users.noreply.github.com>
Date: Fri, 15 May 2026 11:01:51 -0700
Subject: [PATCH 4/4] e2e tests
---
frontend/e2e/converters.spec.ts | 98 ++++++++++++++++++++++++++++++++-
1 file changed, 97 insertions(+), 1 deletion(-)
diff --git a/frontend/e2e/converters.spec.ts b/frontend/e2e/converters.spec.ts
index 1aa0c5096d..085e080e06 100644
--- a/frontend/e2e/converters.spec.ts
+++ b/frontend/e2e/converters.spec.ts
@@ -48,6 +48,14 @@ const MOCK_CATALOG = {
is_llm_based: false,
description: "Compresses images.",
},
+ {
+ converter_type: "AddImageTextConverter",
+ supported_input_types: ["text"],
+ supported_output_types: ["image_path"],
+ parameters: [],
+ is_llm_based: false,
+ description: "Renders text onto a generated image.",
+ },
{
converter_type: "TranslationConverter",
supported_input_types: ["text"],
@@ -61,6 +69,17 @@ const MOCK_CATALOG = {
const MOCK_CONVERSATION_ID = "e2e-conv-001";
+// 1x1 transparent PNG returned by the mock /api/media route so that the
+// inline image preview
![]()
element can resolve its src in headless mode.
+const TINY_PNG_BASE64 =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGIAAgAABQABDQotsgAAAABJRU5ErkJggg==";
+
+// Map of mock image-output converter types β path returned by the preview
+// mock. Used to decide whether to emit text vs. image_path output.
+const IMAGE_OUTPUT_CONVERTERS: Record
= {
+ AddImageTextConverter: "/tmp/output.png",
+};
+
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -73,6 +92,9 @@ const MOCK_CONVERSATION_ID = "e2e-conv-001";
*/
async function mockBackendAPIs(page: Page) {
let accumulatedMessages: Record[] = [];
+ // Track the converter type for each created converter instance so the
+ // preview mock can decide between text and image_path output.
+ const converterTypeById: Record = {};
// ββ Converter-specific routes ββββββββββββββββββββββββββββββββββββββββββ
@@ -89,6 +111,26 @@ async function mockBackendAPIs(page: Page) {
await page.route(/\/api\/converters\/preview/, async (route) => {
if (route.request().method() === "POST") {
const body = JSON.parse(route.request().postData() ?? "{}");
+ const converterIds: string[] = body.converter_ids ?? [];
+ const converterType = converterTypeById[converterIds[0] ?? ""] ?? "";
+
+ // Image-output converters emit a file path the frontend renders via
+ // /api/media β triggers the convertedFileChip + inline preview branch.
+ if (IMAGE_OUTPUT_CONVERTERS[converterType]) {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ original_value: body.original_value,
+ original_value_data_type: body.original_value_data_type ?? "text",
+ converted_value: IMAGE_OUTPUT_CONVERTERS[converterType],
+ converted_value_data_type: "image_path",
+ steps: [],
+ }),
+ });
+ return;
+ }
+
const converted = Buffer.from(body.original_value ?? "").toString("base64");
await route.fulfill({
status: 200,
@@ -108,11 +150,13 @@ async function mockBackendAPIs(page: Page) {
await page.route(/\/api\/converters$/, async (route) => {
if (route.request().method() === "POST") {
const body = JSON.parse(route.request().postData() ?? "{}");
+ const converterId = `mock-converter-${body.type}`;
+ converterTypeById[converterId] = body.type;
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify({
- converter_id: "mock-converter-001",
+ converter_id: converterId,
converter_type: body.type,
display_name: null,
}),
@@ -122,6 +166,16 @@ async function mockBackendAPIs(page: Page) {
}
});
+ // Media route β serves the generated image referenced by the preview
+ // mock. Returning a tiny valid PNG keeps the
element layout-stable.
+ await page.route(/\/api\/media\?path=/, async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "image/png",
+ body: Buffer.from(TINY_PNG_BASE64, "base64"),
+ });
+ });
+
// ββ Standard chat routes (matching chat.spec.ts pattern) βββββββββββββββ
// Targets list
@@ -515,4 +569,46 @@ test.describe("Converter Panel", () => {
// The converter badge should be visible in the attack table
await expect(page.getByText("Base64Converter")).toBeVisible({ timeout: 10000 });
});
+
+ test("should render inline image preview in input box after a textβimage conversion", async ({ page }) => {
+ // Type a prompt before opening the panel
+ await page.getByTestId("chat-input").fill("hello world");
+
+ // Select AddImageTextConverter (text input β image_path output)
+ await selectConverter(page, "AddImageTextConverter");
+
+ // Auto-preview only fires for text-output converters, so click Preview
+ await page.getByTestId("converter-preview-btn").click();
+ await expect(page.getByTestId("converter-preview-result")).toBeVisible({ timeout: 10000 });
+
+ // Apply the converted value to the input
+ await page.getByTestId("use-converted-btn").click();
+
+ // Close converter panel so the input area is unobscured
+ await page.getByTestId("close-converter-panel-btn").click();
+ await expect(page.getByTestId("converter-panel")).not.toBeVisible();
+
+ // The converted-file-chip block is rendered in the input area for image_path outputs
+ const chip = page.getByTestId("converted-file-chip");
+ await expect(chip).toBeVisible();
+ await expect(chip).toContainText("output.png");
+
+ // The inline image preview is rendered alongside the filename + Open link
+ const preview = page.getByTestId("converted-file-preview-image");
+ await expect(preview).toBeVisible({ timeout: 10000 });
+ await expect(preview).toHaveAttribute("src", /\/api\/media\?path=/);
+ await expect(preview).toHaveAttribute("alt", "output.png");
+
+ // The Open link is still present alongside the preview
+ await expect(page.getByTestId("converted-file-open")).toHaveAttribute("href", /\/api\/media\?path=/);
+
+ // Audio / video previews must NOT be rendered for an image conversion
+ await expect(page.getByTestId("converted-file-preview-audio")).toHaveCount(0);
+ await expect(page.getByTestId("converted-file-preview-video")).toHaveCount(0);
+
+ // Dismissing the chip clears the entire block (including the preview)
+ await page.getByTestId("clear-converted-file-chip").click();
+ await expect(chip).not.toBeVisible();
+ await expect(page.getByTestId("converted-file-preview-image")).toHaveCount(0);
+ });
});