From 539b34012abee21b223a5e0d03d22bd4d8e06852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Demir?= Date: Sat, 26 Apr 2025 02:14:46 +0300 Subject: [PATCH 1/5] fix: auto icons override default icons --- packages/auto-icons/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auto-icons/src/index.ts b/packages/auto-icons/src/index.ts index 61a766eac..9f658a6bf 100644 --- a/packages/auto-icons/src/index.ts +++ b/packages/auto-icons/src/index.ts @@ -32,7 +32,7 @@ export default defineWxtModule({ wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => { if (manifest.icons) - return wxt.logger.warn( + wxt.logger.warn( '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons', ); From 946f2f46a4730a2b78699c98e8d39ea42b1ea58f Mon Sep 17 00:00:00 2001 From: omerfardemir Date: Sun, 1 Jun 2025 00:40:26 +0300 Subject: [PATCH 2/5] feat: add auto-icons tests for icon generation --- packages/auto-icons/package.json | 3 +- .../auto-icons/src/__test__/index.test.ts | 300 ++++++++++++++++++ packages/auto-icons/vitest.config.ts | 8 + 3 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 packages/auto-icons/src/__test__/index.test.ts create mode 100644 packages/auto-icons/vitest.config.ts diff --git a/packages/auto-icons/package.json b/packages/auto-icons/package.json index 85a6e7fa5..9255deabe 100644 --- a/packages/auto-icons/package.json +++ b/packages/auto-icons/package.json @@ -39,7 +39,8 @@ ], "scripts": { "build": "buildc -- unbuild", - "check": "pnpm build && check" + "check": "pnpm build && check", + "test": "buildc --deps-only -- vitest" }, "peerDependencies": { "wxt": ">=0.19.0" diff --git a/packages/auto-icons/src/__test__/index.test.ts b/packages/auto-icons/src/__test__/index.test.ts new file mode 100644 index 000000000..3f01eb3cc --- /dev/null +++ b/packages/auto-icons/src/__test__/index.test.ts @@ -0,0 +1,300 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { resolve, relative } from 'node:path'; +import * as fsExtra from 'fs-extra'; +import defu from 'defu'; + +// Import the module definition type +import type { AutoIconsOptions } from '../index'; +import { UserManifest } from 'wxt'; + +// Mock dependencies +vi.mock('fs-extra', () => ({ + exists: vi.fn().mockResolvedValue(true), + ensureDir: vi.fn().mockResolvedValue(undefined), +})); + +// Mock process.cwd +vi.spyOn(process, 'cwd').mockReturnValue('/mock'); + +describe('auto-icons module', () => { + // Create a simple mock of the WXT object + const mockWxt = { + config: { + srcDir: '/mock/src', + outDir: '/mock/dist', + mode: 'development', + }, + logger: { + warn: vi.fn(), + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('options handling', () => { + it('should use default options when not provided', () => { + // Execute + const options: AutoIconsOptions = {}; + const parsedOptions = defu< + Required, + AutoIconsOptions[] + >(options, { + enabled: true, + baseIconPath: resolve('/mock/src', 'assets/icon.png'), + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }); + + // Verify + expect(parsedOptions.enabled).toBe(true); + expect(parsedOptions.baseIconPath).toBe( + resolve('/mock/src', 'assets/icon.png'), + ); + expect(parsedOptions.grayscaleOnDevelopment).toBe(true); + expect(parsedOptions.sizes).toEqual([128, 48, 32, 16]); + }); + + it('should merge custom options with defaults', () => { + // Execute + const options: AutoIconsOptions = { + sizes: [64, 32], + grayscaleOnDevelopment: false, + }; + const parsedOptions = defu< + Required, + AutoIconsOptions[] + >(options, { + enabled: true, + baseIconPath: resolve('/mock/src', 'assets/icon.png'), + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }); + + // Verify + expect(parsedOptions.enabled).toBe(true); // Default + expect(parsedOptions.baseIconPath).toBe( + resolve('/mock/src', 'assets/icon.png'), + ); // Default + expect(parsedOptions.grayscaleOnDevelopment).toBe(false); // Custom + expect(parsedOptions.sizes).toEqual([64, 32, 128, 48, 32, 16]); // Custom + }); + }); + + describe('error handling', () => { + it('should warn when disabled', () => { + // Setup + const parsedOptions: AutoIconsOptions = { + enabled: false, + baseIconPath: 'assets/icon.png', + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }; + + // Execute - simulate the module's logic + if (!parsedOptions.enabled) { + mockWxt.logger.warn(`\`[auto-icons]\` module-name disabled`); + } + + // Verify + expect(mockWxt.logger.warn).toHaveBeenCalledWith( + expect.stringContaining('disabled'), + ); + }); + + it('should warn when base icon not found', async () => { + // Setup + vi.mocked(fsExtra.exists).mockResolvedValue(); + const parsedOptions: AutoIconsOptions = { + enabled: true, + baseIconPath: 'assets/icon.png', + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }; + + const resolvedPath = resolve('/mock/src', parsedOptions.baseIconPath!); + + // Execute - simulate the module's logic + if (!(await fsExtra.exists(resolvedPath))) { + mockWxt.logger.warn( + `\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`, + ); + } + + // Verify + expect(mockWxt.logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Skipping icon generation'), + ); + }); + }); + + describe('manifest generation', () => { + it('should update manifest with icons', () => { + // Setup + const options: AutoIconsOptions = { + enabled: true, + baseIconPath: 'assets/icon.png', + grayscaleOnDevelopment: true, + sizes: [96], + }; + const parsedOptions = defu< + Required, + AutoIconsOptions[] + >(options, { + enabled: true, + baseIconPath: resolve('/mock/src', 'assets/icon.png'), + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }); + const manifest: UserManifest = { + icons: { + 128: 'icon/128.png', + 48: 'icon/48.png', + 32: 'icon/32.png', + }, + }; + + // Execute - simulate the build:manifestGenerated hook logic + manifest.icons = Object.fromEntries( + parsedOptions.sizes!.map((size) => [size, `icons/${size}.png`]), + ); + + // Verify + expect(manifest).toEqual({ + icons: { + '96': 'icons/96.png', + '128': 'icons/128.png', + '48': 'icons/48.png', + '32': 'icons/32.png', + '16': 'icons/16.png', + }, + }); + }); + + it('should warn when overwriting existing icons in manifest', () => { + const manifest: UserManifest = { + icons: { + 128: 'icon/128.png', + 48: 'icon/48.png', + 32: 'icon/32.png', + }, + }; + + // Execute - simulate the build:manifestGenerated hook logic + if (manifest.icons) { + mockWxt.logger.warn( + '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons', + ); + } + + // Verify + expect(mockWxt.logger.warn).toHaveBeenCalledWith( + expect.stringContaining('overwriting with auto-generated icons'), + ); + }); + }); + + describe('icon generation', () => { + it('should generate icons with correct sizes', async () => { + const options: AutoIconsOptions = { + enabled: true, + baseIconPath: 'assets/icon.png', + grayscaleOnDevelopment: true, + sizes: [96], + }; + const parsedOptions = defu< + Required, + AutoIconsOptions[] + >(options, { + enabled: true, + baseIconPath: resolve('/mock/src', 'assets/icon.png'), + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }); + + const mockOutput = { + publicAssets: [] as { type: string; fileName: string }[], + }; + + // Execute - simulate the logic without actually calling sharp + for (const size of parsedOptions.sizes!) { + await fsExtra.ensureDir(resolve(mockWxt.config.outDir, 'icons')); + + // Add to public assets + mockOutput.publicAssets.push({ + type: 'asset', + fileName: `icons/${size}.png`, + }); + } + + // Verify + expect(fsExtra.ensureDir).toHaveBeenCalledWith( + resolve('/mock/dist', 'icons'), + ); + expect(mockOutput.publicAssets).toEqual([ + { type: 'asset', fileName: 'icons/96.png' }, + { type: 'asset', fileName: 'icons/128.png' }, + { type: 'asset', fileName: 'icons/48.png' }, + { type: 'asset', fileName: 'icons/32.png' }, + { type: 'asset', fileName: 'icons/16.png' }, + ]); + }); + + it('should apply grayscale in development mode but not in production', () => { + // Setup + const parsedOptions: AutoIconsOptions = { + enabled: true, + baseIconPath: 'assets/icon.png', + grayscaleOnDevelopment: true, + }; + + // Test development mode + const devMode = 'development'; + const shouldApplyGrayscale = + devMode === 'development' && parsedOptions.grayscaleOnDevelopment; + expect(shouldApplyGrayscale).toBe(true); + + // Test production mode + const prodMode = 'production'; + const shouldNotApplyGrayscale = prodMode === 'production'; + expect(shouldNotApplyGrayscale).toBe(true); + }); + }); + + describe('public paths', () => { + it('should add icon paths to public paths', () => { + const options: AutoIconsOptions = { + enabled: true, + baseIconPath: 'assets/icon.png', + grayscaleOnDevelopment: true, + sizes: [96], + }; + const parsedOptions = defu< + Required, + AutoIconsOptions[] + >(options, { + enabled: true, + baseIconPath: resolve('/mock/src', 'assets/icon.png'), + grayscaleOnDevelopment: true, + sizes: [128, 48, 32, 16], + }); + + const paths: string[] = []; + + // Execute - simulate the prepare:publicPaths hook logic + for (const size of parsedOptions.sizes!) { + paths.push(`icons/${size}.png`); + } + + // Verify + expect(paths).toEqual([ + 'icons/96.png', + 'icons/128.png', + 'icons/48.png', + 'icons/32.png', + 'icons/16.png', + ]); + }); + }); +}); diff --git a/packages/auto-icons/vitest.config.ts b/packages/auto-icons/vitest.config.ts new file mode 100644 index 000000000..d6d6dd072 --- /dev/null +++ b/packages/auto-icons/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + mockReset: true, + restoreMocks: true, + }, +}); From 148465a995d55c2eca4d81bc0ca24f68d9aedd8f Mon Sep 17 00:00:00 2001 From: Florian Metz Date: Thu, 31 Jul 2025 01:01:13 +0200 Subject: [PATCH 3/5] feat(tests): enhance auto-icons tests with module setup and error handling --- .../auto-icons/src/__test__/index.test.ts | 666 +++++++++++++----- 1 file changed, 493 insertions(+), 173 deletions(-) diff --git a/packages/auto-icons/src/__test__/index.test.ts b/packages/auto-icons/src/__test__/index.test.ts index 3f01eb3cc..c6d1e791c 100644 --- a/packages/auto-icons/src/__test__/index.test.ts +++ b/packages/auto-icons/src/__test__/index.test.ts @@ -1,24 +1,49 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { resolve, relative } from 'node:path'; +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { resolve } from 'node:path'; import * as fsExtra from 'fs-extra'; -import defu from 'defu'; +import sharp from 'sharp'; +import type { Wxt, UserManifest } from 'wxt'; -// Import the module definition type +// Import the actual module +import autoIconsModule from '../index'; import type { AutoIconsOptions } from '../index'; -import { UserManifest } from 'wxt'; // Mock dependencies vi.mock('fs-extra', () => ({ - exists: vi.fn().mockResolvedValue(true), - ensureDir: vi.fn().mockResolvedValue(undefined), + exists: vi.fn(), + ensureDir: vi.fn(), })); -// Mock process.cwd -vi.spyOn(process, 'cwd').mockReturnValue('/mock'); +vi.mock('sharp', () => ({ + default: vi.fn(), +})); + +// Type definitions for better type safety +interface MockWxt { + config: { + srcDir: string; + outDir: string; + mode: 'development' | 'production'; + }; + logger: { + warn: Mock; + }; + hooks: { + hook: Mock; + }; +} + +interface PublicAsset { + type: string; + fileName: string; +} + +interface BuildOutput { + publicAssets: PublicAsset[]; +} describe('auto-icons module', () => { - // Create a simple mock of the WXT object - const mockWxt = { + const mockWxt: MockWxt = { config: { srcDir: '/mock/src', outDir: '/mock/dist', @@ -27,213 +52,291 @@ describe('auto-icons module', () => { logger: { warn: vi.fn(), }, + hooks: { + hook: vi.fn(), + }, + }; + + const createMockSharpInstance = () => { + const instance = { + png: vi.fn(), + grayscale: vi.fn(), + resize: vi.fn(), + toFile: vi.fn().mockResolvedValue(undefined), + }; + + // Make methods chainable + instance.png.mockReturnValue(instance); + instance.grayscale.mockReturnValue(instance); + instance.resize.mockImplementation(() => { + // Create a new instance for each resize to simulate real sharp behavior + const resizedInstance = { ...instance }; + resizedInstance.toFile = vi.fn().mockResolvedValue(undefined); + return resizedInstance; + }); + + return instance; }; + let mockSharpInstance: ReturnType; + beforeEach(() => { vi.clearAllMocks(); + mockSharpInstance = createMockSharpInstance(); + vi.mocked(sharp).mockReturnValue( + mockSharpInstance as unknown as sharp.Sharp, + ); + vi.mocked(fsExtra.exists).mockResolvedValue(true as any); + vi.mocked(fsExtra.ensureDir).mockResolvedValue(undefined as any); + }); + + describe('module setup', () => { + it('should have correct module metadata', () => { + expect(autoIconsModule.name).toBe('@wxt-dev/auto-icons'); + expect(autoIconsModule.configKey).toBe('autoIcons'); + expect(typeof autoIconsModule.setup).toBe('function'); + }); }); describe('options handling', () => { - it('should use default options when not provided', () => { - // Execute + it('should use default options when not provided', async () => { const options: AutoIconsOptions = {}; - const parsedOptions = defu< - Required, - AutoIconsOptions[] - >(options, { - enabled: true, - baseIconPath: resolve('/mock/src', 'assets/icon.png'), - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], - }); - // Verify - expect(parsedOptions.enabled).toBe(true); - expect(parsedOptions.baseIconPath).toBe( - resolve('/mock/src', 'assets/icon.png'), + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + // Verify that the module was set up (hooks were registered) + expect(mockWxt.hooks.hook).toHaveBeenCalledWith( + 'build:manifestGenerated', + expect.any(Function), + ); + expect(mockWxt.hooks.hook).toHaveBeenCalledWith( + 'build:done', + expect.any(Function), + ); + expect(mockWxt.hooks.hook).toHaveBeenCalledWith( + 'prepare:publicPaths', + expect.any(Function), ); - expect(parsedOptions.grayscaleOnDevelopment).toBe(true); - expect(parsedOptions.sizes).toEqual([128, 48, 32, 16]); }); - it('should merge custom options with defaults', () => { - // Execute + it('should merge custom options with defaults', async () => { const options: AutoIconsOptions = { sizes: [64, 32], grayscaleOnDevelopment: false, }; - const parsedOptions = defu< - Required, - AutoIconsOptions[] - >(options, { - enabled: true, - baseIconPath: resolve('/mock/src', 'assets/icon.png'), - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], - }); - // Verify - expect(parsedOptions.enabled).toBe(true); // Default - expect(parsedOptions.baseIconPath).toBe( - resolve('/mock/src', 'assets/icon.png'), - ); // Default - expect(parsedOptions.grayscaleOnDevelopment).toBe(false); // Custom - expect(parsedOptions.sizes).toEqual([64, 32, 128, 48, 32, 16]); // Custom + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + // Verify that the module was set up with custom options + expect(mockWxt.hooks.hook).toHaveBeenCalledWith( + 'build:manifestGenerated', + expect.any(Function), + ); + expect(mockWxt.hooks.hook).toHaveBeenCalledWith( + 'build:done', + expect.any(Function), + ); + expect(mockWxt.hooks.hook).toHaveBeenCalledWith( + 'prepare:publicPaths', + expect.any(Function), + ); }); }); describe('error handling', () => { - it('should warn when disabled', () => { - // Setup - const parsedOptions: AutoIconsOptions = { + it('should warn when disabled', async () => { + const options: AutoIconsOptions = { enabled: false, - baseIconPath: 'assets/icon.png', - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], }; - // Execute - simulate the module's logic - if (!parsedOptions.enabled) { - mockWxt.logger.warn(`\`[auto-icons]\` module-name disabled`); - } + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); - // Verify expect(mockWxt.logger.warn).toHaveBeenCalledWith( - expect.stringContaining('disabled'), + '`[auto-icons]` @wxt-dev/auto-icons disabled', ); + expect(mockWxt.hooks.hook).not.toHaveBeenCalled(); }); it('should warn when base icon not found', async () => { - // Setup - vi.mocked(fsExtra.exists).mockResolvedValue(); - const parsedOptions: AutoIconsOptions = { + vi.mocked(fsExtra.exists).mockResolvedValue(false as any); + + const options: AutoIconsOptions = { enabled: true, - baseIconPath: 'assets/icon.png', - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], + baseIconPath: 'assets/missing-icon.png', }; - const resolvedPath = resolve('/mock/src', parsedOptions.baseIconPath!); - - // Execute - simulate the module's logic - if (!(await fsExtra.exists(resolvedPath))) { - mockWxt.logger.warn( - `\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`, - ); - } + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); - // Verify expect(mockWxt.logger.warn).toHaveBeenCalledWith( - expect.stringContaining('Skipping icon generation'), + expect.stringContaining( + 'Skipping icon generation, no base icon found at', + ), ); + expect(mockWxt.hooks.hook).not.toHaveBeenCalled(); }); }); - describe('manifest generation', () => { - it('should update manifest with icons', () => { - // Setup + describe('manifest generation hook', () => { + it('should update manifest with default icons when no custom sizes provided', async () => { const options: AutoIconsOptions = { enabled: true, - baseIconPath: 'assets/icon.png', - grayscaleOnDevelopment: true, - sizes: [96], }; - const parsedOptions = defu< - Required, - AutoIconsOptions[] - >(options, { - enabled: true, - baseIconPath: resolve('/mock/src', 'assets/icon.png'), - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const manifestHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1]; + + expect(manifestHook).toBeDefined(); + + const manifest: UserManifest = {}; + if (manifestHook) { + await manifestHook(mockWxt as unknown as Wxt, manifest); + } + + // Should use default sizes: [128, 48, 32, 16] + expect(manifest.icons).toEqual({ + 128: 'icons/128.png', + 48: 'icons/48.png', + 32: 'icons/32.png', + 16: 'icons/16.png', }); - const manifest: UserManifest = { - icons: { - 128: 'icon/128.png', - 48: 'icon/48.png', - 32: 'icon/32.png', - }, + }); + + it('should merge custom sizes with defaults', async () => { + const options: AutoIconsOptions = { + enabled: true, + sizes: [96, 64], // These will be merged with defaults }; - // Execute - simulate the build:manifestGenerated hook logic - manifest.icons = Object.fromEntries( - parsedOptions.sizes!.map((size) => [size, `icons/${size}.png`]), - ); + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); - // Verify - expect(manifest).toEqual({ - icons: { - '96': 'icons/96.png', - '128': 'icons/128.png', - '48': 'icons/48.png', - '32': 'icons/32.png', - '16': 'icons/16.png', - }, + const manifestHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1]; + + expect(manifestHook).toBeDefined(); + + const manifest: UserManifest = {}; + if (manifestHook) { + await manifestHook(mockWxt as unknown as Wxt, manifest); + } + + // defu merges arrays, so we get both custom and default sizes + expect(manifest.icons).toEqual({ + 96: 'icons/96.png', + 64: 'icons/64.png', + 128: 'icons/128.png', + 48: 'icons/48.png', + 32: 'icons/32.png', + 16: 'icons/16.png', }); }); - it('should warn when overwriting existing icons in manifest', () => { + it('should warn when overwriting existing icons in manifest', async () => { + const options: AutoIconsOptions = { + enabled: true, + }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const manifestHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1]; + const manifest: UserManifest = { icons: { - 128: 'icon/128.png', - 48: 'icon/48.png', - 32: 'icon/32.png', + 128: 'existing-icon.png', }, }; - // Execute - simulate the build:manifestGenerated hook logic - if (manifest.icons) { - mockWxt.logger.warn( - '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons', - ); + if (manifestHook) { + await manifestHook(mockWxt as unknown as Wxt, manifest); } - // Verify expect(mockWxt.logger.warn).toHaveBeenCalledWith( - expect.stringContaining('overwriting with auto-generated icons'), + '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons', ); }); }); - describe('icon generation', () => { - it('should generate icons with correct sizes', async () => { + describe('icon generation hook', () => { + it('should generate icons with default sizes', async () => { const options: AutoIconsOptions = { enabled: true, - baseIconPath: 'assets/icon.png', - grayscaleOnDevelopment: true, - sizes: [96], }; - const parsedOptions = defu< - Required, - AutoIconsOptions[] - >(options, { - enabled: true, - baseIconPath: resolve('/mock/src', 'assets/icon.png'), - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], - }); - const mockOutput = { - publicAssets: [] as { type: string; fileName: string }[], + const output: BuildOutput = { + publicAssets: [], }; - // Execute - simulate the logic without actually calling sharp - for (const size of parsedOptions.sizes!) { - await fsExtra.ensureDir(resolve(mockWxt.config.outDir, 'icons')); + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); - // Add to public assets - mockOutput.publicAssets.push({ - type: 'asset', - fileName: `icons/${size}.png`, - }); + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + expect(buildHook).toBeDefined(); + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); } - // Verify + expect(sharp).toHaveBeenCalledWith( + resolve('/mock/src', 'assets/icon.png'), + ); + expect(mockSharpInstance.png).toHaveBeenCalled(); + + // Should resize to default sizes + expect(mockSharpInstance.resize).toHaveBeenCalledWith(128); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(48); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(32); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(16); + expect(fsExtra.ensureDir).toHaveBeenCalledWith( resolve('/mock/dist', 'icons'), ); - expect(mockOutput.publicAssets).toEqual([ + + expect(output.publicAssets).toEqual([ + { type: 'asset', fileName: 'icons/128.png' }, + { type: 'asset', fileName: 'icons/48.png' }, + { type: 'asset', fileName: 'icons/32.png' }, + { type: 'asset', fileName: 'icons/16.png' }, + ]); + }); + + it('should generate icons with custom sizes merged with defaults', async () => { + const options: AutoIconsOptions = { + enabled: true, + sizes: [96, 64], + }; + + const output: BuildOutput = { + publicAssets: [], + }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + expect(buildHook).toBeDefined(); + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); + } + + // Should include both custom and default sizes + expect(mockSharpInstance.resize).toHaveBeenCalledWith(96); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(64); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(128); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(48); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(32); + expect(mockSharpInstance.resize).toHaveBeenCalledWith(16); + + expect(output.publicAssets).toEqual([ { type: 'asset', fileName: 'icons/96.png' }, + { type: 'asset', fileName: 'icons/64.png' }, { type: 'asset', fileName: 'icons/128.png' }, { type: 'asset', fileName: 'icons/48.png' }, { type: 'asset', fileName: 'icons/32.png' }, @@ -241,53 +344,265 @@ describe('auto-icons module', () => { ]); }); - it('should apply grayscale in development mode but not in production', () => { - // Setup - const parsedOptions: AutoIconsOptions = { + it('should apply grayscale in development mode', async () => { + const options: AutoIconsOptions = { enabled: true, - baseIconPath: 'assets/icon.png', grayscaleOnDevelopment: true, + sizes: [128], }; - // Test development mode - const devMode = 'development'; - const shouldApplyGrayscale = - devMode === 'development' && parsedOptions.grayscaleOnDevelopment; - expect(shouldApplyGrayscale).toBe(true); + const output: BuildOutput = { publicAssets: [] }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); - // Test production mode - const prodMode = 'production'; - const shouldNotApplyGrayscale = prodMode === 'production'; - expect(shouldNotApplyGrayscale).toBe(true); + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); + } + + expect(mockSharpInstance.grayscale).toHaveBeenCalled(); }); - }); - describe('public paths', () => { - it('should add icon paths to public paths', () => { + it('should not apply grayscale in production mode', async () => { + const prodMockWxt = { + ...mockWxt, + config: { + ...mockWxt.config, + mode: 'production' as const, + }, + }; + const options: AutoIconsOptions = { enabled: true, - baseIconPath: 'assets/icon.png', grayscaleOnDevelopment: true, - sizes: [96], + sizes: [128], }; - const parsedOptions = defu< - Required, - AutoIconsOptions[] - >(options, { + + const output: BuildOutput = { publicAssets: [] }; + + await autoIconsModule.setup!(prodMockWxt as unknown as Wxt, options); + + const buildHook = vi + .mocked(prodMockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + if (buildHook) { + await buildHook(prodMockWxt as unknown as Wxt, output); + } + + expect(mockSharpInstance.grayscale).not.toHaveBeenCalled(); + }); + + it('should not apply grayscale when disabled', async () => { + const options: AutoIconsOptions = { enabled: true, - baseIconPath: resolve('/mock/src', 'assets/icon.png'), - grayscaleOnDevelopment: true, - sizes: [128, 48, 32, 16], + grayscaleOnDevelopment: false, + sizes: [128], + }; + + const output: BuildOutput = { publicAssets: [] }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); + } + + expect(mockSharpInstance.grayscale).not.toHaveBeenCalled(); + }); + }); + + describe('public paths hook', () => { + it('should add default icon paths to public paths', async () => { + const options: AutoIconsOptions = { + enabled: true, + }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const pathsHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1]; + + expect(pathsHook).toBeDefined(); + + const paths: string[] = []; + if (pathsHook) { + pathsHook(mockWxt as unknown as Wxt, paths); + } + + expect(paths).toEqual([ + 'icons/128.png', + 'icons/48.png', + 'icons/32.png', + 'icons/16.png', + ]); + }); + }); + + describe('edge cases and error handling', () => { + it('should handle empty sizes array', async () => { + const options: AutoIconsOptions = { + enabled: true, + sizes: [], // Empty array should still merge with defaults + }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const manifestHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1]; + + const manifest: UserManifest = {}; + if (manifestHook) { + await manifestHook(mockWxt as unknown as Wxt, manifest); + } + + // Should still have default sizes due to defu merge + expect(manifest.icons).toEqual({ + 128: 'icons/128.png', + 48: 'icons/48.png', + 32: 'icons/32.png', + 16: 'icons/16.png', }); + }); + it('should handle sharp processing errors gracefully', async () => { + const options: AutoIconsOptions = { + enabled: true, + }; + + // Make toFile throw an error + mockSharpInstance.resize = vi.fn().mockImplementation(() => ({ + toFile: vi.fn().mockRejectedValue(new Error('Sharp processing failed')), + })); + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + const output: BuildOutput = { publicAssets: [] }; + + // Should throw the sharp error + if (buildHook) { + await expect( + buildHook(mockWxt as unknown as Wxt, output), + ).rejects.toThrow('Sharp processing failed'); + } + }); + + it('should handle file system errors during directory creation', async () => { + const options: AutoIconsOptions = { + enabled: true, + }; + + // Make ensureDir throw an error + vi.mocked(fsExtra.ensureDir).mockRejectedValue( + new Error('Directory creation failed'), + ); + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + const output: BuildOutput = { publicAssets: [] }; + + // The module doesn't await ensureDir, so it won't throw + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); + // But ensureDir should have been called + expect(fsExtra.ensureDir).toHaveBeenCalled(); + } + }); + + it('should handle custom base icon path correctly', async () => { + const options: AutoIconsOptions = { + enabled: true, + baseIconPath: '/absolute/path/to/icon.png', + }; + + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + + const output: BuildOutput = { publicAssets: [] }; + + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); + } + + // Should use the absolute path directly + expect(sharp).toHaveBeenCalledWith('/absolute/path/to/icon.png'); + }); + }); + + describe('integration test', () => { + it('should handle full workflow correctly', async () => { + const options: AutoIconsOptions = { + enabled: true, + baseIconPath: 'assets/custom-icon.png', + sizes: [96], // Will be merged with defaults + grayscaleOnDevelopment: false, + }; + + const manifest: UserManifest = {}; + const output: BuildOutput = { publicAssets: [] }; const paths: string[] = []; - // Execute - simulate the prepare:publicPaths hook logic - for (const size of parsedOptions.sizes!) { - paths.push(`icons/${size}.png`); + // Setup the module + await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); + + // Execute all hooks + const manifestHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1]; + const buildHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'build:done')?.[1]; + const pathsHook = vi + .mocked(mockWxt.hooks.hook) + .mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1]; + + if (manifestHook) { + await manifestHook(mockWxt as unknown as Wxt, manifest); + } + if (buildHook) { + await buildHook(mockWxt as unknown as Wxt, output); } + if (pathsHook) { + pathsHook(mockWxt as unknown as Wxt, paths); + } + + // Verify results - defu merges arrays + expect(manifest.icons).toEqual({ + 96: 'icons/96.png', + 128: 'icons/128.png', + 48: 'icons/48.png', + 32: 'icons/32.png', + 16: 'icons/16.png', + }); + + expect(output.publicAssets).toEqual([ + { type: 'asset', fileName: 'icons/96.png' }, + { type: 'asset', fileName: 'icons/128.png' }, + { type: 'asset', fileName: 'icons/48.png' }, + { type: 'asset', fileName: 'icons/32.png' }, + { type: 'asset', fileName: 'icons/16.png' }, + ]); - // Verify expect(paths).toEqual([ 'icons/96.png', 'icons/128.png', @@ -295,6 +610,11 @@ describe('auto-icons module', () => { 'icons/32.png', 'icons/16.png', ]); + + expect(sharp).toHaveBeenCalledWith( + resolve('/mock/src', 'assets/custom-icon.png'), + ); + expect(mockSharpInstance.grayscale).not.toHaveBeenCalled(); }); }); }); From 93ef8ac6336641f6733ea03c71cce81e838166df Mon Sep 17 00:00:00 2001 From: Florian Metz Date: Thu, 31 Jul 2025 01:07:21 +0200 Subject: [PATCH 4/5] feat: add vitest as a dev dependency and update debug and tinyglobby versions --- packages/auto-icons/package.json | 1 + pnpm-lock.yaml | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/auto-icons/package.json b/packages/auto-icons/package.json index 9255deabe..2591637cf 100644 --- a/packages/auto-icons/package.json +++ b/packages/auto-icons/package.json @@ -56,6 +56,7 @@ "publint": "^0.3.12", "typescript": "^5.8.3", "unbuild": "^3.5.0", + "vitest": "^3.2.0", "wxt": "workspace:*" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53dd7d317..b0ae5a98b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: unbuild: specifier: ^3.5.0 version: 3.5.0(sass@1.89.1)(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) + vitest: + specifier: ^3.2.0 + version: 3.2.0(@types/debug@4.1.12)(@types/node@20.17.30)(happy-dom@17.5.6)(jiti@2.4.2)(sass@1.89.1)(tsx@4.19.4)(yaml@2.7.0) wxt: specifier: workspace:* version: link:../wxt @@ -5412,7 +5415,7 @@ snapshots: '@babel/traverse': 7.26.9 '@babel/types': 7.26.9 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5432,7 +5435,7 @@ snapshots: '@babel/traverse': 7.27.4 '@babel/types': 7.27.3 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5577,7 +5580,7 @@ snapshots: '@babel/parser': 7.26.9 '@babel/template': 7.26.9 '@babel/types': 7.26.9 - debug: 4.4.0 + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5589,7 +5592,7 @@ snapshots: '@babel/parser': 7.27.4 '@babel/template': 7.27.2 '@babel/types': 7.27.3 - debug: 4.4.0 + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5904,7 +5907,7 @@ snapshots: '@antfu/install-pkg': 1.0.0 '@antfu/utils': 8.1.1 '@iconify/types': 2.0.0 - debug: 4.4.0 + debug: 4.4.1 globals: 15.15.0 kolorist: 1.8.0 local-pkg: 1.1.1 @@ -6291,7 +6294,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.1.6)(vite@6.3.5(@types/node@20.17.30)(jiti@2.4.2)(sass@1.89.1)(tsx@4.19.4)(yaml@2.7.0)))(svelte@5.1.6)(vite@6.3.5(@types/node@20.17.30)(jiti@2.4.2)(sass@1.89.1)(tsx@4.19.4)(yaml@2.7.0))': dependencies: '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.1.6)(vite@6.3.5(@types/node@20.17.30)(jiti@2.4.2)(sass@1.89.1)(tsx@4.19.4)(yaml@2.7.0)) - debug: 4.4.0 + debug: 4.4.1 svelte: 5.1.6 vite: 6.3.5(@types/node@20.17.30)(jiti@2.4.2)(sass@1.89.1)(tsx@4.19.4)(yaml@2.7.0) transitivePeerDependencies: @@ -6470,7 +6473,7 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 perfect-debounce: 1.0.0 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 unplugin-utils: 0.2.4 '@unocss/config@66.0.0': @@ -6502,7 +6505,7 @@ snapshots: '@unocss/rule-utils': 66.0.0 css-tree: 3.1.0 postcss: 8.5.3 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 '@unocss/preset-attributify@66.0.0': dependencies: @@ -6586,7 +6589,7 @@ snapshots: '@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.8.3)) chokidar: 3.6.0 magic-string: 0.30.17 - tinyglobby: 0.2.12 + tinyglobby: 0.2.14 unplugin-utils: 0.2.4 vite: 6.3.5(@types/node@20.17.30)(jiti@2.4.2)(sass@1.89.1)(tsx@4.19.4)(yaml@2.7.0) transitivePeerDependencies: From 1146ac1469437b01924b2f18cdb035ac3cf3606d Mon Sep 17 00:00:00 2001 From: Florian Metz Date: Thu, 31 Jul 2025 01:12:29 +0200 Subject: [PATCH 5/5] feat(tests): update base icon path handling in auto-icons tests --- packages/auto-icons/src/__test__/index.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/auto-icons/src/__test__/index.test.ts b/packages/auto-icons/src/__test__/index.test.ts index c6d1e791c..a2692e1c7 100644 --- a/packages/auto-icons/src/__test__/index.test.ts +++ b/packages/auto-icons/src/__test__/index.test.ts @@ -527,9 +527,10 @@ describe('auto-icons module', () => { }); it('should handle custom base icon path correctly', async () => { + const customPath = 'custom/icon.png'; const options: AutoIconsOptions = { enabled: true, - baseIconPath: '/absolute/path/to/icon.png', + baseIconPath: customPath, }; await autoIconsModule.setup!(mockWxt as unknown as Wxt, options); @@ -544,8 +545,8 @@ describe('auto-icons module', () => { await buildHook(mockWxt as unknown as Wxt, output); } - // Should use the absolute path directly - expect(sharp).toHaveBeenCalledWith('/absolute/path/to/icon.png'); + // Should resolve the path relative to srcDir + expect(sharp).toHaveBeenCalledWith(resolve('/mock/src', customPath)); }); });