From d14e14ac1aa2577459dd25d4dad3acc05def1ebc Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 12 Sep 2026 15:09:32 +0500 Subject: [PATCH] test: add new split tool tests --- docs/plan-split-into-parts.md | 7 +- web/src/lib/core/geometry.test.ts | 114 +++++++++++++++++++++++++- web/src/lib/registry/registry.test.ts | 86 ++++++++++++++++++- 3 files changed, 201 insertions(+), 6 deletions(-) diff --git a/docs/plan-split-into-parts.md b/docs/plan-split-into-parts.md index d11f34d..b16595d 100644 --- a/docs/plan-split-into-parts.md +++ b/docs/plan-split-into-parts.md @@ -32,8 +32,8 @@ ZIP-архивом. `zipSync`. 5. **`core/errors.ts`:** ключ `errors.tooManyParts` (страховочный лимит cols·rows ≤ 1000; при максимуме 6×6 недостижим). -6. **`registry/geometry.ts`:** схема (`columns`, `rows`, дефолт 2×2, min 1, - max 6 — максимум 36 частей) + entry `split-into-parts-png` в `geometryEntries`. +6. **`registry/geometry.ts`:** схема (`columns`, `rows`, дефолт 2×2, min 1, max + 6 — максимум 36 частей) + entry `split-into-parts-png` в `geometryEntries`. 7. **Executor:** сериализация/десериализация `FileResult` в `executor.worker.ts` и тип ветки в `executor.ts`. 8. **UI:** `SchemaToolView.svelte` (`fileResult`, Download → zip), @@ -66,8 +66,7 @@ ZIP-архивом. ## Нейминг файлов в ZIP -`part-01-01.png` … `part--.png` (row, col нумеруются с 1, ведущий ноль -для выравнивания по числу строк/столбцов). +`part--.png` (row, col нумеруются с 1, при 2×2 — `part-1-1.png`). ## Верификация diff --git a/web/src/lib/core/geometry.test.ts b/web/src/lib/core/geometry.test.ts index 47c172a..aa11321 100644 --- a/web/src/lib/core/geometry.test.ts +++ b/web/src/lib/core/geometry.test.ts @@ -6,9 +6,11 @@ import { flip, resize, rotate90, + splitToParts, tile, } from "./geometry"; -import { makeImage } from "./test-helpers"; +import { expectImageEqual, makeImage } from "./test-helpers"; +import type { PixelImage } from "./types"; const square = () => makeImage(2, 2, [ @@ -210,3 +212,113 @@ describe("resize", () => { expect(() => resize(img, 10.5, 10)).toThrow(/errors\.sizeInt/); }); }); + +describe("splitToParts", () => { + function grid(w: number, h: number): PixelImage { + const pixels: number[][] = []; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const v = y * w + x + 1; + pixels.push([v, v, v, 255]); + } + } + return makeImage(w, h, pixels); + } + + it("ровное деление 4x4 на 2x2 даёт четыре части 2x2 в row-major порядке", () => { + const parts = splitToParts(grid(4, 4), 2, 2); + expect(parts).toHaveLength(4); + for (const part of parts) { + expect(part.width).toBe(2); + expect(part.height).toBe(2); + } + expectImageEqual(parts[0], [ + [1, 1, 1, 255], + [2, 2, 2, 255], + [5, 5, 5, 255], + [6, 6, 6, 255], + ]); + expectImageEqual(parts[1], [ + [3, 3, 3, 255], + [4, 4, 4, 255], + [7, 7, 7, 255], + [8, 8, 8, 255], + ]); + expectImageEqual(parts[2], [ + [9, 9, 9, 255], + [10, 10, 10, 255], + [13, 13, 13, 255], + [14, 14, 14, 255], + ]); + expectImageEqual(parts[3], [ + [11, 11, 11, 255], + [12, 12, 12, 255], + [15, 15, 15, 255], + [16, 16, 16, 255], + ]); + }); + + it("неделимый размер: канвас дополняется прозрачным, части одинаковые", () => { + const parts = splitToParts(grid(5, 3), 2, 2); + expect(parts).toHaveLength(4); + for (const part of parts) { + expect(part.width).toBe(3); + expect(part.height).toBe(2); + } + expectImageEqual(parts[0], [ + [1, 1, 1, 255], + [2, 2, 2, 255], + [3, 3, 3, 255], + [6, 6, 6, 255], + [7, 7, 7, 255], + [8, 8, 8, 255], + ]); + expectImageEqual(parts[1], [ + [4, 4, 4, 255], + [5, 5, 5, 255], + [0, 0, 0, 0], + [9, 9, 9, 255], + [10, 10, 10, 255], + [0, 0, 0, 0], + ]); + expectImageEqual(parts[2], [ + [11, 11, 11, 255], + [12, 12, 12, 255], + [13, 13, 13, 255], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]); + expectImageEqual(parts[3], [ + [14, 14, 14, 255], + [15, 15, 15, 255], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]); + }); + + it("одна колонка или строка — полосы без паддинга", () => { + const parts = splitToParts(grid(3, 4), 1, 2); + expect(parts).toHaveLength(2); + expect(parts[0].width).toBe(3); + expect(parts[0].height).toBe(2); + expect(parts[1].height).toBe(2); + }); + + it("части в row-major порядке: последний кусок содержит правый нижний пиксель", () => { + const img = grid(5, 3); + const parts = splitToParts(img, 2, 2); + expect(parts[parts.length - 1].data[0]).toBe(14); + expect(parts[parts.length - 1].data[4]).toBe(15); + expect(parts[parts.length - 1].data[8]).toBe(0); + }); + + it("дробные и нулевые значения колонок/строк нормализуются", () => { + const parts = splitToParts(grid(6, 6), 2.9, 0); + expect(parts).toHaveLength(2); + expect(parts[0].width).toBe(3); + expect(parts[0].height).toBe(6); + }); +}); diff --git a/web/src/lib/registry/registry.test.ts b/web/src/lib/registry/registry.test.ts index 3d68401..ebc1252 100644 --- a/web/src/lib/registry/registry.test.ts +++ b/web/src/lib/registry/registry.test.ts @@ -3,7 +3,7 @@ import { TOOLS } from "."; import { PREVIEW_GROUPS } from "../catalog"; import type { PixelImage } from "../core/types"; import { defaultSchemaParams, sanitizeSchemaParams } from "../registry-schema"; -import type { ToolResult } from "./types"; +import type { FileResult, ToolResult } from "./types"; function asImage(result: ToolResult): PixelImage { if (typeof result === "string" || "files" in result) { @@ -517,6 +517,90 @@ describe("registry-new (переведённые инструменты)", () => }); }); +describe("split-into-parts-png", () => { + it("объявлен с result=files и дефолтом 2x2", () => { + const tool = TOOLS.find((t) => t.id === "split-into-parts-png")!; + expect(tool).toBeDefined(); + expect(tool.result).toBe("files"); + const defaults = sanitizeSchemaParams(tool.schema, {}); + expect(defaults.columns).toBe(2); + expect(defaults.rows).toBe(2); + }); + + it("на ровном размере даёт cols*rows частей одинакового размера", async () => { + const tool = TOOLS.find((t) => t.id === "split-into-parts-png")!; + const files = asFiles( + await tool.run({ + source: solid(100, 100), + params: sanitizeSchemaParams(tool.schema, { + columns: 2, + rows: 2, + }), + }), + ).files; + expect(files).toHaveLength(4); + for (const file of files) { + expect(file.image.width).toBe(50); + expect(file.image.height).toBe(50); + } + expect(files.map((f) => f.name)).toEqual([ + "part-1-1.png", + "part-1-2.png", + "part-2-1.png", + "part-2-2.png", + ]); + }); + + it("на неделимом размере части строго равные (канвас дополняется)", async () => { + const tool = TOOLS.find((t) => t.id === "split-into-parts-png")!; + const files = asFiles( + await tool.run({ + source: solid(101, 77), + params: sanitizeSchemaParams(tool.schema, { + columns: 3, + rows: 2, + }), + }), + ).files; + expect(files).toHaveLength(6); + for (const file of files) { + expect(file.image.width).toBe(34); + expect(file.image.height).toBe(39); + } + }); + + it("работает максимум 6x6 = 36 частей", async () => { + const tool = TOOLS.find((t) => t.id === "split-into-parts-png")!; + const files = asFiles( + await tool.run({ + source: solid(120, 120), + params: sanitizeSchemaParams(tool.schema, { + columns: 6, + rows: 6, + }), + }), + ).files; + expect(files).toHaveLength(36); + }); + + it("выбрасывает errors.tooManyParts при переполнении", () => { + const tool = TOOLS.find((t) => t.id === "split-into-parts-png")!; + expect(() => + tool.run({ + source: solid(120, 120), + params: { columns: 1000, rows: 1000 }, + }), + ).toThrow("errors.tooManyParts"); + }); +}); + +function asFiles(result: ToolResult): FileResult { + if (typeof result === "string" || !("files" in result)) { + throw new Error("expected a files result"); + } + return result; +} + function solid(width: number, height: number) { const data = new Uint8ClampedArray(width * height * 4).fill(255); return { width, height, data };