mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
test: runtime fixtures - bad files
This commit is contained in:
+4
-4
@@ -51,17 +51,17 @@
|
|||||||
только если темы нет в существующем документе; иначе обновляется источник
|
только если темы нет в существующем документе; иначе обновляется источник
|
||||||
истины.
|
истины.
|
||||||
|
|
||||||
## Текущий baseline Q6c
|
## Текущий baseline Q6d
|
||||||
|
|
||||||
На момент фиксации Q6c:
|
На момент фиксации Q6d:
|
||||||
|
|
||||||
- production registry содержит 122 инструмента;
|
- production registry содержит 122 инструмента;
|
||||||
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
- `pnpm --dir web check` проходит без ошибок и предупреждений;
|
||||||
- `pnpm --dir web test` — 42 test files / 455 tests;
|
- `pnpm --dir web test` — 42 test files / 457 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 — 112 passed, 0 skipped.
|
- `pnpm verify` проходит; production E2E — 121 passed, 0 skipped.
|
||||||
|
|
||||||
Baseline и следующий шаг поддерживаются в `docs/plan-codebase-quality.md`.
|
Baseline и следующий шаг поддерживаются в `docs/plan-codebase-quality.md`.
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -107,7 +107,8 @@
|
|||||||
|
|
||||||
## Качество production UI
|
## Качество production UI
|
||||||
|
|
||||||
Q5 закрывает известные preview-проблемы; активных `test.fixme` в e2e больше нет.
|
Q5–Q6d закрывают известные preview-проблемы и baseline special fixtures;
|
||||||
|
активных `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 через общий
|
||||||
@@ -116,6 +117,11 @@ Q5 закрывает известные preview-проблемы; активн
|
|||||||
auto-run — `web/e2e/pipeline.spec.ts`.
|
auto-run — `web/e2e/pipeline.spec.ts`.
|
||||||
- [x] Ошибки хранят i18n key/vars и переводятся при выводе; изменение языка
|
- [x] Ошибки хранят i18n key/vars и переводятся при выводе; изменение языка
|
||||||
обновляет alert — `web/e2e/i18n.spec.ts`.
|
обновляет alert — `web/e2e/i18n.spec.ts`.
|
||||||
|
- [x] Malformed/special PNG fixtures: CRC, truncated, palette и 16-bit проходят
|
||||||
|
через upload без краша — `web/e2e/png-fixtures.spec.ts`.
|
||||||
|
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
||||||
|
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это
|
||||||
|
продуктовая задача, не Q6d.
|
||||||
|
|
||||||
## Идеи
|
## Идеи
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,13 @@
|
|||||||
- [ ] PNG 1×1 — инструменты не падают (проверено авто: flip-png, см. suite).
|
- [ ] PNG 1×1 — инструменты не падают (проверено авто: flip-png, см. suite).
|
||||||
- [ ] PNG с полупрозрачностью/чётким альфа-краёв (чёрно-белая шахматка) —
|
- [ ] PNG с полупрозрачностью/чётким альфа-краёв (чёрно-белая шахматка) —
|
||||||
remove-background, feather-edges, clean-edges дают корректные края.
|
remove-background, feather-edges, clean-edges дают корректные края.
|
||||||
- [ ] Палитровый PNG (индексированные цвета) — конвертируется/анализируется.
|
- [ ] Палитровый PNG (индексированные цвета) — конвертируется/анализируется
|
||||||
|
(авто-покрытие: `web/e2e/png-fixtures.spec.ts`).
|
||||||
- [ ] Анимированный/APNG, если попадётся — не ломает конвейер.
|
- [ ] Анимированный/APNG, если попадётся — не ломает конвейер.
|
||||||
- [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в
|
- [ ] Повреждённый/не-PNG файл — понятная ошибка, нет краша (авто-чека в
|
||||||
pipeline.spec.ts).
|
`pipeline.spec.ts` и `png-fixtures.spec.ts`).
|
||||||
- [ ] PNG с битой CRC / 16-bit — как ведут себя анализаторы (ориентация, размер,
|
- [ ] PNG с битой CRC / 16-bit — как ведут себя анализаторы (ориентация, размер,
|
||||||
прозрачность).
|
прозрачность); CRC/16-bit авто-покрыты в `png-fixtures.spec.ts`.
|
||||||
|
|
||||||
### Файлы (`tests/fixtures/manual-edge-cases/`)
|
### Файлы (`tests/fixtures/manual-edge-cases/`)
|
||||||
|
|
||||||
@@ -95,9 +96,9 @@
|
|||||||
корректный (не «кракозябры»).
|
корректный (не «кракозябры»).
|
||||||
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
- [ ] remove-background на сложной полупрозрачности — края не «звенят».
|
||||||
|
|
||||||
## G. Закрытые Q5/Q6b/Q6c-проблемы
|
## G. Закрытые Q5/Q6b/Q6c/Q6d-проблемы
|
||||||
|
|
||||||
> Q5/Q6b/Q6c-проблемы закрыты автоматическими тестами; для ручной проверки
|
> Q5/Q6b/Q6c/Q6d-проблемы закрыты автоматическими тестами; для ручной проверки
|
||||||
> остаются субъективные сценарии разделов A–F.
|
> остаются субъективные сценарии разделов A–F.
|
||||||
|
|
||||||
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
|
||||||
@@ -114,6 +115,8 @@
|
|||||||
`web/e2e/pipeline.spec.ts`.
|
`web/e2e/pipeline.spec.ts`.
|
||||||
- [x] **Output MIME/quality:** JPG/WebP/BMP имеют ожидаемые расширения и
|
- [x] **Output MIME/quality:** JPG/WebP/BMP имеют ожидаемые расширения и
|
||||||
сигнатуры, quality 10/90 меняет размер файла; `web/e2e/pipeline.spec.ts`.
|
сигнатуры, quality 10/90 меняет размер файла; `web/e2e/pipeline.spec.ts`.
|
||||||
|
- [x] **Malformed/special PNG:** CRC, truncated, palette и 16-bit fixtures
|
||||||
|
проходят через upload без краша; `web/e2e/png-fixtures.spec.ts`.
|
||||||
|
|
||||||
## Как долго
|
## Как долго
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# План: линтеры, тесты и качество кодовой базы
|
# План: линтеры, тесты и качество кодовой базы
|
||||||
|
|
||||||
> Статус: **draft — Q0–Q5 и Q6a–Q6c готовы к ревью; Q6d — следующий этап**.
|
> Статус: **draft — Q0–Q5 и Q6a–Q6d готовы к ревью; Q6e — следующий этап**.
|
||||||
>
|
>
|
||||||
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
> Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и
|
||||||
> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6c выполнены; Q6d–Q8 выполняются
|
> инфраструктурных скриптов. Q0–Q5 и Q6a–Q6d выполнены; Q6e–Q8 выполняются
|
||||||
> отдельными атомарными шагами.
|
> отдельными атомарными шагами.
|
||||||
>
|
>
|
||||||
> Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность»
|
> Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность»
|
||||||
@@ -243,9 +243,24 @@ JPG/WebP/BMP, не превращаясь в UX-райз кнопки Download.
|
|||||||
- E2E проверяет JPEG `FF D8 FF`, WebP `RIFF/WEBP`, BMP `BM` и PNG-фолбэк.
|
- E2E проверяет JPEG `FF D8 FF`, WebP `RIFF/WEBP`, BMP `BM` и PNG-фолбэк.
|
||||||
- Ошибки кодирования и unsupported MIME используют существующие i18n-ключи.
|
- Ошибки кодирования и unsupported MIME используют существующие i18n-ключи.
|
||||||
|
|
||||||
#### Q6d–Q6f. Оставшиеся направления
|
#### Q6d. Malformed и special PNG fixtures
|
||||||
|
|
||||||
|
**Статус:** выполнен. Runtime-fixtures покрывают CRC, truncated, palette и
|
||||||
|
16-bit без бинарных файлов в git. E2E фиксирует браузерный контракт: critical
|
||||||
|
CRC/truncated отклоняются без падения, ancillary CRC и хвост после `IEND`
|
||||||
|
допускаются, palette/16-bit декодируются. Unit фиксирует контракт анализаторов
|
||||||
|
для RGBA-данных, которые браузер отдаёт после нормализации.
|
||||||
|
|
||||||
|
- `web/e2e/helpers/fixtures.ts` собирает indexed, 16-bit, CRC-mutation,
|
||||||
|
truncation и tail-варианты из уже существующих `crc32`/`chunk`.
|
||||||
|
- Проверки выполняются в Chromium; browser matrix остаётся Q6f.
|
||||||
|
- Поведение `createImageBitmap` является частью browser-контракта, а не
|
||||||
|
самостоятельного PNG-парсера product-кода.
|
||||||
|
- Сырое сообщение браузера при decode-ошибке не локализуется в Q6d; это
|
||||||
|
отдельная задача в backlog.
|
||||||
|
|
||||||
|
#### Q6e–Q6f. Оставшиеся направления
|
||||||
|
|
||||||
- malformed и special PNG fixtures: CRC, truncated, palette, 16-bit;
|
|
||||||
- smoke всех типов инструментов из registry;
|
- smoke всех типов инструментов из registry;
|
||||||
- component-тесты `SchemaToolView`, `SchemaPreview` и schema fields;
|
- component-тесты `SchemaToolView`, `SchemaPreview` и schema fields;
|
||||||
- browser matrix, mobile viewport, visual snapshots и coverage thresholds.
|
- browser matrix, mobile viewport, visual snapshots и coverage thresholds.
|
||||||
@@ -408,7 +423,6 @@ refactor, новые i18n-правила и исправление продук
|
|||||||
## 7. Решения, которые нужно принять на ревью
|
## 7. Решения, которые нужно принять на ревью
|
||||||
|
|
||||||
- Какие executor/worker/fallback сценарии войдут в первый Q6 coverage PR?
|
- Какие executor/worker/fallback сценарии войдут в первый Q6 coverage PR?
|
||||||
- Какие special PNG fixtures обязательны для baseline?
|
|
||||||
- Нужны ли browser/mobile matrix и visual snapshots до coverage thresholds?
|
- Нужны ли browser/mobile matrix и visual snapshots до coverage thresholds?
|
||||||
- Какие unused design tokens можно удалить, а какие должны остаться?
|
- Какие unused design tokens можно удалить, а какие должны остаться?
|
||||||
- Какие browser/e2e проверки должны быть обязательными для PR, а какие —
|
- Какие browser/e2e проверки должны быть обязательными для PR, а какие —
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ pnpm --dir web test:e2e
|
|||||||
```
|
```
|
||||||
|
|
||||||
Запускать его обязательно для изменений маршрутов, загрузки файла, результата,
|
Запускать его обязательно для изменений маршрутов, загрузки файла, результата,
|
||||||
download, темы или локализации. В текущем CI это отдельный non-blocking
|
download, темы или локализации. Изменения runtime fixtures/загрузки файла
|
||||||
|
проверяются отдельным `web/e2e/png-fixtures.spec.ts`; ручные бинарники из
|
||||||
|
`tests/fixtures/` не входят в `verify`. В текущем CI это отдельный non-blocking
|
||||||
baseline; после закрытия известных ошибок его можно сделать blocking.
|
baseline; после закрытия известных ошибок его можно сделать blocking.
|
||||||
|
|
||||||
## Быстрые подмножества
|
## Быстрые подмножества
|
||||||
@@ -46,6 +48,7 @@ baseline; после закрытия известных ошибок его м
|
|||||||
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
|
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
|
||||||
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
|
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
|
||||||
| Пользовательский поток | `pnpm --dir web test:e2e` |
|
| Пользовательский поток | `pnpm --dir web test:e2e` |
|
||||||
|
| Fixtures/загрузка файла | `pnpm --dir web test:e2e` |
|
||||||
| Полный локальный gate | `pnpm verify` |
|
| Полный локальный gate | `pnpm verify` |
|
||||||
|
|
||||||
## Errors и warnings
|
## Errors и warnings
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ CSS-классы и конкретный формат временного URL
|
|||||||
|
|
||||||
Изображения создаются воспроизводимыми helpers в `web/e2e/helpers/fixtures.ts`.
|
Изображения создаются воспроизводимыми helpers в `web/e2e/helpers/fixtures.ts`.
|
||||||
Тесты не добавляют большие бинарные файлы и не зависят от локальных
|
Тесты не добавляют большие бинарные файлы и не зависят от локальных
|
||||||
пользовательских файлов. `toPass()` допускается только для ограниченного
|
пользовательских файлов. Runtime-fixtures для malformed/special PNG проверяют
|
||||||
ожидания hydration или асинхронного результата; он не должен превращать проверку
|
browser-контракт: critical CRC и truncated отклоняются, ancillary CRC и хвост
|
||||||
продукта в бесконечный retry.
|
после `IEND` допускаются, palette/16-bit декодируются. `toPass()` допускается
|
||||||
|
только для ограниченного ожидания hydration или асинхронного результата; он не
|
||||||
|
должен превращать проверку продукта в бесконечный retry.
|
||||||
|
|
||||||
## 5. Helpers и `test.fixme`
|
## 5. Helpers и `test.fixme`
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ ZIP-сценарием: общий image-smoke не применяется к н
|
|||||||
|
|
||||||
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
|
`test.fixme` допускается только для зарегистрированной проблемы в backlog или
|
||||||
checklist. В комментарии указывается ссылка на неё. После исправления тест и
|
checklist. В комментарии указывается ссылка на неё. После исправления тест и
|
||||||
ссылка обновляются вместе; в текущем Q6a baseline активных fixme нет.
|
ссылка обновляются вместе; в текущем Q6d baseline активных fixme нет.
|
||||||
|
|
||||||
## 6. Проверки
|
## 6. Проверки
|
||||||
|
|
||||||
|
|||||||
+176
-8
@@ -23,6 +23,22 @@ function crc32(data: Uint8Array): number {
|
|||||||
return (c ^ 0xffffffff) >>> 0;
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
|
|
||||||
|
function makeIhdr(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
bitDepth: number,
|
||||||
|
colorType: number,
|
||||||
|
): Buffer {
|
||||||
|
const ihdr = Buffer.alloc(13);
|
||||||
|
ihdr.writeUInt32BE(width, 0);
|
||||||
|
ihdr.writeUInt32BE(height, 4);
|
||||||
|
ihdr[8] = bitDepth;
|
||||||
|
ihdr[9] = colorType;
|
||||||
|
return ihdr;
|
||||||
|
}
|
||||||
|
|
||||||
function chunk(type: string, data: Buffer): Buffer {
|
function chunk(type: string, data: Buffer): Buffer {
|
||||||
const out = Buffer.alloc(8 + data.length + 4);
|
const out = Buffer.alloc(8 + data.length + 4);
|
||||||
out.writeUInt32BE(data.length, 0);
|
out.writeUInt32BE(data.length, 0);
|
||||||
@@ -32,18 +48,23 @@ function chunk(type: string, data: Buffer): Buffer {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findChunk(png: Buffer, type: string): number {
|
||||||
|
let offset = PNG_SIGNATURE.length;
|
||||||
|
while (offset + 12 <= png.length) {
|
||||||
|
const length = png.readUInt32BE(offset);
|
||||||
|
const chunkType = png.subarray(offset + 4, offset + 8).toString("ascii");
|
||||||
|
if (chunkType === type) return offset;
|
||||||
|
offset += 12 + length;
|
||||||
|
}
|
||||||
|
throw new Error(`PNG chunk ${type} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
/** Строит валидный RGBA PNG в рантайме (8-bit, фильтр type 0, deflate/zlib). */
|
/** Строит валидный RGBA PNG в рантайме (8-bit, фильтр type 0, deflate/zlib). */
|
||||||
export function makePng(
|
export function makePng(
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
pixelAt: (x: number, y: number) => [number, number, number, number],
|
pixelAt: (x: number, y: number) => [number, number, number, number],
|
||||||
): Buffer {
|
): Buffer {
|
||||||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
||||||
const ihdr = Buffer.alloc(13);
|
|
||||||
ihdr.writeUInt32BE(width, 0);
|
|
||||||
ihdr.writeUInt32BE(height, 4);
|
|
||||||
ihdr[8] = 8;
|
|
||||||
ihdr[9] = 6;
|
|
||||||
const stride = width * 4 + 1;
|
const stride = width * 4 + 1;
|
||||||
const raw = Buffer.alloc(height * stride);
|
const raw = Buffer.alloc(height * stride);
|
||||||
for (let y = 0; y < height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
@@ -57,13 +78,100 @@ export function makePng(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Buffer.concat([
|
return Buffer.concat([
|
||||||
sig,
|
PNG_SIGNATURE,
|
||||||
chunk("IHDR", ihdr),
|
chunk("IHDR", makeIhdr(width, height, 8, 6)),
|
||||||
chunk("IDAT", deflateSync(raw)),
|
chunk("IDAT", deflateSync(raw)),
|
||||||
chunk("IEND", Buffer.alloc(0)),
|
chunk("IEND", Buffer.alloc(0)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeIndexedPng(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
palette: [number, number, number][],
|
||||||
|
pixelAt: (x: number, y: number) => number,
|
||||||
|
): Buffer {
|
||||||
|
const plte = Buffer.alloc(palette.length * 3);
|
||||||
|
for (const [index, color] of palette.entries()) {
|
||||||
|
plte[index * 3] = color[0];
|
||||||
|
plte[index * 3 + 1] = color[1];
|
||||||
|
plte[index * 3 + 2] = color[2];
|
||||||
|
}
|
||||||
|
const stride = width + 1;
|
||||||
|
const raw = Buffer.alloc(height * stride);
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
raw[y * stride + 1 + x] = pixelAt(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Buffer.concat([
|
||||||
|
PNG_SIGNATURE,
|
||||||
|
chunk("IHDR", makeIhdr(width, height, 8, 3)),
|
||||||
|
chunk("PLTE", plte),
|
||||||
|
chunk("IDAT", deflateSync(raw)),
|
||||||
|
chunk("IEND", Buffer.alloc(0)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function make16BitPng(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
pixelAt: (x: number, y: number) => [number, number, number, number],
|
||||||
|
): Buffer {
|
||||||
|
const stride = width * 8 + 1;
|
||||||
|
const raw = Buffer.alloc(height * stride);
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const [r, g, b, a] = pixelAt(x, y);
|
||||||
|
const p = y * stride + 1 + x * 8;
|
||||||
|
raw.writeUInt16BE(r, p);
|
||||||
|
raw.writeUInt16BE(g, p + 2);
|
||||||
|
raw.writeUInt16BE(b, p + 4);
|
||||||
|
raw.writeUInt16BE(a, p + 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Buffer.concat([
|
||||||
|
PNG_SIGNATURE,
|
||||||
|
chunk("IHDR", makeIhdr(width, height, 16, 6)),
|
||||||
|
chunk("IDAT", deflateSync(raw)),
|
||||||
|
chunk("IEND", Buffer.alloc(0)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withPngChunk(png: Buffer, type: string, data: Buffer): Buffer {
|
||||||
|
const iendOffset = png.length - 12;
|
||||||
|
return Buffer.concat([
|
||||||
|
png.subarray(0, iendOffset),
|
||||||
|
chunk(type, data),
|
||||||
|
png.subarray(iendOffset),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withBadChunkCrc(png: Buffer, type: string): Buffer {
|
||||||
|
const copy = Buffer.from(png);
|
||||||
|
const offset = findChunk(copy, type);
|
||||||
|
const length = copy.readUInt32BE(offset);
|
||||||
|
copy[offset + 8 + length + 3] ^= 0xff;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncatePng(png: Buffer, length: number): Buffer {
|
||||||
|
return Buffer.from(png.subarray(0, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncateChunkData(
|
||||||
|
png: Buffer,
|
||||||
|
type: string,
|
||||||
|
length: number,
|
||||||
|
): Buffer {
|
||||||
|
const offset = findChunk(png, type);
|
||||||
|
return truncatePng(png, offset + 8 + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTrailingBytes(png: Buffer, tail: Buffer): Buffer {
|
||||||
|
return Buffer.concat([png, Buffer.from(tail)]);
|
||||||
|
}
|
||||||
|
|
||||||
const checker = (x: number, y: number): [number, number, number, number] =>
|
const checker = (x: number, y: number): [number, number, number, number] =>
|
||||||
x % 2 === y % 2 ? [255, 0, 0, 255] : [255, 255, 255, 255];
|
x % 2 === y % 2 ? [255, 0, 0, 255] : [255, 255, 255, 255];
|
||||||
|
|
||||||
@@ -102,6 +210,66 @@ export const corruptPng: SourceFile = {
|
|||||||
buffer: Buffer.from("this is definitely not a png file", "utf8"),
|
buffer: Buffer.from("this is definitely not a png file", "utf8"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const specialPng = makePng(8, 8, checker);
|
||||||
|
const specialWithText = withPngChunk(
|
||||||
|
specialPng,
|
||||||
|
"tEXt",
|
||||||
|
Buffer.from("Comment\0q6d", "ascii"),
|
||||||
|
);
|
||||||
|
const palette4 = [
|
||||||
|
[255, 0, 0],
|
||||||
|
[0, 255, 0],
|
||||||
|
[0, 0, 255],
|
||||||
|
[255, 255, 255],
|
||||||
|
] as [number, number, number][];
|
||||||
|
const palette256 = Array.from({ length: 256 }, (_, index) => [
|
||||||
|
index,
|
||||||
|
(index * 3) % 256,
|
||||||
|
(index * 7) % 256,
|
||||||
|
]) as [number, number, number][];
|
||||||
|
|
||||||
|
export const crcBadIdatPng = asSourceFile(
|
||||||
|
withBadChunkCrc(specialPng, "IDAT"),
|
||||||
|
"crc-bad-idat.png",
|
||||||
|
);
|
||||||
|
export const crcBadAncillaryPng = asSourceFile(
|
||||||
|
withBadChunkCrc(specialWithText, "tEXt"),
|
||||||
|
"crc-bad-ancillary.png",
|
||||||
|
);
|
||||||
|
export const truncatedHeaderPng = asSourceFile(
|
||||||
|
truncatePng(specialPng, PNG_SIGNATURE.length + 12 + 13),
|
||||||
|
"truncated-header.png",
|
||||||
|
);
|
||||||
|
export const truncatedNoIendPng = asSourceFile(
|
||||||
|
truncatePng(specialPng, specialPng.length - 12),
|
||||||
|
"truncated-no-iend.png",
|
||||||
|
);
|
||||||
|
export const idatCutPng = asSourceFile(
|
||||||
|
truncateChunkData(specialPng, "IDAT", 4),
|
||||||
|
"idat-cut.png",
|
||||||
|
);
|
||||||
|
export const garbageTailPng = asSourceFile(
|
||||||
|
withTrailingBytes(specialPng, Buffer.from("trailing-garbage", "ascii")),
|
||||||
|
"garbage-tail.png",
|
||||||
|
);
|
||||||
|
export const palette4Png = asSourceFile(
|
||||||
|
makeIndexedPng(
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
palette4,
|
||||||
|
(x, y) => (Math.floor(x / 2) + Math.floor(y / 2)) % palette4.length,
|
||||||
|
),
|
||||||
|
"palette-4.png",
|
||||||
|
);
|
||||||
|
export const palette256Png = asSourceFile(
|
||||||
|
makeIndexedPng(16, 16, palette256, (x, y) => (x + y * 16) % 256),
|
||||||
|
"palette-256.png",
|
||||||
|
);
|
||||||
|
export const sixteenBitPng = asSourceFile(
|
||||||
|
make16BitPng(8, 8, (x, y) => [x * 8000, y * 8000, 32768, 65535]),
|
||||||
|
"16bit.png",
|
||||||
|
);
|
||||||
|
|
||||||
export const tinyBase64 = makePng(8, 8, solidRed).toString("base64");
|
export const tinyBase64 = makePng(8, 8, solidRed).toString("base64");
|
||||||
|
|
||||||
export const svgMarkup =
|
export const svgMarkup =
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { expect, test, type Page } from "playwright/test";
|
||||||
|
import {
|
||||||
|
crcBadAncillaryPng,
|
||||||
|
crcBadIdatPng,
|
||||||
|
garbageTailPng,
|
||||||
|
idatCutPng,
|
||||||
|
palette4Png,
|
||||||
|
palette256Png,
|
||||||
|
sixteenBitPng,
|
||||||
|
truncatedHeaderPng,
|
||||||
|
truncatedNoIendPng,
|
||||||
|
type SourceFile,
|
||||||
|
} from "./helpers/fixtures";
|
||||||
|
import {
|
||||||
|
errorAlert,
|
||||||
|
expectNoErrorAlert,
|
||||||
|
expectNoErrors,
|
||||||
|
openTool,
|
||||||
|
resultImage,
|
||||||
|
trackErrors,
|
||||||
|
uploadImage,
|
||||||
|
} from "./helpers/page";
|
||||||
|
|
||||||
|
async function expectRejected(page: Page, file: SourceFile): Promise<void> {
|
||||||
|
const sink = trackErrors(page);
|
||||||
|
await openTool(page, "flip-png");
|
||||||
|
await uploadImage(page, file);
|
||||||
|
await expect(errorAlert(page)).toBeVisible();
|
||||||
|
await expect(resultImage(page)).toHaveCount(0);
|
||||||
|
expectNoErrors(sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectAccepted(page: Page, file: SourceFile): Promise<void> {
|
||||||
|
const sink = trackErrors(page);
|
||||||
|
await openTool(page, "flip-png");
|
||||||
|
await uploadImage(page, file);
|
||||||
|
await expect(resultImage(page)).toBeVisible();
|
||||||
|
await expectNoErrorAlert(page);
|
||||||
|
expectNoErrors(sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rejectedFixtures = [
|
||||||
|
crcBadIdatPng,
|
||||||
|
truncatedHeaderPng,
|
||||||
|
truncatedNoIendPng,
|
||||||
|
idatCutPng,
|
||||||
|
];
|
||||||
|
|
||||||
|
const acceptedFixtures = [
|
||||||
|
crcBadAncillaryPng,
|
||||||
|
garbageTailPng,
|
||||||
|
palette4Png,
|
||||||
|
palette256Png,
|
||||||
|
sixteenBitPng,
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe("PNG special fixtures", () => {
|
||||||
|
for (const file of rejectedFixtures) {
|
||||||
|
test(`${file.name}: shows an error without a result`, async ({ page }) => {
|
||||||
|
await expectRejected(page, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of acceptedFixtures) {
|
||||||
|
test(`${file.name}: decodes without an error`, async ({ page }) => {
|
||||||
|
await expectAccepted(page, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -63,7 +63,7 @@ export default tseslint.config(
|
|||||||
"e2e/text-and-verdicts.spec.ts",
|
"e2e/text-and-verdicts.spec.ts",
|
||||||
"e2e/tools-smoke.spec.ts",
|
"e2e/tools-smoke.spec.ts",
|
||||||
"e2e/generators.spec.ts",
|
"e2e/generators.spec.ts",
|
||||||
"e2e/known-issues.spec.ts",
|
"e2e/png-fixtures.spec.ts",
|
||||||
"e2e/i18n.spec.ts",
|
"e2e/i18n.spec.ts",
|
||||||
// Тесты и фикстуры кастомных линт-правил лежат вне src/ (не в
|
// Тесты и фикстуры кастомных линт-правил лежат вне src/ (не в
|
||||||
// tsconfig), поэтому для типизированного парсинга резолвятся
|
// tsconfig), поэтому для типизированного парсинга резолвятся
|
||||||
|
|||||||
@@ -72,6 +72,40 @@ describe("hasTransparency", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("special decoded pixel data", () => {
|
||||||
|
it("палитра после browser-декодирования сохраняет количество цветов", () => {
|
||||||
|
const image = makeImage(2, 2, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[0, 255, 0, 255],
|
||||||
|
[0, 0, 255, 255],
|
||||||
|
[255, 255, 255, 255],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(imageInfo(image)).toMatchObject({
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
hasAlpha: false,
|
||||||
|
colorCount: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("16-bit после downscale остаётся цветным и непрозрачным", () => {
|
||||||
|
const image = makeImage(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
Array.from({ length: 16 }, (_, index) => {
|
||||||
|
const value = index * 8;
|
||||||
|
return [value, value, 128, 255];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imageInfo(image).colorCount).toBe(16);
|
||||||
|
expect(hasTransparency(image)).toBe(false);
|
||||||
|
expect(isGrayscale(image)).toBe(false);
|
||||||
|
expect(orientationOf(image)).toBe("square");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("orientationOf", () => {
|
describe("orientationOf", () => {
|
||||||
it("определяет ориентацию", () => {
|
it("определяет ориентацию", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user