mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
feat(translator): lift useTranslateSession to shell + connect PipelineView to real result
- Move useTranslateSession() to TranslatorPageClient (shell level) for PipelineView to receive real steps - Build PipelineStep[] from session result (detected/intermediate/translated/response) - Pass session as prop down to TranslateTab; internal hook kept for isolated test compatibility - Dedup useProviderOptions: only TranslateTab calls the hook, props pass to SimpleControls - Remove unused data-advanced-section/data-input-text placeholder (DOM data leak GAP-5) - Add aria-expanded + aria-controls to AutoFeaturesCard toggle (a11y GAP-2) Addresses code review RISCO-4, GAP-2, GAP-3, GAP-5.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Suspense, useState } from "react";
|
import { Suspense, useMemo, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Badge, Card, SegmentedControl } from "@/shared/components";
|
import { Badge, Card, SegmentedControl } from "@/shared/components";
|
||||||
import TranslatorConceptCard from "./components/TranslatorConceptCard";
|
import TranslatorConceptCard from "./components/TranslatorConceptCard";
|
||||||
@@ -9,10 +9,12 @@ import MonitorTab from "./components/MonitorTab";
|
|||||||
import AdvancedSection from "./components/advanced/AdvancedSection";
|
import AdvancedSection from "./components/advanced/AdvancedSection";
|
||||||
import RawJsonPanel from "./components/advanced/RawJsonPanel";
|
import RawJsonPanel from "./components/advanced/RawJsonPanel";
|
||||||
import PipelineView from "./components/advanced/PipelineView";
|
import PipelineView from "./components/advanced/PipelineView";
|
||||||
|
import type { PipelineStep } from "./components/advanced/PipelineView";
|
||||||
import StreamTransformerAccordion from "./components/advanced/StreamTransformerAccordion";
|
import StreamTransformerAccordion from "./components/advanced/StreamTransformerAccordion";
|
||||||
import TestBenchAccordion from "./components/advanced/TestBenchAccordion";
|
import TestBenchAccordion from "./components/advanced/TestBenchAccordion";
|
||||||
import CompressionPreviewAccordion from "./components/advanced/CompressionPreviewAccordion";
|
import CompressionPreviewAccordion from "./components/advanced/CompressionPreviewAccordion";
|
||||||
import { useTranslateDeepLink } from "./hooks/useTranslateDeepLink";
|
import { useTranslateDeepLink } from "./hooks/useTranslateDeepLink";
|
||||||
|
import { useTranslateSession } from "./hooks/useTranslateSession";
|
||||||
import type { AdvancedSlug, TranslatorTab } from "./types";
|
import type { AdvancedSlug, TranslatorTab } from "./types";
|
||||||
|
|
||||||
export default function TranslatorPageClient() {
|
export default function TranslatorPageClient() {
|
||||||
@@ -28,6 +30,9 @@ function TranslatorPageClientInner() {
|
|||||||
const [sharedInputContent, setSharedInputContent] = useState("");
|
const [sharedInputContent, setSharedInputContent] = useState("");
|
||||||
const { state, setTab, setAdvanced } = useTranslateDeepLink();
|
const { state, setTab, setAdvanced } = useTranslateDeepLink();
|
||||||
|
|
||||||
|
// Lift session to shell so PipelineView can receive real steps
|
||||||
|
const session = useTranslateSession();
|
||||||
|
|
||||||
const makeOpenHandler = (slug: AdvancedSlug) => (open: boolean) => {
|
const makeOpenHandler = (slug: AdvancedSlug) => (open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setAdvanced(slug);
|
setAdvanced(slug);
|
||||||
@@ -36,6 +41,80 @@ function TranslatorPageClientInner() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tr = (key: string, fallback: string): string => {
|
||||||
|
try {
|
||||||
|
const v = t(key as Parameters<typeof t>[0]);
|
||||||
|
if (v === key || v === `translator.${key}`) return fallback;
|
||||||
|
return v as string;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build PipelineStep[] from session.result so PipelineView reflects real state
|
||||||
|
const pipelineSteps = useMemo<PipelineStep[]>(() => {
|
||||||
|
const r = session.result;
|
||||||
|
if (r.status === "idle") return [];
|
||||||
|
|
||||||
|
const steps: PipelineStep[] = [];
|
||||||
|
|
||||||
|
// Step 1 — Client Request (always present once started)
|
||||||
|
steps.push({
|
||||||
|
id: "1",
|
||||||
|
name: tr("pipelineStepClientRequest", "Client Request"),
|
||||||
|
description: tr("pipelineStepClientRequestDesc", "Request received in client format"),
|
||||||
|
format: r.detected ?? "openai",
|
||||||
|
content: sharedInputContent.slice(0, 500),
|
||||||
|
status: r.status === "error" ? "error" : "done",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2 — Format Detected
|
||||||
|
steps.push({
|
||||||
|
id: "2",
|
||||||
|
name: tr("pipelineStepFormatDetected", "Format Detected"),
|
||||||
|
description: tr("pipelineStepFormatDetectedDesc", "Auto-detected source format"),
|
||||||
|
format: r.detected ?? null,
|
||||||
|
content: r.detected ? JSON.stringify({ detectedFormat: r.detected, confidence: "high" }, null, 2) : "",
|
||||||
|
status: r.detected ? "done" : r.status === "translating" ? "active" : "pending",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3 — OpenAI Intermediate (only when hub-and-spoke)
|
||||||
|
if (r.pipelinePath === "hub-and-spoke") {
|
||||||
|
steps.push({
|
||||||
|
id: "3",
|
||||||
|
name: tr("pipelineStepOpenAIIntermediate", "OpenAI Intermediate"),
|
||||||
|
description: tr("pipelineStepOpenAIIntermediateDesc", "Translated to OpenAI hub format"),
|
||||||
|
format: "openai",
|
||||||
|
content: r.intermediateJson ?? "",
|
||||||
|
status: r.intermediateJson ? "done" : r.status === "translating" ? "active" : "pending",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4 — Provider Format (translated result)
|
||||||
|
steps.push({
|
||||||
|
id: r.pipelinePath === "hub-and-spoke" ? "4" : "3",
|
||||||
|
name: tr("pipelineStepProviderFormat", "Provider Format"),
|
||||||
|
description: tr("pipelineStepProviderFormatDesc", "Translated to provider target format"),
|
||||||
|
format: r.target,
|
||||||
|
content: r.translatedJson ?? "",
|
||||||
|
status: r.translatedJson ? "done" : r.status === "translating" ? "active" : "pending",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 5 — Provider Response (only when mode=send and response present)
|
||||||
|
if (r.responsePreview !== null) {
|
||||||
|
steps.push({
|
||||||
|
id: r.pipelinePath === "hub-and-spoke" ? "5" : "4",
|
||||||
|
name: tr("pipelineStepProviderResponse", "Provider Response"),
|
||||||
|
description: tr("pipelineStepProviderResponseDesc", "Streaming response from provider"),
|
||||||
|
format: "openai",
|
||||||
|
content: r.responsePreview,
|
||||||
|
status: r.status === "ok" ? "done" : r.status === "sending" ? "active" : "pending",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}, [session.result, sharedInputContent]);
|
||||||
|
|
||||||
const advancedSlot = (
|
const advancedSlot = (
|
||||||
<AdvancedSection forceOpenSlug={state.advanced}>
|
<AdvancedSection forceOpenSlug={state.advanced}>
|
||||||
<RawJsonPanel
|
<RawJsonPanel
|
||||||
@@ -47,7 +126,7 @@ function TranslatorPageClientInner() {
|
|||||||
slug="pipeline"
|
slug="pipeline"
|
||||||
forceOpen={state.advanced === "pipeline"}
|
forceOpen={state.advanced === "pipeline"}
|
||||||
onOpenChange={makeOpenHandler("pipeline")}
|
onOpenChange={makeOpenHandler("pipeline")}
|
||||||
steps={[]}
|
pipelineSteps={pipelineSteps.length > 0 ? pipelineSteps : undefined}
|
||||||
/>
|
/>
|
||||||
<StreamTransformerAccordion
|
<StreamTransformerAccordion
|
||||||
forceOpen={state.advanced === "streamtransform"}
|
forceOpen={state.advanced === "streamtransform"}
|
||||||
@@ -92,6 +171,7 @@ function TranslatorPageClientInner() {
|
|||||||
<TranslateTab
|
<TranslateTab
|
||||||
forceOpenAdvancedSlug={state.advanced}
|
forceOpenAdvancedSlug={state.advanced}
|
||||||
onAdvancedSlugChange={(slug) => setAdvanced(slug)}
|
onAdvancedSlugChange={(slug) => setAdvanced(slug)}
|
||||||
|
session={session}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -111,7 +191,10 @@ function AutoFeaturesCard() {
|
|||||||
return (
|
return (
|
||||||
<Card className="border-primary/10 bg-primary/5">
|
<Card className="border-primary/10 bg-primary/5">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setShowFeatures((prev) => !prev)}
|
onClick={() => setShowFeatures((prev) => !prev)}
|
||||||
|
aria-expanded={showFeatures}
|
||||||
|
aria-controls="auto-features-grid"
|
||||||
className="flex w-full items-center justify-between p-4 text-left"
|
className="flex w-full items-center justify-between p-4 text-left"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -130,6 +213,7 @@ function AutoFeaturesCard() {
|
|||||||
|
|
||||||
{showFeatures && (
|
{showFeatures && (
|
||||||
<div
|
<div
|
||||||
|
id="auto-features-grid"
|
||||||
className="grid grid-cols-1 gap-3 px-4 pb-4 sm:grid-cols-2 lg:grid-cols-4"
|
className="grid grid-cols-1 gap-3 px-4 pb-4 sm:grid-cols-2 lg:grid-cols-4"
|
||||||
data-testid="auto-features-grid"
|
data-testid="auto-features-grid"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useTranslations } from "next-intl";
|
|||||||
import { Button, Select, SegmentedControl } from "@/shared/components";
|
import { Button, Select, SegmentedControl } from "@/shared/components";
|
||||||
import { InfoTooltip } from "@/shared/components";
|
import { InfoTooltip } from "@/shared/components";
|
||||||
import { FORMAT_OPTIONS, FORMAT_META, getExampleTemplates } from "../exampleTemplates";
|
import { FORMAT_OPTIONS, FORMAT_META, getExampleTemplates } from "../exampleTemplates";
|
||||||
import { useProviderOptions } from "../hooks/useProviderOptions";
|
|
||||||
import type { FormatId, TranslateMode } from "../types";
|
import type { FormatId, TranslateMode } from "../types";
|
||||||
|
|
||||||
interface SimpleControlsProps {
|
interface SimpleControlsProps {
|
||||||
@@ -22,6 +21,8 @@ interface SimpleControlsProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onOpenAdvanced: () => void;
|
onOpenAdvanced: () => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
providerOptions: Array<{ value: string; label: string }>;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SimpleControls({
|
export default function SimpleControls({
|
||||||
@@ -38,9 +39,10 @@ export default function SimpleControls({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onOpenAdvanced,
|
onOpenAdvanced,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
providerOptions,
|
||||||
|
loading = false,
|
||||||
}: SimpleControlsProps) {
|
}: SimpleControlsProps) {
|
||||||
const t = useTranslations("translator");
|
const t = useTranslations("translator");
|
||||||
const { providerOptions } = useProviderOptions(provider);
|
|
||||||
|
|
||||||
const tr = useCallback(
|
const tr = useCallback(
|
||||||
(key: string, fallback: string): string => {
|
(key: string, fallback: string): string => {
|
||||||
@@ -150,6 +152,7 @@ export default function SimpleControls({
|
|||||||
options={providerOptions.length > 0 ? providerOptions : [{ value: provider, label: provider }]}
|
options={providerOptions.length > 0 ? providerOptions : [{ value: provider, label: provider }]}
|
||||||
value={provider}
|
value={provider}
|
||||||
onChange={handleProviderChange}
|
onChange={handleProviderChange}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Card } from "@/shared/components";
|
import { Card } from "@/shared/components";
|
||||||
import { useTranslateSession } from "../hooks/useTranslateSession";
|
import { useTranslateSession } from "../hooks/useTranslateSession";
|
||||||
|
import type { UseTranslateSessionReturn } from "../hooks/useTranslateSession";
|
||||||
import { useProviderOptions } from "../hooks/useProviderOptions";
|
import { useProviderOptions } from "../hooks/useProviderOptions";
|
||||||
import SimpleControls from "./SimpleControls";
|
import SimpleControls from "./SimpleControls";
|
||||||
import ResultNarrated from "./ResultNarrated";
|
import ResultNarrated from "./ResultNarrated";
|
||||||
@@ -19,11 +20,18 @@ interface TranslateTabProps {
|
|||||||
* (open or close). F9 syncs this with the URL query string.
|
* (open or close). F9 syncs this with the URL query string.
|
||||||
*/
|
*/
|
||||||
onAdvancedSlugChange?: (slug: AdvancedSlug | null) => void;
|
onAdvancedSlugChange?: (slug: AdvancedSlug | null) => void;
|
||||||
|
/**
|
||||||
|
* Optional session lifted from shell (TranslatorPageClient) so PipelineView
|
||||||
|
* can read the result at the shell level. When undefined, an internal session
|
||||||
|
* is used (isolated rendering mode, e.g. tests).
|
||||||
|
*/
|
||||||
|
session?: UseTranslateSessionReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TranslateTab({
|
export default function TranslateTab({
|
||||||
forceOpenAdvancedSlug = null,
|
forceOpenAdvancedSlug = null,
|
||||||
onAdvancedSlugChange,
|
onAdvancedSlugChange,
|
||||||
|
session: sessionProp,
|
||||||
}: TranslateTabProps) {
|
}: TranslateTabProps) {
|
||||||
// Internal simple-mode state
|
// Internal simple-mode state
|
||||||
const [source, setSource] = useState<FormatId>("claude");
|
const [source, setSource] = useState<FormatId>("claude");
|
||||||
@@ -31,11 +39,14 @@ export default function TranslateTab({
|
|||||||
const [mode, setMode] = useState<TranslateMode>("send");
|
const [mode, setMode] = useState<TranslateMode>("send");
|
||||||
|
|
||||||
// Provider/target state: derive from useProviderOptions
|
// Provider/target state: derive from useProviderOptions
|
||||||
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
|
// GAP-3: useProviderOptions lives only here; SimpleControls receives it as props
|
||||||
|
const { provider, setProvider, providerOptions, loading } = useProviderOptions("openai");
|
||||||
// target FormatId mirrors provider selection; managed via SimpleControls callback
|
// target FormatId mirrors provider selection; managed via SimpleControls callback
|
||||||
const [target, setTarget] = useState<FormatId>("openai");
|
const [target, setTarget] = useState<FormatId>("openai");
|
||||||
|
|
||||||
const { result, run } = useTranslateSession();
|
// Rules of Hooks: always call unconditionally; fall back to prop when provided
|
||||||
|
const internalSession = useTranslateSession();
|
||||||
|
const { result, run } = sessionProp ?? internalSession;
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
run({ source, target, provider, inputText, mode });
|
run({ source, target, provider, inputText, mode });
|
||||||
@@ -45,11 +56,6 @@ export default function TranslateTab({
|
|||||||
if (onAdvancedSlugChange) {
|
if (onAdvancedSlugChange) {
|
||||||
onAdvancedSlugChange(slug);
|
onAdvancedSlugChange(slug);
|
||||||
}
|
}
|
||||||
// Scroll to advanced section if it exists (guard for environments without scrollIntoView)
|
|
||||||
const advancedEl = document.querySelector("[data-advanced-section]");
|
|
||||||
if (advancedEl && typeof advancedEl.scrollIntoView === "function") {
|
|
||||||
advancedEl.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSeeTranslatedJson = () => {
|
const handleSeeTranslatedJson = () => {
|
||||||
@@ -60,10 +66,6 @@ export default function TranslateTab({
|
|||||||
handleOpenAdvanced("pipeline");
|
handleOpenAdvanced("pipeline");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose forceOpenAdvancedSlug to the advanced section slot via data attribute
|
|
||||||
// F4 will read this; for now we write it to a data attribute that F9 will wire
|
|
||||||
const advancedSlug = forceOpenAdvancedSlug;
|
|
||||||
|
|
||||||
// Sync provider options: when providerOptions loads, keep provider in sync
|
// Sync provider options: when providerOptions loads, keep provider in sync
|
||||||
// (useProviderOptions handles this internally; we just need to expose setProvider)
|
// (useProviderOptions handles this internally; we just need to expose setProvider)
|
||||||
const handleProviderChange = (prov: string) => {
|
const handleProviderChange = (prov: string) => {
|
||||||
@@ -90,6 +92,8 @@ export default function TranslateTab({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onOpenAdvanced={() => handleOpenAdvanced("rawjson")}
|
onOpenAdvanced={() => handleOpenAdvanced("rawjson")}
|
||||||
isLoading={result.status === "translating" || result.status === "sending"}
|
isLoading={result.status === "translating" || result.status === "sending"}
|
||||||
|
providerOptions={providerOptions}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -100,17 +104,6 @@ export default function TranslateTab({
|
|||||||
onSeePipeline={handleSeePipeline}
|
onSeePipeline={handleSeePipeline}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced section slot — F4 will mount AdvancedSection here.
|
|
||||||
F9 composes: <TranslateTab onAdvancedSlugChange={...} /> + <AdvancedSection forceOpenSlug={...} />.
|
|
||||||
For now we render the placeholder that F4/F9 will replace. */}
|
|
||||||
<div
|
|
||||||
data-advanced-section
|
|
||||||
data-force-open-slug={advancedSlug ?? ""}
|
|
||||||
data-provider-options={JSON.stringify(providerOptions.map((o) => o.value))}
|
|
||||||
data-source={source}
|
|
||||||
data-input-text={inputText.slice(0, 100)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,28 @@ vi.mock("@/shared/components", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ── useTranslateSession stub (now lifted to shell) ────────────────────────────
|
||||||
|
vi.mock(
|
||||||
|
"@/app/(dashboard)/dashboard/translator/hooks/useTranslateSession",
|
||||||
|
() => ({
|
||||||
|
useTranslateSession: () => ({
|
||||||
|
result: {
|
||||||
|
detected: null,
|
||||||
|
target: "openai",
|
||||||
|
status: "idle",
|
||||||
|
responsePreview: null,
|
||||||
|
translatedJson: null,
|
||||||
|
pipelinePath: null,
|
||||||
|
intermediateJson: null,
|
||||||
|
errorMessage: null,
|
||||||
|
latencyMs: null,
|
||||||
|
},
|
||||||
|
run: vi.fn(),
|
||||||
|
reset: vi.fn(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// ── Sub-component stubs that capture received props ───────────────────────────
|
// ── Sub-component stubs that capture received props ───────────────────────────
|
||||||
const capturedTranslateTabProps: Array<{
|
const capturedTranslateTabProps: Array<{
|
||||||
forceOpenAdvancedSlug?: string | null;
|
forceOpenAdvancedSlug?: string | null;
|
||||||
@@ -100,6 +122,7 @@ vi.mock(
|
|||||||
}: {
|
}: {
|
||||||
forceOpenAdvancedSlug?: string | null;
|
forceOpenAdvancedSlug?: string | null;
|
||||||
onAdvancedSlugChange?: (slug: string | null) => void;
|
onAdvancedSlugChange?: (slug: string | null) => void;
|
||||||
|
session?: unknown;
|
||||||
}) => {
|
}) => {
|
||||||
capturedTranslateTabProps.push({ forceOpenAdvancedSlug });
|
capturedTranslateTabProps.push({ forceOpenAdvancedSlug });
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +177,7 @@ vi.mock(
|
|||||||
vi.mock(
|
vi.mock(
|
||||||
"@/app/(dashboard)/dashboard/translator/components/advanced/PipelineView",
|
"@/app/(dashboard)/dashboard/translator/components/advanced/PipelineView",
|
||||||
() => ({
|
() => ({
|
||||||
default: ({ forceOpen }: { forceOpen?: boolean; steps?: unknown[] }) => (
|
default: ({ forceOpen }: { forceOpen?: boolean; pipelineSteps?: unknown[] }) => (
|
||||||
<div data-testid="pipeline-view" data-force-open={String(forceOpen ?? false)} />
|
<div data-testid="pipeline-view" data-force-open={String(forceOpen ?? false)} />
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -94,6 +94,28 @@ vi.mock("@/shared/components", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ── useTranslateSession stub (now lifted to shell) ────────────────────────────
|
||||||
|
vi.mock(
|
||||||
|
"@/app/(dashboard)/dashboard/translator/hooks/useTranslateSession",
|
||||||
|
() => ({
|
||||||
|
useTranslateSession: () => ({
|
||||||
|
result: {
|
||||||
|
detected: null,
|
||||||
|
target: "openai",
|
||||||
|
status: "idle",
|
||||||
|
responsePreview: null,
|
||||||
|
translatedJson: null,
|
||||||
|
pipelinePath: null,
|
||||||
|
intermediateJson: null,
|
||||||
|
errorMessage: null,
|
||||||
|
latencyMs: null,
|
||||||
|
},
|
||||||
|
run: vi.fn(),
|
||||||
|
reset: vi.fn(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// ── Sub-component stubs ───────────────────────────────────────────────────────
|
// ── Sub-component stubs ───────────────────────────────────────────────────────
|
||||||
vi.mock(
|
vi.mock(
|
||||||
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard",
|
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard",
|
||||||
@@ -111,6 +133,7 @@ vi.mock(
|
|||||||
}: {
|
}: {
|
||||||
forceOpenAdvancedSlug?: string | null;
|
forceOpenAdvancedSlug?: string | null;
|
||||||
onAdvancedSlugChange?: (slug: string | null) => void;
|
onAdvancedSlugChange?: (slug: string | null) => void;
|
||||||
|
session?: unknown;
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
data-testid="translate-tab"
|
data-testid="translate-tab"
|
||||||
@@ -159,7 +182,7 @@ vi.mock(
|
|||||||
vi.mock(
|
vi.mock(
|
||||||
"@/app/(dashboard)/dashboard/translator/components/advanced/PipelineView",
|
"@/app/(dashboard)/dashboard/translator/components/advanced/PipelineView",
|
||||||
() => ({
|
() => ({
|
||||||
default: ({ forceOpen }: { forceOpen?: boolean }) => (
|
default: ({ forceOpen }: { forceOpen?: boolean; pipelineSteps?: unknown[] }) => (
|
||||||
<div data-testid="pipeline-view" data-force-open={String(forceOpen ?? false)} />
|
<div data-testid="pipeline-view" data-force-open={String(forceOpen ?? false)} />
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -88,22 +88,6 @@ vi.mock("@/shared/components", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// --- Mock useProviderOptions ---
|
|
||||||
vi.mock(
|
|
||||||
"@/app/(dashboard)/dashboard/translator/hooks/useProviderOptions",
|
|
||||||
() => ({
|
|
||||||
useProviderOptions: () => ({
|
|
||||||
provider: "openai",
|
|
||||||
setProvider: vi.fn(),
|
|
||||||
providerOptions: [
|
|
||||||
{ value: "openai", label: "OpenAI" },
|
|
||||||
{ value: "anthropic", label: "Anthropic" },
|
|
||||||
],
|
|
||||||
loading: false,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Mock useAvailableModels ---
|
// --- Mock useAvailableModels ---
|
||||||
vi.mock(
|
vi.mock(
|
||||||
"@/app/(dashboard)/dashboard/translator/hooks/useAvailableModels",
|
"@/app/(dashboard)/dashboard/translator/hooks/useAvailableModels",
|
||||||
@@ -180,6 +164,8 @@ function makeProps(overrides: Partial<{
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onOpenAdvanced: () => void;
|
onOpenAdvanced: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
providerOptions: Array<{ value: string; label: string }>;
|
||||||
|
loading: boolean;
|
||||||
}> = {}) {
|
}> = {}) {
|
||||||
return {
|
return {
|
||||||
source: "claude" as FormatId,
|
source: "claude" as FormatId,
|
||||||
@@ -195,6 +181,8 @@ function makeProps(overrides: Partial<{
|
|||||||
onSubmit: vi.fn(),
|
onSubmit: vi.fn(),
|
||||||
onOpenAdvanced: vi.fn(),
|
onOpenAdvanced: vi.fn(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
providerOptions: [{ value: "openai", label: "OpenAI" }, { value: "anthropic", label: "Anthropic" }],
|
||||||
|
loading: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ describe("TranslateTab", () => {
|
|||||||
expect(gridEl?.className).toContain("lg:grid-cols-2");
|
expect(gridEl?.className).toContain("lg:grid-cols-2");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the advanced section slot (data-advanced-section)", async () => {
|
it("does not expose data-advanced-section placeholder div (GAP-5)", async () => {
|
||||||
const { default: TranslateTab } = await import(
|
const { default: TranslateTab } = await import(
|
||||||
"@/app/(dashboard)/dashboard/translator/components/TranslateTab"
|
"@/app/(dashboard)/dashboard/translator/components/TranslateTab"
|
||||||
);
|
);
|
||||||
@@ -206,8 +206,8 @@ describe("TranslateTab", () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.render(<TranslateTab />);
|
root.render(<TranslateTab />);
|
||||||
});
|
});
|
||||||
const advancedSlot = container.querySelector("[data-advanced-section]");
|
// GAP-5: the data-advanced-section DOM data-leak placeholder must not exist
|
||||||
expect(advancedSlot).toBeTruthy();
|
expect(container.querySelector("[data-advanced-section]")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onAdvancedSlugChange with 'rawjson' when the Advanced button is clicked", async () => {
|
it("calls onAdvancedSlugChange with 'rawjson' when the Advanced button is clicked", async () => {
|
||||||
@@ -233,37 +233,6 @@ describe("TranslateTab", () => {
|
|||||||
expect(onAdvancedSlugChange).toHaveBeenCalledWith("rawjson");
|
expect(onAdvancedSlugChange).toHaveBeenCalledWith("rawjson");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reflects forceOpenAdvancedSlug in data attribute", async () => {
|
|
||||||
const { default: TranslateTab } = await import(
|
|
||||||
"@/app/(dashboard)/dashboard/translator/components/TranslateTab"
|
|
||||||
);
|
|
||||||
const container = makeContainer();
|
|
||||||
const root = createRoot(container);
|
|
||||||
const slug: AdvancedSlug = "pipeline";
|
|
||||||
await act(async () => {
|
|
||||||
root.render(
|
|
||||||
<TranslateTab forceOpenAdvancedSlug={slug} onAdvancedSlugChange={vi.fn()} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const advancedSlot = container.querySelector("[data-advanced-section]");
|
|
||||||
expect(advancedSlot?.getAttribute("data-force-open-slug")).toBe("pipeline");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("forceOpenAdvancedSlug=null results in empty data attribute", async () => {
|
|
||||||
const { default: TranslateTab } = await import(
|
|
||||||
"@/app/(dashboard)/dashboard/translator/components/TranslateTab"
|
|
||||||
);
|
|
||||||
const container = makeContainer();
|
|
||||||
const root = createRoot(container);
|
|
||||||
await act(async () => {
|
|
||||||
root.render(
|
|
||||||
<TranslateTab forceOpenAdvancedSlug={null} onAdvancedSlugChange={vi.fn()} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const advancedSlot = container.querySelector("[data-advanced-section]");
|
|
||||||
expect(advancedSlot?.getAttribute("data-force-open-slug")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders without onAdvancedSlugChange prop (optional)", async () => {
|
it("renders without onAdvancedSlugChange prop (optional)", async () => {
|
||||||
const { default: TranslateTab } = await import(
|
const { default: TranslateTab } = await import(
|
||||||
"@/app/(dashboard)/dashboard/translator/components/TranslateTab"
|
"@/app/(dashboard)/dashboard/translator/components/TranslateTab"
|
||||||
|
|||||||
Reference in New Issue
Block a user