mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: separate schema fields logic
This commit is contained in:
+3
-3
@@ -51,13 +51,13 @@
|
|||||||
только если темы нет в существующем документе; иначе обновляется источник
|
только если темы нет в существующем документе; иначе обновляется источник
|
||||||
истины.
|
истины.
|
||||||
|
|
||||||
## Текущий baseline Q6e
|
## Текущий baseline Q6f.1
|
||||||
|
|
||||||
На момент фиксации Q6e:
|
На момент фиксации Q6f.1:
|
||||||
|
|
||||||
- production registry содержит 122 инструмента;
|
- production registry содержит 122 инструмента;
|
||||||
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
- `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 test:rules` — 4 test files / 74 tests;
|
||||||
- `pnpm --dir web lint:all` — 0 errors и 0 i18n warnings; остаются 4 unused
|
- `pnpm --dir web lint:all` — 0 errors и 0 i18n warnings; остаются 4 unused
|
||||||
design tokens от token audit;
|
design tokens от token audit;
|
||||||
|
|||||||
+6
-2
@@ -107,8 +107,8 @@
|
|||||||
|
|
||||||
## Качество production UI
|
## Качество production UI
|
||||||
|
|
||||||
Q5–Q6e закрывают известные preview-проблемы, special fixtures и registry-wide
|
Q5–Q6f.1 закрывают известные preview-проблемы, special fixtures, smoke и
|
||||||
smoke; активных `test.fixme` в e2e больше нет.
|
layout-инварианты; активных `test.fixme` в e2e больше нет.
|
||||||
|
|
||||||
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
|
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
|
||||||
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
|
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
|
||||||
@@ -122,6 +122,10 @@ smoke; активных `test.fixme` в e2e больше нет.
|
|||||||
- [x] Registry-wide smoke покрывает все 122 инструмента по input/result-типу —
|
- [x] Registry-wide smoke покрывает все 122 инструмента по input/result-типу —
|
||||||
`web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`,
|
`web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`,
|
||||||
`text-and-verdicts.spec.ts`.
|
`text-and-verdicts.spec.ts`.
|
||||||
|
- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта
|
||||||
|
unit-тестами; `SchemaFields.svelte` использует общий seam.
|
||||||
|
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
|
||||||
|
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
||||||
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
||||||
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это
|
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это
|
||||||
продуктовая задача, не Q6d.
|
продуктовая задача, не Q6d.
|
||||||
|
|||||||
@@ -96,9 +96,9 @@
|
|||||||
корректный (не «кракозябры»).
|
корректный (не «кракозябры»).
|
||||||
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
- [ ] 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.
|
> проверки остаются субъективные сценарии разделов A–F.
|
||||||
|
|
||||||
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
||||||
@@ -120,6 +120,8 @@
|
|||||||
- [x] **Registry smoke:** все 122 production tools имеют специализированный
|
- [x] **Registry smoke:** все 122 production tools имеют специализированный
|
||||||
browser smoke по input/result-типу; `web/e2e/tools-smoke.spec.ts`,
|
browser smoke по input/result-типу; `web/e2e/tools-smoke.spec.ts`,
|
||||||
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
|
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
|
||||||
|
- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI
|
||||||
|
группировка полей использует тот же seam.
|
||||||
|
|
||||||
## Как долго
|
## Как долго
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# План: линтеры, тесты и качество кодовой базы
|
# План: линтеры, тесты и качество кодовой базы
|
||||||
|
|
||||||
> Статус: **draft — Q0–Q5 и Q6a–Q6e готовы к ревью; Q6f — следующий этап**.
|
> Статус: **draft — Q0–Q5 и Q6a–Q6f.1 готовы к ревью; Q6f.2 — следующий этап**.
|
||||||
>
|
>
|
||||||
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
> Источник: `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 или
|
а текущий e2e-контракт уже покрывает поведение preview. Выбор test seam или
|
||||||
DOM-окружения перенесён в Q6f.
|
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.
|
- browser matrix, mobile viewport, visual snapshots и coverage thresholds.
|
||||||
|
|
||||||
Не применять один одинаковый smoke-сценарий к image-, text-, generator- и
|
Не применять один одинаковый smoke-сценарий к image-, text-, generator- и
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ browser-контракт: critical CRC и truncated отклоняются, anci
|
|||||||
только для ограниченного ожидания hydration или асинхронного результата; он не
|
только для ограниченного ожидания hydration или асинхронного результата; он не
|
||||||
должен превращать проверку продукта в бесконечный retry.
|
должен превращать проверку продукта в бесконечный retry.
|
||||||
|
|
||||||
|
Чистая логика схемы и layout (defaults, sanitization, `resolveLayoutGroups`)
|
||||||
|
проверяется в `src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в
|
||||||
|
Playwright. Component-тесты не дублируют e2e-контракт.
|
||||||
|
|
||||||
## 5. Helpers и `test.fixme`
|
## 5. Helpers и `test.fixme`
|
||||||
|
|
||||||
`web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`,
|
`web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
FieldSpecKind,
|
FieldSpecKind,
|
||||||
ToolSchema,
|
ToolSchema,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
|
import { resolveLayoutGroups } from "$lib/registry-schema";
|
||||||
import { fieldLabel, groupLabel } from "$lib/i18n/schema-tool-strings";
|
import { fieldLabel, groupLabel } from "$lib/i18n/schema-tool-strings";
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
import { RotateCcw } from "@lucide/svelte";
|
import { RotateCcw } from "@lucide/svelte";
|
||||||
@@ -57,38 +58,7 @@
|
|||||||
}
|
}
|
||||||
let { schema, values, toolId, onchange, onreset }: Props = $props();
|
let { schema, values, toolId, onchange, onreset }: Props = $props();
|
||||||
|
|
||||||
interface LayoutGroup {
|
const layoutGroups = $derived(resolveLayoutGroups(schema));
|
||||||
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<string, true> = {};
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each layoutGroups as group (group.key)}
|
{#each layoutGroups as group (group.key)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applySourceDefaults,
|
applySourceDefaults,
|
||||||
defaultSchemaParams,
|
defaultSchemaParams,
|
||||||
field,
|
field,
|
||||||
|
resolveLayoutGroups,
|
||||||
sanitizeSchemaParams,
|
sanitizeSchemaParams,
|
||||||
toolSchema,
|
toolSchema,
|
||||||
type Dimension,
|
type Dimension,
|
||||||
@@ -130,6 +131,82 @@ describe("registry-schema: раскладка (schema.layout)", () => {
|
|||||||
});
|
});
|
||||||
expect(plain.layout).toBeUndefined();
|
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<FrameParams>(
|
||||||
|
{
|
||||||
|
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<FrameParams>({
|
||||||
|
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: санитайз", () => {
|
describe("registry-schema: санитайз", () => {
|
||||||
|
|||||||
@@ -341,6 +341,41 @@ export function toolSchema<P>(
|
|||||||
return { fields, ...(meta ?? {}) };
|
return { fields, ...(meta ?? {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResolvedLayoutGroup {
|
||||||
|
key: string;
|
||||||
|
title?: string;
|
||||||
|
cols: number;
|
||||||
|
fields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLayoutGroups<P>(
|
||||||
|
schema: ToolSchema<P>,
|
||||||
|
): ResolvedLayoutGroup[] {
|
||||||
|
const all = Object.keys(schema.fields);
|
||||||
|
const groups = schema.layout?.groups ?? [];
|
||||||
|
const used: Record<string, true> = {};
|
||||||
|
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 {
|
function clamp(value: number, min?: number, max?: number): number {
|
||||||
if (min !== undefined && value < min) return min;
|
if (min !== undefined && value < min) return min;
|
||||||
if (max !== undefined && value > max) return max;
|
if (max !== undefined && value > max) return max;
|
||||||
|
|||||||
Reference in New Issue
Block a user