From 6ab59897d10dd391c549a3c1e6d9df6fc7623288 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 26 Sep 2026 09:25:00 +0500 Subject: [PATCH] refactor: separate schema preview logic --- docs/README.md | 6 +- docs/backlog.md | 6 +- docs/checklist-manual-testing.md | 8 +- docs/plan-codebase-quality.md | 22 +++- docs/testing-strategy.md | 7 +- web/src/lib/components/SchemaPreview.svelte | 44 ++----- .../components/schema-preview-model.test.ts | 110 ++++++++++++++++++ .../lib/components/schema-preview-model.ts | 60 ++++++++++ 8 files changed, 212 insertions(+), 51 deletions(-) create mode 100644 web/src/lib/components/schema-preview-model.test.ts create mode 100644 web/src/lib/components/schema-preview-model.ts diff --git a/docs/README.md b/docs/README.md index 7c27c30..adf9b07 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,13 +51,13 @@ только если темы нет в существующем документе; иначе обновляется источник истины. -## Текущий baseline Q6f.1 +## Текущий baseline Q6f.2 -На момент фиксации Q6f.1: +На момент фиксации Q6f.2: - production registry содержит 122 инструмента; - `pnpm --dir web check` проходит без ошибок и предупреждений; -- `pnpm --dir web test` — 42 test files / 460 tests; +- `pnpm --dir web test` — 43 test files / 465 tests; - `pnpm --dir web test:rules` — 4 test files / 74 tests; - `pnpm --dir web lint:all` — 0 errors и 0 i18n warnings; остаются 4 unused design tokens от token audit; diff --git a/docs/backlog.md b/docs/backlog.md index 14d17cb..4c6b0f0 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -107,8 +107,8 @@ ## Качество production UI -Q5–Q6f.1 закрывают известные preview-проблемы, special fixtures, smoke и -layout-инварианты; активных `test.fixme` в e2e больше нет. +Q5–Q6f.2 закрывают известные preview-проблемы, special fixtures, smoke и +layout/view-model инварианты; активных `test.fixme` в e2e больше нет. - [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`. - [x] `resize-png` и `crop-png` используют размеры текущего source через общий @@ -124,6 +124,8 @@ layout-инварианты; активных `test.fixme` в e2e больше `text-and-verdicts.spec.ts`. - [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта unit-тестами; `SchemaFields.svelte` использует общий seam. +- [x] `SchemaPreview` view-model вынесена в `buildSchemaPreviewModel` и покрыта + unit-тестами для всех input/result-типов. - [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение. - [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается diff --git a/docs/checklist-manual-testing.md b/docs/checklist-manual-testing.md index 87c14ec..f5cb703 100644 --- a/docs/checklist-manual-testing.md +++ b/docs/checklist-manual-testing.md @@ -96,10 +96,10 @@ корректный (не «кракозябры»). - [ ] remove-background на сложной полупрозрачности — края не «звенят». -## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1-проблемы +## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2-проблемы -> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1-проблемы закрыты автоматическими тестами; для ручной -> проверки остаются субъективные сценарии разделов A–F. +> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2-проблемы закрыты автоматическими тестами; для +> ручной проверки остаются субъективные сценарии разделов A–F. - [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без кнопки Generate. `web/e2e/generators.spec.ts`. @@ -122,6 +122,8 @@ `generators.spec.ts`, `text-and-verdicts.spec.ts`. - [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI группировка полей использует тот же seam. +- [x] **Preview model:** `buildSchemaPreviewModel` покрывает source/result + values, формат и `hasResult` для image/text/verdict/files. ## Как долго diff --git a/docs/plan-codebase-quality.md b/docs/plan-codebase-quality.md index 07a329b..6550820 100644 --- a/docs/plan-codebase-quality.md +++ b/docs/plan-codebase-quality.md @@ -1,9 +1,9 @@ # План: линтеры, тесты и качество кодовой базы -> Статус: **draft — Q0–Q5 и Q6a–Q6f.1 готовы к ревью; Q6f.2 — следующий этап**. +> Статус: **draft — Q0–Q5 и Q6a–Q6f.2 готовы к ревью; Q6f.3 — следующий этап**. > > Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и -> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6f.1 выполнены; Q6f.2–Q8 выполняются +> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6f.2 выполнены; Q6f.3–Q8 выполняются > отдельными атомарными шагами. > > Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность» @@ -287,11 +287,21 @@ image→image, image→text, text→image, generator input→image и verdict. F отдельным решением Q6f.2. - Новые зависимости, vitest-конфиг и browser matrix не добавлялись. -#### Q6f.2–Q6f.4. Оставшиеся направления +#### Q6f.2. SchemaPreview model seam -- выбрать чистый seam для `SchemaPreview`/view-model или DOM-окружение для - `SchemaToolView`/`SchemaPreview`; -- browser matrix, mobile viewport, visual snapshots и coverage thresholds. +**Статус:** выполнен. Вычисления `sourceValue`, `resultValue`, `formatValue` и +`hasResult` вынесены в `buildSchemaPreviewModel`; Svelte-компонент использует +модель, а unit-тесты проверяют все input/result-ветки без DOM. + +- Translator передаётся в модель явно, поэтому unit-тесты не зависят от locale + state и проверяют только view-model. +- image/text/verdict/files и пустые результаты покрыты в `pnpm verify`. +- Новые зависимости, vitest-конфиг и browser matrix не добавлялись. + +#### Q6f.3–Q6f.4. Оставшиеся направления + +- browser matrix, mobile viewport, visual snapshots и coverage thresholds; +- при необходимости отдельное решение по DOM-тестам `SchemaToolView`/controls. Не применять один одинаковый smoke-сценарий к image-, text-, generator- и file-result-инструментам. diff --git a/docs/testing-strategy.md b/docs/testing-strategy.md index b6f8f6f..a0909b3 100644 --- a/docs/testing-strategy.md +++ b/docs/testing-strategy.md @@ -59,9 +59,10 @@ browser-контракт: critical CRC и truncated отклоняются, anci только для ограниченного ожидания hydration или асинхронного результата; он не должен превращать проверку продукта в бесконечный retry. -Чистая логика схемы и layout (defaults, sanitization, `resolveLayoutGroups`) -проверяется в `src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в -Playwright. Component-тесты не дублируют e2e-контракт. +Чистая логика схемы, layout и preview-модели (defaults, sanitization, +`resolveLayoutGroups`, `buildSchemaPreviewModel`) проверяется в +`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright. +Component-тесты не дублируют e2e-контракт. ## 5. Helpers и `test.fixme` diff --git a/web/src/lib/components/SchemaPreview.svelte b/web/src/lib/components/SchemaPreview.svelte index 0addba8..9e57199 100644 --- a/web/src/lib/components/SchemaPreview.svelte +++ b/web/src/lib/components/SchemaPreview.svelte @@ -1,5 +1,6 @@ @@ -83,7 +59,7 @@ {t("resultCard.previewPanel")} diff --git a/web/src/lib/components/schema-preview-model.test.ts b/web/src/lib/components/schema-preview-model.test.ts new file mode 100644 index 0000000..e5f2962 --- /dev/null +++ b/web/src/lib/components/schema-preview-model.test.ts @@ -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[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, + }); + }, + ); +}); diff --git a/web/src/lib/components/schema-preview-model.ts b/web/src/lib/components/schema-preview-model.ts new file mode 100644 index 0000000..65eb4b0 --- /dev/null +++ b/web/src/lib/components/schema-preview-model.ts @@ -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), + }; +}