mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 06:59:40 +02:00
Add comprehensive test suite (174 tests) covering encoding, schema, image, database CRUD, and PNG round-trip. Fix critical bugs: - PNG compression: replace non-functional zlibCompress with pako.deflate - PNG import: add CRC validation, support all filter types (Sub/Up/Avg/Paeth) - Input validation: validate records against schema before insert - Index overflow: dynamic dataStartRing prevents index/data ring overlap - Image expansion: expand before writes instead of after to prevent OOB - update() read bug: search index from end to find latest entry, not deleted one - String encoding: enforce 511-byte max length - Index ring count: use 6 bits (2 pixels) instead of 3 bits for >7 ring support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
251 lines
7.1 KiB
TypeScript
251 lines
7.1 KiB
TypeScript
/**
|
|
* Image Tests
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
createImage,
|
|
createImageForRing,
|
|
getPixelByIndex,
|
|
setPixelByIndex,
|
|
getPixelByXY,
|
|
setPixelByXY,
|
|
readPixelRange,
|
|
writePixelRange,
|
|
expandImage,
|
|
getMaxRingForImage,
|
|
imageToRGBA,
|
|
rgbaToImage,
|
|
imageToColorGrid,
|
|
visualizeSpiralOrder,
|
|
visualizeImageEmoji,
|
|
} from './image.js';
|
|
import type { ColorIndex } from './types.js';
|
|
|
|
// =============================================================================
|
|
// CREATE IMAGE
|
|
// =============================================================================
|
|
|
|
describe('createImage', () => {
|
|
it('should create a square image with correct dimensions', () => {
|
|
const image = createImage(5);
|
|
expect(image.width).toBe(5);
|
|
expect(image.height).toBe(5);
|
|
expect(image.pixels.length).toBe(5 * 5 * 3);
|
|
});
|
|
|
|
it('should initialize all pixels to black (0)', () => {
|
|
const image = createImage(3);
|
|
for (let i = 0; i < image.pixels.length; i++) {
|
|
expect(image.pixels[i]).toBe(0);
|
|
}
|
|
});
|
|
|
|
it('should reject even sizes', () => {
|
|
expect(() => createImage(4)).toThrow('Image size must be odd');
|
|
});
|
|
|
|
it('should create 1x1 image', () => {
|
|
const image = createImage(1);
|
|
expect(image.width).toBe(1);
|
|
expect(image.pixels.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('createImageForRing', () => {
|
|
it('should create correct size for ring 0', () => {
|
|
const image = createImageForRing(0);
|
|
expect(image.width).toBe(1);
|
|
});
|
|
|
|
it('should create correct size for ring 2', () => {
|
|
const image = createImageForRing(2);
|
|
expect(image.width).toBe(5);
|
|
});
|
|
|
|
it('should create correct size for ring 5', () => {
|
|
const image = createImageForRing(5);
|
|
expect(image.width).toBe(11);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// PIXEL ACCESS
|
|
// =============================================================================
|
|
|
|
describe('Pixel Access by XY', () => {
|
|
it('should set and get pixel', () => {
|
|
const image = createImage(3);
|
|
setPixelByXY(image, 1, 1, 7); // white at center
|
|
expect(getPixelByXY(image, 1, 1)).toBe(7);
|
|
});
|
|
|
|
it('should set all 8 colors', () => {
|
|
const image = createImage(3);
|
|
for (let i = 0; i < 8; i++) {
|
|
setPixelByXY(image, i % 3, Math.floor(i / 3), i as ColorIndex);
|
|
}
|
|
for (let i = 0; i < 8; i++) {
|
|
expect(getPixelByXY(image, i % 3, Math.floor(i / 3))).toBe(i);
|
|
}
|
|
});
|
|
|
|
it('should throw on out-of-bounds access', () => {
|
|
const image = createImage(3);
|
|
expect(() => getPixelByXY(image, -1, 0)).toThrow('out of bounds');
|
|
expect(() => getPixelByXY(image, 3, 0)).toThrow('out of bounds');
|
|
expect(() => getPixelByXY(image, 0, 3)).toThrow('out of bounds');
|
|
expect(() => setPixelByXY(image, 0, -1, 0)).toThrow('out of bounds');
|
|
});
|
|
});
|
|
|
|
describe('Pixel Access by Index', () => {
|
|
it('should set and get center pixel (index 0)', () => {
|
|
const image = createImage(5);
|
|
setPixelByIndex(image, 0, 7);
|
|
expect(getPixelByIndex(image, 0)).toBe(7);
|
|
});
|
|
|
|
it('should set and get ring 1 pixels', () => {
|
|
const image = createImage(5);
|
|
for (let i = 1; i <= 8; i++) {
|
|
setPixelByIndex(image, i, (i % 8) as ColorIndex);
|
|
}
|
|
for (let i = 1; i <= 8; i++) {
|
|
expect(getPixelByIndex(image, i)).toBe(i % 8);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Pixel Range Operations', () => {
|
|
it('should write and read a range', () => {
|
|
const image = createImage(5);
|
|
const colors: ColorIndex[] = [1, 2, 3, 4, 5];
|
|
writePixelRange(image, 0, colors);
|
|
const read = readPixelRange(image, 0, 5);
|
|
expect(read).toEqual(colors);
|
|
});
|
|
|
|
it('should handle range of 1', () => {
|
|
const image = createImage(3);
|
|
writePixelRange(image, 0, [7]);
|
|
expect(readPixelRange(image, 0, 1)).toEqual([7]);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// IMAGE EXPANSION
|
|
// =============================================================================
|
|
|
|
describe('expandImage', () => {
|
|
it('should grow image to accommodate new ring', () => {
|
|
const image = createImage(3); // ring 1
|
|
setPixelByIndex(image, 0, 7); // white center
|
|
|
|
const expanded = expandImage(image, 3);
|
|
expect(expanded.width).toBe(7); // ring 3 → 2*3+1
|
|
expect(expanded.height).toBe(7);
|
|
|
|
// Center pixel should be preserved
|
|
expect(getPixelByIndex(expanded, 0)).toBe(7);
|
|
});
|
|
|
|
it('should not expand if already large enough', () => {
|
|
const image = createImage(7);
|
|
const same = expandImage(image, 2); // ring 2 needs 5, we have 7
|
|
expect(same).toBe(image); // same reference
|
|
});
|
|
|
|
it('should preserve all existing pixels', () => {
|
|
const image = createImage(3);
|
|
// Set all 9 pixels
|
|
for (let i = 0; i < 9; i++) {
|
|
setPixelByIndex(image, i, (i % 8) as ColorIndex);
|
|
}
|
|
|
|
const expanded = expandImage(image, 3);
|
|
|
|
// Verify all original pixels preserved
|
|
for (let i = 0; i < 9; i++) {
|
|
expect(getPixelByIndex(expanded, i)).toBe(i % 8);
|
|
}
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// FORMAT CONVERSIONS
|
|
// =============================================================================
|
|
|
|
describe('RGBA Conversion', () => {
|
|
it('should convert to RGBA and back', () => {
|
|
const image = createImage(3);
|
|
setPixelByIndex(image, 0, 7); // white center
|
|
setPixelByIndex(image, 1, 4); // red
|
|
|
|
const rgba = imageToRGBA(image);
|
|
expect(rgba.length).toBe(3 * 3 * 4); // 4 bytes per pixel
|
|
|
|
const back = rgbaToImage(rgba, 3, 3);
|
|
expect(getPixelByIndex(back, 0)).toBe(7);
|
|
expect(getPixelByIndex(back, 1)).toBe(4);
|
|
});
|
|
|
|
it('should set alpha to 255 in RGBA', () => {
|
|
const image = createImage(1);
|
|
const rgba = imageToRGBA(image);
|
|
expect(rgba[3]).toBe(255); // alpha
|
|
});
|
|
|
|
it('should reject non-square RGBA', () => {
|
|
const rgba = new Uint8Array(2 * 3 * 4);
|
|
expect(() => rgbaToImage(rgba, 2, 3)).toThrow('must be square');
|
|
});
|
|
|
|
it('should reject even-sized RGBA', () => {
|
|
const rgba = new Uint8Array(4 * 4 * 4);
|
|
expect(() => rgbaToImage(rgba, 4, 4)).toThrow('must be odd');
|
|
});
|
|
});
|
|
|
|
describe('Color Grid', () => {
|
|
it('should return 2D grid of color indices', () => {
|
|
const image = createImage(3);
|
|
setPixelByXY(image, 0, 0, 4);
|
|
setPixelByXY(image, 2, 2, 2);
|
|
|
|
const grid = imageToColorGrid(image);
|
|
expect(grid.length).toBe(3);
|
|
expect(grid[0].length).toBe(3);
|
|
expect(grid[0][0]).toBe(4);
|
|
expect(grid[2][2]).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('getMaxRingForImage', () => {
|
|
it('should return correct max ring', () => {
|
|
expect(getMaxRingForImage(createImage(1))).toBe(0);
|
|
expect(getMaxRingForImage(createImage(3))).toBe(1);
|
|
expect(getMaxRingForImage(createImage(5))).toBe(2);
|
|
expect(getMaxRingForImage(createImage(11))).toBe(5);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// VISUALIZATION
|
|
// =============================================================================
|
|
|
|
describe('Visualization', () => {
|
|
it('should produce emoji visualization with correct dimensions', () => {
|
|
const image = createImage(3);
|
|
setPixelByIndex(image, 0, 7);
|
|
const emoji = visualizeImageEmoji(image);
|
|
const lines = emoji.split('\n');
|
|
expect(lines.length).toBe(3);
|
|
});
|
|
|
|
it('should produce spiral order visualization', () => {
|
|
const viz = visualizeSpiralOrder(3);
|
|
expect(viz).toContain('0'); // center
|
|
expect(viz.split('\n')).toHaveLength(3);
|
|
});
|
|
});
|