From cdb23fe1c113f5bcc41228b9508e68a65a2746b6 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 26 Sep 2026 09:08:28 +0500 Subject: [PATCH] refactor: separate schema fields logic --- docs/README.md | 6 +- docs/backlog.md | 8 ++- docs/checklist-manual-testing.md | 6 +- docs/plan-codebase-quality.md | 22 +++++-- docs/testing-strategy.md | 4 ++ web/src/lib/components/SchemaFields.svelte | 34 +--------- web/src/lib/registry-schema.test.ts | 77 ++++++++++++++++++++++ web/src/lib/registry-schema.ts | 35 ++++++++++ 8 files changed, 148 insertions(+), 44 deletions(-) diff --git a/docs/README.md b/docs/README.md index cb3b3f5..7c27c30 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,13 +51,13 @@ только если темы нет в существующем документе; иначе обновляется источник истины. -## Текущий baseline Q6e +## Текущий baseline Q6f.1 -На момент фиксации Q6e: +На момент фиксации Q6f.1: - production registry содержит 122 инструмента; - `pnpm --dir web check` проходит без ошибок и предупреждений; -- `pnpm --dir web test` — 42 test files / 457 tests; +- `pnpm --dir web test` — 42 test files / 460 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 87db889..14d17cb 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -107,8 +107,8 @@ ## Качество production UI -Q5–Q6e закрывают известные preview-проблемы, special fixtures и registry-wide -smoke; активных `test.fixme` в e2e больше нет. +Q5–Q6f.1 закрывают известные preview-проблемы, special fixtures, smoke и +layout-инварианты; активных `test.fixme` в e2e больше нет. - [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`. - [x] `resize-png` и `crop-png` используют размеры текущего source через общий @@ -122,6 +122,10 @@ smoke; активных `test.fixme` в e2e больше нет. - [x] Registry-wide smoke покрывает все 122 инструмента по input/result-типу — `web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`, `text-and-verdicts.spec.ts`. +- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта + unit-тестами; `SchemaFields.svelte` использует общий seam. +- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного + решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение. - [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это продуктовая задача, не Q6d. diff --git a/docs/checklist-manual-testing.md b/docs/checklist-manual-testing.md index 5722bfa..87c14ec 100644 --- a/docs/checklist-manual-testing.md +++ b/docs/checklist-manual-testing.md @@ -96,9 +96,9 @@ корректный (не «кракозябры»). - [ ] remove-background на сложной полупрозрачности — края не «звенят». -## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e-проблемы +## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1-проблемы -> Q5/Q6b/Q6c/Q6d/Q6e-проблемы закрыты автоматическими тестами; для ручной +> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1-проблемы закрыты автоматическими тестами; для ручной > проверки остаются субъективные сценарии разделов A–F. - [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без @@ -120,6 +120,8 @@ - [x] **Registry smoke:** все 122 production tools имеют специализированный browser smoke по input/result-типу; `web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`, `text-and-verdicts.spec.ts`. +- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI + группировка полей использует тот же seam. ## Как долго diff --git a/docs/plan-codebase-quality.md b/docs/plan-codebase-quality.md index 263a040..07a329b 100644 --- a/docs/plan-codebase-quality.md +++ b/docs/plan-codebase-quality.md @@ -1,9 +1,9 @@ # План: линтеры, тесты и качество кодовой базы -> Статус: **draft — Q0–Q5 и Q6a–Q6e готовы к ревью; Q6f — следующий этап**. +> Статус: **draft — Q0–Q5 и Q6a–Q6f.1 готовы к ревью; Q6f.2 — следующий этап**. > > Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и -> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6e выполнены; Q6f–Q8 выполняются +> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6f.1 выполнены; Q6f.2–Q8 выполняются > отдельными атомарными шагами. > > Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность» @@ -275,10 +275,22 @@ image→image, image→text, text→image, generator input→image и verdict. F а текущий e2e-контракт уже покрывает поведение preview. Выбор test seam или DOM-окружения перенесён в Q6f. -#### Q6f. Component coverage, browser matrix и visual gates +#### Q6f.1. Layout resolver seam -- решить, нужен ли чистый seam для `SchemaFields`/view-model или полноценное - DOM-окружение для `SchemaToolView`/`SchemaPreview`; +**Статус:** выполнен. Чистая логика группировки полей вынесена из +`SchemaFields.svelte` в `resolveLayoutGroups` и покрыта unit-тестами на +дедупликацию, неизвестные поля, пустые группы, `cols` и default-хвост. + +- Компонент использует общий resolver, поэтому unit-тесты блокируют изменения + layout-логики без DOM/Testing Library. +- `SchemaPreview` view-model и DOM-компоненты не менялись; их seam остаётся + отдельным решением Q6f.2. +- Новые зависимости, vitest-конфиг и browser matrix не добавлялись. + +#### Q6f.2–Q6f.4. Оставшиеся направления + +- выбрать чистый seam для `SchemaPreview`/view-model или DOM-окружение для + `SchemaToolView`/`SchemaPreview`; - browser matrix, mobile viewport, visual snapshots и coverage thresholds. Не применять один одинаковый smoke-сценарий к image-, text-, generator- и diff --git a/docs/testing-strategy.md b/docs/testing-strategy.md index 0c9a3b0..b6f8f6f 100644 --- a/docs/testing-strategy.md +++ b/docs/testing-strategy.md @@ -59,6 +59,10 @@ browser-контракт: critical CRC и truncated отклоняются, anci только для ограниченного ожидания hydration или асинхронного результата; он не должен превращать проверку продукта в бесконечный retry. +Чистая логика схемы и layout (defaults, sanitization, `resolveLayoutGroups`) +проверяется в `src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в +Playwright. Component-тесты не дублируют e2e-контракт. + ## 5. Helpers и `test.fixme` `web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`, diff --git a/web/src/lib/components/SchemaFields.svelte b/web/src/lib/components/SchemaFields.svelte index 8a55259..362e88a 100644 --- a/web/src/lib/components/SchemaFields.svelte +++ b/web/src/lib/components/SchemaFields.svelte @@ -4,6 +4,7 @@ FieldSpecKind, ToolSchema, } from "$lib/registry-schema"; + import { resolveLayoutGroups } from "$lib/registry-schema"; import { fieldLabel, groupLabel } from "$lib/i18n/schema-tool-strings"; import { t } from "$lib/i18n/t"; import { RotateCcw } from "@lucide/svelte"; @@ -57,38 +58,7 @@ } let { schema, values, toolId, onchange, onreset }: Props = $props(); - interface LayoutGroup { - key: string; - title?: string; - cols: number; - fields: string[]; - } - - const layoutGroups = $derived.by((): LayoutGroup[] => { - const all = Object.keys(schema.fields); - const groups = schema.layout?.groups ?? []; - const used: Record = {}; - const named = groups - .map((g, gi) => { - const fields = g.fields.filter((f) => { - if (used[f] || !(f in schema.fields)) return false; - used[f] = true; - return true; - }); - return { - key: `${g.title ?? "group"}-${gi}`, - title: g.title, - cols: Math.max(1, Math.trunc(g.cols ?? 1)), - fields, - } satisfies LayoutGroup; - }) - .filter((g) => g.fields.length > 0); - const rest = all.filter((f) => !used[f]); - if (rest.length > 0) { - named.push({ key: "__default", title: undefined, cols: 1, fields: rest }); - } - return named; - }); + const layoutGroups = $derived(resolveLayoutGroups(schema)); {#each layoutGroups as group (group.key)} diff --git a/web/src/lib/registry-schema.test.ts b/web/src/lib/registry-schema.test.ts index b787e9f..8d88c88 100644 --- a/web/src/lib/registry-schema.test.ts +++ b/web/src/lib/registry-schema.test.ts @@ -3,6 +3,7 @@ import { applySourceDefaults, defaultSchemaParams, field, + resolveLayoutGroups, sanitizeSchemaParams, toolSchema, type Dimension, @@ -130,6 +131,82 @@ describe("registry-schema: раскладка (schema.layout)", () => { }); expect(plain.layout).toBeUndefined(); }); + + it("разрешает named-группы и хвост без layout", () => { + expect(resolveLayoutGroups(frameSchema)).toEqual([ + { + key: "Frame-0", + title: "Frame", + cols: 1, + fields: ["thickness", "color"], + }, + { key: "__default", cols: 1, fields: ["enabled", "count"] }, + ]); + }); + + it("убирает неизвестные и повторные поля, нормализует cols", () => { + const schema = toolSchema( + { + thickness: field.slider({ min: 1, max: 500, default: 5 }), + color: field.color({ default: "#000000" }), + enabled: field.checkbox({ default: true }), + count: field.select({ + default: "two", + options: [ + { value: "one", label: "One" }, + { value: "two", label: "Two" }, + ], + }), + }, + { + layout: { + groups: [ + { + title: "Duplicate", + cols: 0, + fields: ["thickness", "thickness", "missing"], + }, + { title: "Empty", fields: ["missing"] }, + { cols: 3, fields: ["color"] }, + ], + }, + }, + ); + + expect(resolveLayoutGroups(schema)).toEqual([ + { + key: "Duplicate-0", + title: "Duplicate", + cols: 1, + fields: ["thickness"], + }, + { key: "group-2", title: undefined, cols: 3, fields: ["color"] }, + { key: "__default", cols: 1, fields: ["enabled", "count"] }, + ]); + }); + + it("складывает все поля без layout в default-группу", () => { + const plain = toolSchema({ + thickness: field.slider({ min: 1, max: 500, default: 5 }), + color: field.color({ default: "#000000" }), + enabled: field.checkbox({ default: true }), + count: field.select({ + default: "two", + options: [ + { value: "one", label: "One" }, + { value: "two", label: "Two" }, + ], + }), + }); + + expect(resolveLayoutGroups(plain)).toEqual([ + { + key: "__default", + cols: 1, + fields: ["thickness", "color", "enabled", "count"], + }, + ]); + }); }); describe("registry-schema: санитайз", () => { diff --git a/web/src/lib/registry-schema.ts b/web/src/lib/registry-schema.ts index 35f269f..c92be45 100644 --- a/web/src/lib/registry-schema.ts +++ b/web/src/lib/registry-schema.ts @@ -341,6 +341,41 @@ export function toolSchema

( return { fields, ...(meta ?? {}) }; } +export interface ResolvedLayoutGroup { + key: string; + title?: string; + cols: number; + fields: string[]; +} + +export function resolveLayoutGroups

( + schema: ToolSchema

, +): ResolvedLayoutGroup[] { + const all = Object.keys(schema.fields); + const groups = schema.layout?.groups ?? []; + const used: Record = {}; + const named: ResolvedLayoutGroup[] = groups + .map((group, index) => { + const fields = group.fields.filter((fieldId) => { + if (used[fieldId] || !(fieldId in schema.fields)) return false; + used[fieldId] = true; + return true; + }); + return { + key: `${group.title ?? "group"}-${index}`, + title: group.title, + cols: Math.max(1, Math.trunc(group.cols ?? 1)), + fields, + } satisfies ResolvedLayoutGroup; + }) + .filter((group) => group.fields.length > 0); + const rest = all.filter((fieldId) => !used[fieldId]); + if (rest.length > 0) { + named.push({ key: "__default", cols: 1, fields: rest }); + } + return named; +} + function clamp(value: number, min?: number, max?: number): number { if (min !== undefined && value < min) return min; if (max !== undefined && value > max) return max;