Files
OmniRoute/tests/unit/ui/search-tools-scrape-result.test.tsx
diegosouzapw 4640f0a77b feat(dashboard): refactor search-tools into Studio UI with 3 tabs (F8)
- SearchToolsClient: Studio orchestrator with Search/Scrape/Compare tabs
  and shared SearchToolsConfigPane; latency+cost metrics wired to TopBar
- SearchToolsTopBar: 3-tab switcher with metrics display and Export Code
  button (MockExportCodeModal placeholder annotated TODO(F7-merge))
- SearchToolsConfigPane: provider catalog inline with per-tab options
  (search-type, fetch format/full-page, rerank model, history)
- SearchConceptCard: collapsible explanatory cards for all 5 modalities
- ProviderCatalog: fetches /api/search/providers, renders 12 search + 3
  fetch providers with status badges (configured/missing/rate_limited)
- ScrapeResult: markdown preview + raw toggle + D21 256KB cap with
  truncation warning and full-content raw modal
- tabs/SearchTab: SearchForm + ResultsPanel + RerankPanel with
  noProvidersConfigured CTA wired
- tabs/ScrapeTab: URL input + /v1/web/fetch call via useScrapeFetch hook
- tabs/CompareTab: up to 4 providers (D22) in parallel via Promise.allSettled,
  overlap calculation, best/worst coloring
- hooks/useScrapeFetch: fetch wrapper for /v1/web/fetch with latency
- SearchForm: extended with catalog provider metadata badges
- ResultsPanel: extended with noProvidersConfigured CTA empty state
- ProviderComparison: "Size" hardcoded text annotated with data-i18n attr
- 7 vitest UI test files: 75 tests covering all acceptance criteria
2026-05-28 09:44:09 -03:00

209 lines
8.1 KiB
TypeScript

// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// Lazy import of MarkdownMessage via ScrapeResult's dynamic import — mock it
vi.mock(
"../../../src/app/(dashboard)/dashboard/playground/components/MarkdownMessage",
() => ({
default: ({ content }: { content: string }) =>
React.createElement("div", { "data-testid": "markdown-render" }, content),
}),
);
// ── Fixtures ──────────────────────────────────────────────────────────────────
const CONTENT_256KB_EXACT = "x".repeat(256 * 1024);
const CONTENT_UNDER_CAP = "# Hello\nThis is markdown content";
const CONTENT_OVER_CAP = "y".repeat(256 * 1024 + 100);
function makeScrapeResult(content: string) {
return {
provider: "firecrawl",
url: "https://example.com",
content,
links: ["https://example.com/link1"],
metadata: { title: "Test Page", description: "A test page" },
screenshot_url: null,
};
}
// ── Import component after mocks ──────────────────────────────────────────────
const { default: ScrapeResult } = await import(
"../../../src/app/(dashboard)/dashboard/search-tools/components/ScrapeResult"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderScrapeResult(content: string, latencyMs?: number): HTMLDivElement {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
React.createElement(ScrapeResult, { result: makeScrapeResult(content), latencyMs }),
);
});
containers.push({ root, el });
return el;
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("ScrapeResult", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
document.body.innerHTML = "";
});
it("renders scrape-result data-testid", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
expect(el.querySelector("[data-testid='scrape-result']")).toBeTruthy();
});
it("shows metadata (title and url)", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
expect(el.textContent).toContain("Test Page");
expect(el.textContent).toContain("https://example.com");
});
it("shows latency when provided", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP, 142);
expect(el.textContent).toContain("142");
});
it("shows provider name", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
expect(el.textContent).toContain("firecrawl");
});
it("shows links count", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
// 1 link in fixture
expect(el.textContent).toContain("1");
});
it("defaults to markdown preview mode", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
const markdownToggle = el.querySelector("[data-testid='toggle-markdown']");
expect(markdownToggle?.classList.contains("text-primary")).toBe(true);
// Markdown render area should exist
const markdownPreview = el.querySelector("[data-testid='markdown-preview']");
expect(markdownPreview).toBeTruthy();
});
it("switches to raw mode when Raw toggle is clicked", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
const rawToggle = el.querySelector("[data-testid='toggle-raw']") as HTMLButtonElement;
act(() => {
rawToggle.click();
});
const rawContent = el.querySelector("[data-testid='raw-content']");
expect(rawContent).toBeTruthy();
// Markdown preview should be gone
const markdownPreview = el.querySelector("[data-testid='markdown-preview']");
expect(markdownPreview).toBeNull();
});
it("switches back to markdown when Preview toggle is clicked", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
const rawToggle = el.querySelector("[data-testid='toggle-raw']") as HTMLButtonElement;
act(() => {
rawToggle.click();
});
const markdownToggle = el.querySelector("[data-testid='toggle-markdown']") as HTMLButtonElement;
act(() => {
markdownToggle.click();
});
const markdownPreview = el.querySelector("[data-testid='markdown-preview']");
expect(markdownPreview).toBeTruthy();
});
it("does NOT show truncation warning for content under 256KB", () => {
const el = renderScrapeResult(CONTENT_UNDER_CAP);
const warning = el.querySelector("[data-testid='truncation-warning']");
expect(warning).toBeNull();
});
it("D21 — shows truncation warning for content exactly at 256KB", () => {
const el = renderScrapeResult(CONTENT_256KB_EXACT);
// Exactly at 256KB → NOT truncated (> cap, not >=)
// 256*1024 bytes — the cap is > so 256*1024 == cap is at boundary
// Per implementation: isTruncated = contentSize > CONTENT_CAP_BYTES
// CONTENT_CAP_BYTES = 256*1024 so exactly at cap is NOT truncated
const warning = el.querySelector("[data-testid='truncation-warning']");
expect(warning).toBeNull();
});
it("D21 — shows truncation warning for content over 256KB", () => {
const el = renderScrapeResult(CONTENT_OVER_CAP);
const warning = el.querySelector("[data-testid='truncation-warning']");
expect(warning).toBeTruthy();
expect(warning?.textContent).toContain("256 KB");
});
it("D21 — 'View raw' button opens raw modal for truncated content", () => {
const el = renderScrapeResult(CONTENT_OVER_CAP);
const viewRawBtn = el.querySelector("[data-testid='view-raw-button']") as HTMLButtonElement;
expect(viewRawBtn).toBeTruthy();
act(() => {
viewRawBtn.click();
});
const rawModal = el.querySelector("[data-testid='raw-modal']");
expect(rawModal).toBeTruthy();
});
it("D21 — raw modal shows full content", () => {
const longContent = "z".repeat(256 * 1024 + 500);
const el = renderScrapeResult(longContent);
const viewRawBtn = el.querySelector("[data-testid='view-raw-button']") as HTMLButtonElement;
act(() => {
viewRawBtn.click();
});
const rawModalContent = el.querySelector("[data-testid='raw-modal-content']") as HTMLTextAreaElement;
expect(rawModalContent).toBeTruthy();
// Full content is in the modal
expect(rawModalContent.value.length).toBe(longContent.length);
});
it("D21 — raw modal can be closed", () => {
const el = renderScrapeResult(CONTENT_OVER_CAP);
const viewRawBtn = el.querySelector("[data-testid='view-raw-button']") as HTMLButtonElement;
act(() => {
viewRawBtn.click();
});
expect(el.querySelector("[data-testid='raw-modal']")).toBeTruthy();
const closeBtn = el
.querySelector("[data-testid='raw-modal']")
?.querySelector("button[aria-label='Close raw content modal']") as HTMLButtonElement;
act(() => {
closeBtn.click();
});
expect(el.querySelector("[data-testid='raw-modal']")).toBeNull();
});
it("size display in meta bar shows KB for large content", () => {
const el = renderScrapeResult("x".repeat(2048));
expect(el.textContent).toMatch(/\d+(\.\d+)? KB/);
});
});