Skip to content

Commit

Permalink
test: sprites
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelKreil committed Feb 19, 2024
1 parent a70cfd6 commit 8408035
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 28 deletions.
184 changes: 158 additions & 26 deletions scripts/lib/sprites.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,48 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/naming-convention */


import { jest } from '@jest/globals';
import type { Sharp } from 'sharp';
import type { Pack } from 'tar-stream';

jest.mock('sharp');
jest.unstable_mockModule('node:fs', () => ({
writeFileSync: jest.fn(),
readFileSync: jest.fn(),
rmSync: jest.fn(),
}));
jest.unstable_mockModule('node:child_process', () => ({
spawnSync: jest.fn().mockReturnValue({ status: 0 }),
}));
jest.mock('path');
jest.mock('tar-stream');

const sharp = (await import('sharp')).default;
const { Sprite } = await import('./sprites.ts');
const fs = await import('node:fs');
await import('node:child_process');

describe('Sprite', () => {
describe('fromIcons', () => {
it('should create a Sprite instance from icons', async () => {
// Setup mocks for sharp and any other necessary operations
jest.mocked(sharp).mockImplementation(() => ({
composite: jest.fn<Sharp['composite']>().mockReturnThis(),
raw: jest.fn<Sharp['raw']>().mockReturnThis(),
// @ts-expect-error to lazy
toBuffer: jest.fn<() => Promise<Buffer>>().mockResolvedValue(Buffer.from('test')),
}));

const icons = [{ name: 'icon1', svg: '<svg width="100" height="100"></svg>', size: 100 }];
const sprite = await Sprite.fromIcons(icons, 1, 10);
const fakeIcons = [
{
name: 'icon1', size: 64, svg: '<svg width="64" height="64"><rect width="64" height="64" fill="#000" /></svg>',
},
{
name: 'icon2', size: 32, svg: '<svg width="32" height="32"><circle cx="16" cy="16" r="16" fill="#f00" /></svg>',
},
{
name: 'icon3', size: 48, svg: '<svg width="48" height="48"><path d="M 24 0 L 48 48 H 0 Z" fill="#00f" /></svg>',
},
];

expect(sprite).toBeInstanceOf(Sprite);
// Further assertions to verify the internals like dimensions, buffer, etc., can be added here
});
});

describe('Sprite', () => {
describe('saveToDisk', () => {
it('should save the sprite to disk as PNG and JSON', async () => {
const basename = 'sprite';
const folder = '/test';
const mockSprite = await Sprite.fromIcons([], 1, 10);
const mockSprite = await Sprite.fromIcons(fakeIcons, 1, 10);

// Stub the getPng and getJSON methods to simplify
jest.spyOn(mockSprite as any, 'getPng').mockResolvedValue(Buffer.from('png data'));
Expand All @@ -54,21 +55,152 @@ describe('Sprite', () => {
});
});

describe('Sprite.fromIcons', () => {
describe('fromIcons', () => {
it('creates a Sprite instance from icons', async () => {
// Mock `sharp` and `bin-pack` as necessary
jest.mock('sharp');
jest.mock('bin-pack', () => ({
default: jest.fn(() => ({ width: 100, height: 100 })),
}));

const icons = [{ name: 'icon1', svg: '<svg width="100" height="100"></svg>', size: 50 }];
const sprite = await Sprite.fromIcons(icons, 2, 5); // Example usage
const sprite = await Sprite.fromIcons(fakeIcons, 2, 5); // Example usage

expect(sprite).toBeInstanceOf(Sprite);
expect((sprite as any).width).toBe(120);
expect((sprite as any).height).toBe(120);
expect((sprite as any).width).toBe(264);
expect((sprite as any).height).toBe(232);
});
});

describe('calcSDF', () => {
it('calculates the signed distance field for the sprite', async () => {
// This is a hypothetical test; implementation depends on `calcSDF` visibility and effects
const sprite = await Sprite.fromIcons(fakeIcons, 1, 0); // Setup with your actual data
sprite.calcSDF(); // Assuming it's accessible or its effect can be observed

// Verify the `distance` property or related outputs
expect((sprite as any).distance).toBeDefined();
expect((sprite as any).distance).toBeInstanceOf(Float64Array);
// Further assertions depending on the method's visibility and effects
});
});

describe('getScaledSprite', () => {
it('returns a scaled version of the sprite', async () => {
const originalSprite = await Sprite.fromIcons(fakeIcons, 2, 5);
originalSprite.calcSDF();
const scaledSprite = originalSprite.getScaledSprite(2);

expect(scaledSprite).toBeInstanceOf(Sprite);
expect((scaledSprite as any).width).toBe((originalSprite as any).width / 2);
expect((scaledSprite as any).height).toBe((originalSprite as any).height / 2);
// Additional assertions to verify scaling logic
});
});

describe('saveToTar', () => {
it('adds PNG and JSON entries to the tar pack', async () => {
// Create a mock for the tarPack with an entry method
const tarPackMock = {
entry: jest.fn<Pack['entry']>(),
};

// Create an instance of Sprite (or mock it if necessary)
const sprite = await Sprite.fromIcons(fakeIcons, 2, 5);

const buffer1 = Buffer.from('fake png data');
const buffer2 = Buffer.from('{"fake": "json data"}');

// Mock the getPng and getJSON methods to return fake data
jest.spyOn(sprite as any, 'getPng').mockResolvedValue(buffer1);
jest.spyOn(sprite as any, 'getJSON').mockResolvedValue(buffer2);

// Call the method under test
// @ts-expect-error too lazy
await sprite.saveToTar('testbasename', tarPackMock);

// Assertions to verify that tarPack.entry was called correctly
expect(tarPackMock.entry).toHaveBeenNthCalledWith(1, { name: 'testbasename.png' }, buffer1);
expect(tarPackMock.entry).toHaveBeenNthCalledWith(2, { name: 'testbasename.json' }, buffer2);

// Additional assertions can be made here, for example, checking the content of the buffers
// This might require you to inspect the calls to tarPack.entry more closely
});
});
describe('renderSDF', () => {
it('modifies the buffer based on distance values', async () => {
// Assuming you have a way to construct a Sprite with a custom buffer and distance for testing
const width = 2; // Example width
const height = 2; // Example height
const size = width * height;
const buffer = Buffer.alloc(size * 4).fill(0); // RGBA for each pixel, initialized to 0
const distance = Float64Array.from([1, 2, 3, 4]); // Example distance values

// Create a Sprite instance (or mock one) and set its properties for the test
const sprite = await Sprite.fromIcons(fakeIcons, 2, 5);
(sprite as any).width = width;
(sprite as any).height = height;
(sprite as any).buffer = buffer;
(sprite as any).distance = distance;

sprite.renderSDF(); // Method under test

// Now, assert that the buffer was updated correctly
// For simplicity, just check the alpha values (every 4th value starting from index 3)
expect(buffer[3]).toBe(170);
expect(buffer[7]).toBe(148);
// Add assertions for the rest of the pixels

// Optionally, validate that the RGB channels are set to 0 as specified by the function
expect(buffer[0]).toBe(0); // Red channel of first pixel
expect(buffer[1]).toBe(0); // Green channel of first pixel
expect(buffer[2]).toBe(0); // Blue channel of first pixel
// Repeat for other pixels as needed
});

it('throws an error if distance is not set', async () => {
const sprite = await Sprite.fromIcons(fakeIcons, 2, 5);
// Ensure 'distance' is unset or null
(sprite as any).distance = undefined;

expect(() => {
sprite.renderSDF();
}).toThrow();
});
});

describe('getPng', () => {
it('returns a PNG buffer', async () => {
// Setup
const sprite = await Sprite.fromIcons(fakeIcons, 2, 5);
// Mock sharp and optipng here

// @ts-expect-error too lazy
jest.mocked(fs.readFileSync).mockImplementationOnce(() => Buffer.from('hallo'));

// Execute
const result = await (sprite as any).getPng();

// Assert
expect(result).toBeInstanceOf(Buffer);
// Additional assertions to verify the buffer content
});
});

describe('getJSON', () => {
it('returns a JSON buffer representing the sprite entries', async () => {
// Setup
const sprite = await Sprite.fromIcons(fakeIcons, 2, 5);

// Execute
const result = await (sprite as any).getJSON() as Buffer;
const expectedData = {
icon1: { width: 148, height: 148, x: 0, y: 0, pixelRatio: 2, sdf: true },
icon2: { width: 84, height: 84, x: 0, y: 148, pixelRatio: 2, sdf: true },
icon3: { width: 116, height: 116, x: 148, y: 0, pixelRatio: 2, sdf: true },
};

// Assert
expect(JSON.parse(result.toString())).toStrictEqual(expectedData);
});
});
});
15 changes: 13 additions & 2 deletions scripts/lib/sprites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,13 @@ export class Sprite {

private async getPng(): Promise<Buffer> {
if (this.bufferPng) return this.bufferPng;

const pngBuffer = await sharp(this.buffer, { raw: { width: this.width, height: this.height, channels: 4 } })
.png({ palette: false })
.toBuffer();

this.bufferPng = optipng(pngBuffer);

return this.bufferPng;
}

Expand Down Expand Up @@ -278,11 +281,19 @@ interface SpriteEntry {
pixelRatio: number;
}

function optipng(bufferIn: Buffer): Buffer {
export function optipng(bufferIn: Buffer): Buffer {
const randomString = Math.random().toString(36).replace(/[^a-z0-9]/g, '');
const filename = resolve(tmpdir(), randomString + '.png');

writeFileSync(filename, bufferIn);
spawnSync('optipng', [filename]);

const result = spawnSync('optipng', [filename]);

if (result.status === 1) {
console.log(result.stderr.toString());
throw Error();
}

const bufferOut = readFileSync(filename);
rmSync(filename);
return bufferOut;
Expand Down

0 comments on commit 8408035

Please sign in to comment.