refactor: separate schema preview logic

This commit is contained in:
2026-09-26 09:25:00 +05:00
parent cdb23fe1c1
commit 6ab59897d1
8 changed files with 212 additions and 51 deletions
+10 -34
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import MetaList from "$lib/components/MetaList.svelte";
import { buildSchemaPreviewModel } from "$lib/components/schema-preview-model";
import SchemaActions from "$lib/components/SchemaActions.svelte";
import SchemaResultTile from "$lib/components/SchemaResultTile.svelte";
import SchemaSourceTile from "$lib/components/SchemaSourceTile.svelte";
@@ -46,36 +47,11 @@
ondownload,
}: Props = $props();
const sourceValue = $derived.by(() => {
switch (inputMode) {
case "none":
return "—";
case "text":
return t("textInput.heading");
case "image":
return source ? `${source.width} × ${source.height} px` : "—";
}
});
const resultValue = $derived(
resultKind === "image"
? result
? `${result.width} × ${result.height} px`
: "—"
: resultKind === "files"
? fileResult && fileResult.files.length > 0
? `${fileResult.files.length} ${t("resultCard.parts")}`
: "—"
: textResult
? t("textInput.heading")
: "—",
);
const formatValue = $derived(resultKind === "files" ? "ZIP (PNG)" : "PNG");
const hasResult = $derived(
resultKind === "image"
? Boolean(result)
: resultKind === "files"
? Boolean(fileResult && fileResult.files.length > 0)
: Boolean(textResult),
const model = $derived(
buildSchemaPreviewModel(
{ inputMode, resultKind, source, result, fileResult, textResult },
t,
),
);
</script>
@@ -83,7 +59,7 @@
<span class="label">{t("resultCard.previewPanel")}</span>
<SchemaActions
{inputMode}
canDownload={hasResult}
canDownload={model.hasResult}
{running}
{onupload}
{ondownload}
@@ -119,9 +95,9 @@
<div class="meta">
<MetaList
items={[
{ caption: t("sourceCard.source"), value: sourceValue },
{ caption: t("resultCard.result"), value: resultValue },
{ caption: t("resultCard.format"), value: formatValue },
{ caption: t("sourceCard.source"), value: model.sourceValue },
{ caption: t("resultCard.result"), value: model.resultValue },
{ caption: t("resultCard.format"), value: model.formatValue },
]}
/>
</div>
@@ -0,0 +1,110 @@
import { describe, expect, it } from "vitest";
import { makeImage } from "../core/test-helpers";
import {
buildSchemaPreviewModel,
type PreviewTranslate,
} from "./schema-preview-model";
const image = makeImage(2, 3, [
[1, 2, 3, 255],
[4, 5, 6, 255],
[7, 8, 9, 255],
[10, 11, 12, 255],
[13, 14, 15, 255],
[16, 17, 18, 255],
]);
const translate: PreviewTranslate = (key) => {
if (key === "resultCard.parts") return "parts";
if (key === "textInput.heading") return "Text";
return key;
};
function input(
overrides: Partial<Parameters<typeof buildSchemaPreviewModel>[0]> = {},
) {
return {
inputMode: "image" as const,
resultKind: "image" as const,
source: null,
result: null,
fileResult: null,
textResult: null,
...overrides,
};
}
describe("buildSchemaPreviewModel", () => {
it("formats source values for every input mode", () => {
expect(
buildSchemaPreviewModel(input({ inputMode: "none" }), translate),
).toMatchObject({ sourceValue: "—" });
expect(
buildSchemaPreviewModel(input({ inputMode: "text" }), translate),
).toMatchObject({ sourceValue: "Text" });
expect(
buildSchemaPreviewModel(
input({ inputMode: "image", source: image }),
translate,
),
).toMatchObject({ sourceValue: "2 × 3 px" });
});
it("formats image results and enables download only with a result", () => {
expect(
buildSchemaPreviewModel(input({ result: image }), translate),
).toEqual({
sourceValue: "—",
resultValue: "2 × 3 px",
formatValue: "PNG",
hasResult: true,
});
expect(buildSchemaPreviewModel(input(), translate)).toMatchObject({
resultValue: "—",
hasResult: false,
});
});
it("formats file results as ZIP and rejects empty file results", () => {
const fileResult = { files: [{ name: "part.png", image }] };
expect(
buildSchemaPreviewModel(
input({ resultKind: "files", fileResult }),
translate,
),
).toEqual({
sourceValue: "—",
resultValue: "1 parts",
formatValue: "ZIP (PNG)",
hasResult: true,
});
expect(
buildSchemaPreviewModel(
input({ resultKind: "files", fileResult: { files: [] } }),
translate,
),
).toMatchObject({ resultValue: "—", hasResult: false });
});
it.each(["text", "verdict"] as const)(
"uses text result presence for %s output",
(resultKind) => {
expect(
buildSchemaPreviewModel(
input({ resultKind, textResult: "done" }),
translate,
),
).toMatchObject({
resultValue: "Text",
formatValue: "PNG",
hasResult: true,
});
expect(
buildSchemaPreviewModel(input({ resultKind }), translate),
).toMatchObject({
resultValue: "—",
hasResult: false,
});
},
);
});
@@ -0,0 +1,60 @@
import type { PixelImage } from "../core/types";
import type { FileResult, InputMode, ResultKind } from "../registry";
export type PreviewTranslate = (key: string) => string;
export type SchemaPreviewModelInput = {
inputMode: InputMode;
resultKind: ResultKind;
source: PixelImage | null;
result: PixelImage | null;
fileResult: FileResult | null;
textResult: string | null;
};
export type SchemaPreviewModel = {
sourceValue: string;
resultValue: string;
formatValue: string;
hasResult: boolean;
};
export function buildSchemaPreviewModel(
input: SchemaPreviewModelInput,
translate: PreviewTranslate,
): SchemaPreviewModel {
const { inputMode, resultKind, source, result, fileResult, textResult } =
input;
const sourceValue =
inputMode === "none"
? "—"
: inputMode === "text"
? translate("textInput.heading")
: source
? `${source.width} × ${source.height} px`
: "—";
const hasFiles = Boolean(fileResult && fileResult.files.length > 0);
const resultValue =
resultKind === "image"
? result
? `${result.width} × ${result.height} px`
: "—"
: resultKind === "files"
? hasFiles
? `${fileResult?.files.length} ${translate("resultCard.parts")}`
: "—"
: textResult
? translate("textInput.heading")
: "—";
return {
sourceValue,
resultValue,
formatValue: resultKind === "files" ? "ZIP (PNG)" : "PNG",
hasResult:
resultKind === "image"
? Boolean(result)
: resultKind === "files"
? hasFiles
: Boolean(textResult),
};
}