mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
test: add download mime, quality verification
This commit is contained in:
+4
-4
@@ -51,17 +51,17 @@
|
|||||||
только если темы нет в существующем документе; иначе обновляется источник
|
только если темы нет в существующем документе; иначе обновляется источник
|
||||||
истины.
|
истины.
|
||||||
|
|
||||||
## Текущий baseline Q6b
|
## Текущий baseline Q6c
|
||||||
|
|
||||||
На момент фиксации Q6b:
|
На момент фиксации Q6c:
|
||||||
|
|
||||||
- production registry содержит 122 инструмента;
|
- production registry содержит 122 инструмента;
|
||||||
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
||||||
- `pnpm --dir web test` — 42 test files / 449 tests;
|
- `pnpm --dir web test` — 42 test files / 455 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;
|
||||||
- `pnpm verify` проходит; production E2E — 110 passed, 0 skipped.
|
- `pnpm verify` проходит; production E2E — 112 passed, 0 skipped.
|
||||||
|
|
||||||
Baseline и следующий шаг поддерживаются в `docs/plan-codebase-quality.md`.
|
Baseline и следующий шаг поддерживаются в `docs/plan-codebase-quality.md`.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -154,7 +154,9 @@ Q5 закрывает известные preview-проблемы; активн
|
|||||||
механическая часть реализована в preview — `ToolEntry.output`
|
механическая часть реализована в preview — `ToolEntry.output`
|
||||||
(`OutputFormat`: mime/ext/qualityParamId) в `web/src/lib/registry/types.ts`;
|
(`OutputFormat`: mime/ext/qualityParamId) в `web/src/lib/registry/types.ts`;
|
||||||
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
|
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
|
||||||
`png-to-bmp` переведён сюда же). **Осталось (UX-райз «Download»):**
|
`png-to-bmp` переведён сюда же). Q6c покрывает этот фиксированный контракт
|
||||||
|
unit/E2E-проверками; переключатель форматов и quality остаётся отдельным
|
||||||
|
UX-райзом. **Осталось (UX-райз «Download»):**
|
||||||
- выбор формата/качества прямо в кнопке/диалоге Download для любого
|
- выбор формата/качества прямо в кнопке/диалоге Download для любого
|
||||||
результата (не отдельными конвертерами);
|
результата (не отдельными конвертерами);
|
||||||
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
|
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
|
||||||
|
|||||||
@@ -95,10 +95,10 @@
|
|||||||
корректный (не «кракозябры»).
|
корректный (не «кракозябры»).
|
||||||
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
||||||
|
|
||||||
## G. Закрытые Q5/Q6b-проблемы
|
## G. Закрытые Q5/Q6b/Q6c-проблемы
|
||||||
|
|
||||||
> Q5/Q6b-проблемы закрыты автоматическими тестами; для ручной проверки остаются
|
> Q5/Q6b/Q6c-проблемы закрыты автоматическими тестами; для ручной проверки
|
||||||
> субъективные сценарии разделов A–F.
|
> остаются субъективные сценарии разделов A–F.
|
||||||
|
|
||||||
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
||||||
кнопки Generate. `web/e2e/generators.spec.ts`.
|
кнопки Generate. `web/e2e/generators.spec.ts`.
|
||||||
@@ -112,6 +112,8 @@
|
|||||||
`split-into-parts-png.zip` с именованными PNG-частями; содержимое и
|
`split-into-parts-png.zip` с именованными PNG-частями; содержимое и
|
||||||
сигнатуры проверяются `web/src/lib/zip.test.ts` и
|
сигнатуры проверяются `web/src/lib/zip.test.ts` и
|
||||||
`web/e2e/pipeline.spec.ts`.
|
`web/e2e/pipeline.spec.ts`.
|
||||||
|
- [x] **Output MIME/quality:** JPG/WebP/BMP имеют ожидаемые расширения и
|
||||||
|
сигнатуры, quality 10/90 меняет размер файла; `web/e2e/pipeline.spec.ts`.
|
||||||
|
|
||||||
## Как долго
|
## Как долго
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# План: линтеры, тесты и качество кодовой базы
|
# План: линтеры, тесты и качество кодовой базы
|
||||||
|
|
||||||
> Статус: **draft — Q0–Q5, Q6a и Q6b готовы к ревью; Q6c — следующий этап**.
|
> Статус: **draft — Q0–Q5 и Q6a–Q6c готовы к ревью; Q6d — следующий этап**.
|
||||||
>
|
>
|
||||||
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
||||||
> инфраструктурных скриптов. Q0–Q5, Q6a и Q6b выполнены; Q6c–Q8 выполняются
|
> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6c выполнены; Q6d–Q8 выполняются
|
||||||
> отдельными атомарными шагами.
|
> отдельными атомарными шагами.
|
||||||
>
|
>
|
||||||
> Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность»
|
> Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность»
|
||||||
@@ -229,9 +229,22 @@ seam, unit round-trip для `FileResult`, защиту пустого резу
|
|||||||
- E2E использует `downloadResultFile` и `unzipSync`, не добавляя новые
|
- E2E использует `downloadResultFile` и `unzipSync`, не добавляя новые
|
||||||
`data-testid`; files-инструменты не проходят через image-smoke.
|
`data-testid`; files-инструменты не проходят через image-smoke.
|
||||||
|
|
||||||
#### Q6c–Q6f. Оставшиеся направления
|
#### Q6c. Output MIME/quality
|
||||||
|
|
||||||
|
**Статус:** выполнен. Q6c покрывает текущий фиксированный output-контракт
|
||||||
|
JPG/WebP/BMP, не превращаясь в UX-райз кнопки Download. Registry проверяет
|
||||||
|
согласованность MIME/extension/quality-поля, unit — границы quality, browser —
|
||||||
|
реальные сигнатуры файлов и разницу размера при изменении quality.
|
||||||
|
|
||||||
|
- `ToolEntry.output` остаётся декларативным описанием формата; новый селектор
|
||||||
|
форматов не добавляется.
|
||||||
|
- `validateOutputQuality` отделяет проверку 0..1 от DOM-рендеринга; JPEG/WebP
|
||||||
|
используют quality `1..100` из schema, BMP не имеет quality-параметра.
|
||||||
|
- E2E проверяет JPEG `FF D8 FF`, WebP `RIFF/WEBP`, BMP `BM` и PNG-фолбэк.
|
||||||
|
- Ошибки кодирования и unsupported MIME используют существующие i18n-ключи.
|
||||||
|
|
||||||
|
#### Q6d–Q6f. Оставшиеся направления
|
||||||
|
|
||||||
- output MIME/quality;
|
|
||||||
- malformed и special PNG fixtures: CRC, truncated, palette, 16-bit;
|
- malformed и special PNG fixtures: CRC, truncated, palette, 16-bit;
|
||||||
- smoke всех типов инструментов из registry;
|
- smoke всех типов инструментов из registry;
|
||||||
- component-тесты `SchemaToolView`, `SchemaPreview` и schema fields;
|
- component-тесты `SchemaToolView`, `SchemaPreview` и schema fields;
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ CSS-классы и конкретный формат временного URL
|
|||||||
|
|
||||||
`web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`,
|
`web/e2e/helpers/page.ts` владеет общими операциями `openTool`, `uploadImage`,
|
||||||
`resultImage`, `sourceImage`, `verdictStatus`, `textResult`, `errorAlert`,
|
`resultImage`, `sourceImage`, `verdictStatus`, `textResult`, `errorAlert`,
|
||||||
`emptyState`, `rangeInput`, `downloadResult` и `downloadResultFile`. Специфика
|
`emptyState`, `rangeInput`, `downloadResult`, `downloadResultFile` и
|
||||||
должна вызывать эти helpers, а не повторять raw-селекторы. Инструменты с
|
`downloadResultBytes`. Специфика должна вызывать эти helpers, а не повторять
|
||||||
`result: "files"` проверяются отдельным ZIP-сценарием: общий image-smoke не
|
raw-селекторы. Инструменты с `result: "files"` проверяются отдельным
|
||||||
применяется к ним.
|
ZIP-сценарием: общий image-smoke не применяется к ним.
|
||||||
|
|
||||||
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
|
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
|
||||||
checklist. В комментарии указывается ссылка на неё. После исправления тест и
|
checklist. В комментарии указывается ссылка на неё. После исправления тест и
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import {
|
import {
|
||||||
expect,
|
expect,
|
||||||
type Download,
|
type Download,
|
||||||
@@ -88,6 +89,18 @@ export async function downloadResultFile(page: Page): Promise<Download> {
|
|||||||
return download;
|
return download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadResultBytes(
|
||||||
|
page: Page,
|
||||||
|
): Promise<{ name: string; bytes: Uint8Array }> {
|
||||||
|
const download = await downloadResultFile(page);
|
||||||
|
const path = await download.path();
|
||||||
|
if (!path) throw new Error("download path is unavailable");
|
||||||
|
return {
|
||||||
|
name: download.suggestedFilename(),
|
||||||
|
bytes: new Uint8Array(await readFile(path)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadResult(page: Page): Promise<string> {
|
export async function downloadResult(page: Page): Promise<string> {
|
||||||
return (await downloadResultFile(page)).suggestedFilename();
|
return (await downloadResultFile(page)).suggestedFilename();
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-11
@@ -1,4 +1,3 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { unzipSync } from "fflate";
|
import { unzipSync } from "fflate";
|
||||||
import { expect, test } from "playwright/test";
|
import { expect, test } from "playwright/test";
|
||||||
import {
|
import {
|
||||||
@@ -9,8 +8,7 @@ import {
|
|||||||
opaquePng,
|
opaquePng,
|
||||||
} from "./helpers/fixtures";
|
} from "./helpers/fixtures";
|
||||||
import {
|
import {
|
||||||
downloadResult,
|
downloadResultBytes,
|
||||||
downloadResultFile,
|
|
||||||
errorAlert,
|
errorAlert,
|
||||||
expectNoErrorAlert,
|
expectNoErrorAlert,
|
||||||
expectNoErrors,
|
expectNoErrors,
|
||||||
@@ -58,17 +56,62 @@ test("flip-png: full flow - upload, result, download", async ({ page }) => {
|
|||||||
await openTool(page, "flip-png");
|
await openTool(page, "flip-png");
|
||||||
await uploadImage(page, landscapePng);
|
await uploadImage(page, landscapePng);
|
||||||
await expect(resultImage(page)).toBeVisible();
|
await expect(resultImage(page)).toBeVisible();
|
||||||
expect(await downloadResult(page)).toBe("flip-png.png");
|
const { name, bytes } = await downloadResultBytes(page);
|
||||||
|
expect(name).toBe("flip-png.png");
|
||||||
|
expect(Array.from(bytes.subarray(0, 8))).toEqual([
|
||||||
|
137, 80, 78, 71, 13, 10, 26, 10,
|
||||||
|
]);
|
||||||
await expectNoErrorAlert(page);
|
await expectNoErrorAlert(page);
|
||||||
expectNoErrors(sink);
|
expectNoErrors(sink);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("convert-png-to-jpg: produces a downloadable jpg", async ({ page }) => {
|
test("convert-png-to-jpg: produces JPEG and quality affects size", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
const sink = trackErrors(page);
|
const sink = trackErrors(page);
|
||||||
await openTool(page, "convert-png-to-jpg");
|
await openTool(page, "convert-png-to-jpg");
|
||||||
|
await uploadImage(page, largePng);
|
||||||
|
await expect(resultImage(page)).toBeVisible();
|
||||||
|
const quality = rangeInput(page, "Quality");
|
||||||
|
await quality.fill("10");
|
||||||
|
const low = await downloadResultBytes(page);
|
||||||
|
expect(low.name).toBe("convert-png-to-jpg.jpg");
|
||||||
|
expect(Array.from(low.bytes.subarray(0, 3))).toEqual([255, 216, 255]);
|
||||||
|
await quality.fill("90");
|
||||||
|
const high = await downloadResultBytes(page);
|
||||||
|
expect(high.bytes.length).toBeGreaterThan(low.bytes.length);
|
||||||
|
await expectNoErrorAlert(page);
|
||||||
|
expectNoErrors(sink);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("convert-png-to-webp: produces WebP and quality affects size", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const sink = trackErrors(page);
|
||||||
|
await openTool(page, "convert-png-to-webp");
|
||||||
|
await uploadImage(page, largePng);
|
||||||
|
await expect(resultImage(page)).toBeVisible();
|
||||||
|
const quality = rangeInput(page, "Quality");
|
||||||
|
await quality.fill("10");
|
||||||
|
const low = await downloadResultBytes(page);
|
||||||
|
expect(low.name).toBe("convert-png-to-webp.webp");
|
||||||
|
expect(Array.from(low.bytes.subarray(0, 4))).toEqual([82, 73, 70, 70]);
|
||||||
|
expect(Array.from(low.bytes.subarray(8, 12))).toEqual([87, 69, 66, 80]);
|
||||||
|
await quality.fill("90");
|
||||||
|
const high = await downloadResultBytes(page);
|
||||||
|
expect(high.bytes.length).toBeGreaterThan(low.bytes.length);
|
||||||
|
await expectNoErrorAlert(page);
|
||||||
|
expectNoErrors(sink);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("png-to-bmp: produces a BMP download", async ({ page }) => {
|
||||||
|
const sink = trackErrors(page);
|
||||||
|
await openTool(page, "png-to-bmp");
|
||||||
await uploadImage(page, opaquePng);
|
await uploadImage(page, opaquePng);
|
||||||
await expect(resultImage(page)).toBeVisible();
|
await expect(resultImage(page)).toBeVisible();
|
||||||
expect(await downloadResult(page)).toBe("convert-png-to-jpg.jpg");
|
const { name, bytes } = await downloadResultBytes(page);
|
||||||
|
expect(name).toBe("png-to-bmp.bmp");
|
||||||
|
expect(Array.from(bytes.subarray(0, 2))).toEqual([66, 77]);
|
||||||
await expectNoErrorAlert(page);
|
await expectNoErrorAlert(page);
|
||||||
expectNoErrors(sink);
|
expectNoErrors(sink);
|
||||||
});
|
});
|
||||||
@@ -102,11 +145,9 @@ test("split-into-parts-png downloads a zip with named PNG parts", async ({
|
|||||||
await rangeInput(page, "Rows").fill("2");
|
await rangeInput(page, "Rows").fill("2");
|
||||||
await expect(page.getByText("part-1-3.png")).toBeVisible();
|
await expect(page.getByText("part-1-3.png")).toBeVisible();
|
||||||
|
|
||||||
const download = await downloadResultFile(page);
|
const { name, bytes } = await downloadResultBytes(page);
|
||||||
expect(download.suggestedFilename()).toBe("split-into-parts-png.zip");
|
expect(name).toBe("split-into-parts-png.zip");
|
||||||
const path = await download.path();
|
const entries = unzipSync(bytes);
|
||||||
if (!path) throw new Error("download path is unavailable");
|
|
||||||
const entries = unzipSync(new Uint8Array(await readFile(path)));
|
|
||||||
|
|
||||||
expect(Object.keys(entries)).toEqual([
|
expect(Object.keys(entries)).toEqual([
|
||||||
"part-1-1.png",
|
"part-1-1.png",
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { isSupportedImage, unsupportedImageError } from "./io";
|
import {
|
||||||
|
isSupportedImage,
|
||||||
|
unsupportedImageError,
|
||||||
|
validateOutputQuality,
|
||||||
|
} from "./io";
|
||||||
|
|
||||||
describe("isSupportedImage", () => {
|
describe("isSupportedImage", () => {
|
||||||
it.each([
|
it.each([
|
||||||
@@ -38,3 +42,33 @@ describe("unsupportedImageError", () => {
|
|||||||
expect(err.vars?.type).toBe("unknown");
|
expect(err.vars?.type).toBe("unknown");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("validateOutputQuality", () => {
|
||||||
|
it.each(["image/jpeg", "image/webp"] as const)(
|
||||||
|
"принимает границы 0 и 1 для %s",
|
||||||
|
(mime) => {
|
||||||
|
expect(() => validateOutputQuality(mime, 0)).not.toThrow();
|
||||||
|
expect(() => validateOutputQuality(mime, 1)).not.toThrow();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(["image/jpeg", "image/webp"] as const)(
|
||||||
|
"отклоняет значение вне 0..1 для %s",
|
||||||
|
(mime) => {
|
||||||
|
expect(() => validateOutputQuality(mime, -0.01)).toThrow(
|
||||||
|
"errors.qualityRange",
|
||||||
|
);
|
||||||
|
expect(() => validateOutputQuality(mime, 1.01)).toThrow(
|
||||||
|
"errors.qualityRange",
|
||||||
|
);
|
||||||
|
expect(() => validateOutputQuality(mime, Number.NaN)).toThrow(
|
||||||
|
"errors.qualityRange",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("не применяет quality к PNG и BMP", () => {
|
||||||
|
expect(() => validateOutputQuality("image/png", 2)).not.toThrow();
|
||||||
|
expect(() => validateOutputQuality("image/bmp", 2)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+14
-5
@@ -84,6 +84,19 @@ export async function decodeTextImage(text: string): Promise<PixelImage> {
|
|||||||
return await decodeBytes(bytes);
|
return await decodeBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateOutputQuality(
|
||||||
|
mime: OutputMime,
|
||||||
|
quality?: number,
|
||||||
|
): void {
|
||||||
|
if (mime !== "image/jpeg" && mime !== "image/webp") return;
|
||||||
|
if (
|
||||||
|
quality !== undefined &&
|
||||||
|
(!Number.isFinite(quality) || quality < 0 || quality > 1)
|
||||||
|
) {
|
||||||
|
throw new ToolError("errors.qualityRange");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function encode(
|
export async function encode(
|
||||||
img: PixelImage,
|
img: PixelImage,
|
||||||
mime: OutputMime = "image/png",
|
mime: OutputMime = "image/png",
|
||||||
@@ -92,11 +105,7 @@ export async function encode(
|
|||||||
if (mime === "image/bmp") {
|
if (mime === "image/bmp") {
|
||||||
return new Blob([encodeBmpBytes(img)], { type: mime });
|
return new Blob([encodeBmpBytes(img)], { type: mime });
|
||||||
}
|
}
|
||||||
if (mime === "image/jpeg" || mime === "image/webp") {
|
validateOutputQuality(mime, quality);
|
||||||
if (quality !== undefined && (quality < 0 || quality > 1)) {
|
|
||||||
throw new ToolError("errors.qualityRange");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = img.width;
|
canvas.width = img.width;
|
||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { TOOLS } from ".";
|
import { TOOLS } from ".";
|
||||||
import { PREVIEW_GROUPS } from "../catalog";
|
import { PREVIEW_GROUPS } from "../catalog";
|
||||||
import type { PixelImage } from "../core/types";
|
import type { PixelImage } from "../core/types";
|
||||||
|
import type { OutputMime } from "../core/io";
|
||||||
import { defaultSchemaParams, sanitizeSchemaParams } from "../registry-schema";
|
import { defaultSchemaParams, sanitizeSchemaParams } from "../registry-schema";
|
||||||
import type { FileResult, ToolResult } from "./types";
|
import type { FileResult, ToolResult } from "./types";
|
||||||
|
|
||||||
@@ -78,6 +79,39 @@ describe("registry-new (переведённые инструменты)", () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("output metadata согласован с MIME и quality-схемой", () => {
|
||||||
|
const extensions: Record<OutputMime, string> = {
|
||||||
|
"image/png": "png",
|
||||||
|
"image/jpeg": "jpg",
|
||||||
|
"image/webp": "webp",
|
||||||
|
"image/bmp": "bmp",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const tool of TOOLS) {
|
||||||
|
const output = tool.output;
|
||||||
|
if (!output) continue;
|
||||||
|
expect(extensions[output.mime], `${tool.id}: mime`).toBe(output.ext);
|
||||||
|
if (output.qualityParamId) {
|
||||||
|
const qualityField = tool.schema.fields[output.qualityParamId];
|
||||||
|
expect(
|
||||||
|
qualityField,
|
||||||
|
`${tool.id}.${output.qualityParamId}`,
|
||||||
|
).toBeDefined();
|
||||||
|
expect(["number", "slider"], `${tool.id}: quality kind`).toContain(
|
||||||
|
qualityField?.spec.kind,
|
||||||
|
);
|
||||||
|
} else if (output.mime === "image/jpeg" || output.mime === "image/webp") {
|
||||||
|
expect(output.qualityParamId, `${tool.id}: quality`).toBeTruthy();
|
||||||
|
}
|
||||||
|
if (output.mime === "image/bmp") {
|
||||||
|
expect(
|
||||||
|
output.qualityParamId,
|
||||||
|
`${tool.id}: bmp quality`,
|
||||||
|
).toBeUndefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("source-aware dimension defaults применяются только к image tools", () => {
|
it("source-aware dimension defaults применяются только к image tools", () => {
|
||||||
for (const tool of TOOLS) {
|
for (const tool of TOOLS) {
|
||||||
for (const field of Object.values(tool.schema.fields)) {
|
for (const field of Object.values(tool.schema.fields)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user