mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: separate schema preview logic
This commit is contained in:
+3
-3
@@ -51,13 +51,13 @@
|
|||||||
только если темы нет в существующем документе; иначе обновляется источник
|
только если темы нет в существующем документе; иначе обновляется источник
|
||||||
истины.
|
истины.
|
||||||
|
|
||||||
## Текущий baseline Q6f.1
|
## Текущий baseline Q6f.2
|
||||||
|
|
||||||
На момент фиксации Q6f.1:
|
На момент фиксации Q6f.2:
|
||||||
|
|
||||||
- production registry содержит 122 инструмента;
|
- production registry содержит 122 инструмента;
|
||||||
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
- `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 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;
|
||||||
|
|||||||
+4
-2
@@ -107,8 +107,8 @@
|
|||||||
|
|
||||||
## Качество production UI
|
## Качество production UI
|
||||||
|
|
||||||
Q5–Q6f.1 закрывают известные preview-проблемы, special fixtures, smoke и
|
Q5–Q6f.2 закрывают известные preview-проблемы, special fixtures, smoke и
|
||||||
layout-инварианты; активных `test.fixme` в e2e больше нет.
|
layout/view-model инварианты; активных `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 через общий
|
||||||
@@ -124,6 +124,8 @@ layout-инварианты; активных `test.fixme` в e2e больше
|
|||||||
`text-and-verdicts.spec.ts`.
|
`text-and-verdicts.spec.ts`.
|
||||||
- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта
|
- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта
|
||||||
unit-тестами; `SchemaFields.svelte` использует общий seam.
|
unit-тестами; `SchemaFields.svelte` использует общий seam.
|
||||||
|
- [x] `SchemaPreview` view-model вынесена в `buildSchemaPreviewModel` и покрыта
|
||||||
|
unit-тестами для всех input/result-типов.
|
||||||
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
|
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
|
||||||
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
||||||
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
||||||
|
|||||||
@@ -96,10 +96,10 @@
|
|||||||
корректный (не «кракозябры»).
|
корректный (не «кракозябры»).
|
||||||
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
- [ ] 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-проблемы закрыты автоматическими тестами; для ручной
|
> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2-проблемы закрыты автоматическими тестами; для
|
||||||
> проверки остаются субъективные сценарии разделов A–F.
|
> ручной проверки остаются субъективные сценарии разделов A–F.
|
||||||
|
|
||||||
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
||||||
кнопки Generate. `web/e2e/generators.spec.ts`.
|
кнопки Generate. `web/e2e/generators.spec.ts`.
|
||||||
@@ -122,6 +122,8 @@
|
|||||||
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
|
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
|
||||||
- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI
|
- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI
|
||||||
группировка полей использует тот же seam.
|
группировка полей использует тот же seam.
|
||||||
|
- [x] **Preview model:** `buildSchemaPreviewModel` покрывает source/result
|
||||||
|
values, формат и `hasResult` для image/text/verdict/files.
|
||||||
|
|
||||||
## Как долго
|
## Как долго
|
||||||
|
|
||||||
|
|||||||
@@ -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-тестов и
|
> Источник: `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.
|
отдельным решением Q6f.2.
|
||||||
- Новые зависимости, vitest-конфиг и browser matrix не добавлялись.
|
- Новые зависимости, vitest-конфиг и browser matrix не добавлялись.
|
||||||
|
|
||||||
#### Q6f.2–Q6f.4. Оставшиеся направления
|
#### Q6f.2. SchemaPreview model seam
|
||||||
|
|
||||||
- выбрать чистый seam для `SchemaPreview`/view-model или DOM-окружение для
|
**Статус:** выполнен. Вычисления `sourceValue`, `resultValue`, `formatValue` и
|
||||||
`SchemaToolView`/`SchemaPreview`;
|
`hasResult` вынесены в `buildSchemaPreviewModel`; Svelte-компонент использует
|
||||||
- browser matrix, mobile viewport, visual snapshots и coverage thresholds.
|
модель, а 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- и
|
Не применять один одинаковый smoke-сценарий к image-, text-, generator- и
|
||||||
file-result-инструментам.
|
file-result-инструментам.
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ browser-контракт: critical CRC и truncated отклоняются, anci
|
|||||||
только для ограниченного ожидания hydration или асинхронного результата; он не
|
только для ограниченного ожидания hydration или асинхронного результата; он не
|
||||||
должен превращать проверку продукта в бесконечный retry.
|
должен превращать проверку продукта в бесконечный retry.
|
||||||
|
|
||||||
Чистая логика схемы и layout (defaults, sanitization, `resolveLayoutGroups`)
|
Чистая логика схемы, layout и preview-модели (defaults, sanitization,
|
||||||
проверяется в `src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в
|
`resolveLayoutGroups`, `buildSchemaPreviewModel`) проверяется в
|
||||||
Playwright. Component-тесты не дублируют e2e-контракт.
|
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
|
||||||
|
Component-тесты не дублируют e2e-контракт.
|
||||||
|
|
||||||
## 5. Helpers и `test.fixme`
|
## 5. Helpers и `test.fixme`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MetaList from "$lib/components/MetaList.svelte";
|
import MetaList from "$lib/components/MetaList.svelte";
|
||||||
|
import { buildSchemaPreviewModel } from "$lib/components/schema-preview-model";
|
||||||
import SchemaActions from "$lib/components/SchemaActions.svelte";
|
import SchemaActions from "$lib/components/SchemaActions.svelte";
|
||||||
import SchemaResultTile from "$lib/components/SchemaResultTile.svelte";
|
import SchemaResultTile from "$lib/components/SchemaResultTile.svelte";
|
||||||
import SchemaSourceTile from "$lib/components/SchemaSourceTile.svelte";
|
import SchemaSourceTile from "$lib/components/SchemaSourceTile.svelte";
|
||||||
@@ -46,36 +47,11 @@
|
|||||||
ondownload,
|
ondownload,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const sourceValue = $derived.by(() => {
|
const model = $derived(
|
||||||
switch (inputMode) {
|
buildSchemaPreviewModel(
|
||||||
case "none":
|
{ inputMode, resultKind, source, result, fileResult, textResult },
|
||||||
return "—";
|
t,
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -83,7 +59,7 @@
|
|||||||
<span class="label">{t("resultCard.previewPanel")}</span>
|
<span class="label">{t("resultCard.previewPanel")}</span>
|
||||||
<SchemaActions
|
<SchemaActions
|
||||||
{inputMode}
|
{inputMode}
|
||||||
canDownload={hasResult}
|
canDownload={model.hasResult}
|
||||||
{running}
|
{running}
|
||||||
{onupload}
|
{onupload}
|
||||||
{ondownload}
|
{ondownload}
|
||||||
@@ -119,9 +95,9 @@
|
|||||||
<div class="meta">
|
<div class="meta">
|
||||||
<MetaList
|
<MetaList
|
||||||
items={[
|
items={[
|
||||||
{ caption: t("sourceCard.source"), value: sourceValue },
|
{ caption: t("sourceCard.source"), value: model.sourceValue },
|
||||||
{ caption: t("resultCard.result"), value: resultValue },
|
{ caption: t("resultCard.result"), value: model.resultValue },
|
||||||
{ caption: t("resultCard.format"), value: formatValue },
|
{ caption: t("resultCard.format"), value: model.formatValue },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user