diff --git a/scripts/lib/sprites.test.ts b/scripts/lib/sprites.test.ts index b5b9af7..f18b79a 100644 --- a/scripts/lib/sprites.test.ts +++ b/scripts/lib/sprites.test.ts @@ -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().mockReturnThis(), - raw: jest.fn().mockReturnThis(), - // @ts-expect-error to lazy - toBuffer: jest.fn<() => Promise>().mockResolvedValue(Buffer.from('test')), - })); - - const icons = [{ name: 'icon1', svg: '', size: 100 }]; - const sprite = await Sprite.fromIcons(icons, 1, 10); +const fakeIcons = [ + { + name: 'icon1', size: 64, svg: '', + }, + { + name: 'icon2', size: 32, svg: '', + }, + { + name: 'icon3', size: 48, 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')); @@ -54,7 +55,7 @@ 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'); @@ -62,13 +63,144 @@ describe('Sprite', () => { default: jest.fn(() => ({ width: 100, height: 100 })), })); - const icons = [{ name: 'icon1', 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(), + }; + + // 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); + }); + }); }); diff --git a/scripts/lib/sprites.ts b/scripts/lib/sprites.ts index 1321bed..7100a63 100644 --- a/scripts/lib/sprites.ts +++ b/scripts/lib/sprites.ts @@ -245,10 +245,13 @@ export class Sprite { private async getPng(): Promise { 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; } @@ -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;