/**
* Tests for foreground generator.
*
* Tests SVG, text, and image foreground generation.
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ForegroundConfig } from '../../types'
import { resetMocks } from '../setup'
// Hoisted mock for fs readFile
const mockReadFile = vi.hoisted(() => vi.fn())
// Mock node:fs/promises
vi.mock('node:fs/promises', () => ({
readFile: mockReadFile,
}))
// Mock font loader
vi.mock('../../utils/font_loader', () => ({
loadGoogleFont: vi.fn().mockResolvedValue(Buffer.from('mock-font-data')),
loadSystemFont: vi.fn().mockResolvedValue(null),
}))
describe('ForegroundGenerator', () => {
beforeEach(() => {
resetMocks()
vi.resetModules()
})
describe('generateForeground', () => {
describe('SVG foregrounds', () => {
it('should generate SVG foreground', async () => {
// GIVEN an SVG file with content
const svgContent =
''
mockReadFile.mockResolvedValue(svgContent)
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'svg',
svgPath: '/path/to/icon.svg',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 100, 100)
// THEN buffer should be returned
expect(buffer).toBeInstanceOf(Buffer)
expect(sharp).toHaveBeenCalled()
})
it('should apply color override to SVG', async () => {
// GIVEN an SVG with multiple fill colors
const svgContent =
''
mockReadFile.mockResolvedValue(svgContent)
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'svg',
svgPath: '/path/to/icon.svg',
color: '#0000FF',
}
// WHEN generating the foreground with color override
await generateForeground(config, 100, 100)
// THEN all fills should be replaced with override color
const svgBuffer = sharp.lastInput as Buffer
const modifiedSvg = svgBuffer.toString()
expect(modifiedSvg).toContain('fill="#0000FF"')
expect(modifiedSvg).not.toContain('fill="#FF0000"')
expect(modifiedSvg).not.toContain('fill="#00FF00"')
})
it('should not modify SVG when no color override', async () => {
// GIVEN an SVG with original fill color
const svgContent = ''
mockReadFile.mockResolvedValue(svgContent)
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'svg',
svgPath: '/path/to/icon.svg',
}
// WHEN generating the foreground without color override
await generateForeground(config, 100, 100)
// THEN original fill color should be preserved
const svgBuffer = sharp.lastInput as Buffer
const modifiedSvg = svgBuffer.toString()
expect(modifiedSvg).toContain('fill="#FF0000"')
})
it('should resize with contain fit mode', async () => {
// GIVEN an SVG file
const svgContent = ''
mockReadFile.mockResolvedValue(svgContent)
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'svg',
svgPath: '/path/to/icon.svg',
}
// WHEN generating the foreground at specific dimensions
await generateForeground(config, 200, 100)
// THEN image should be resized with contain fit and transparent background
expect(sharp.mockInstance.resize).toHaveBeenCalledWith(200, 100, {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
})
})
describe('text foregrounds', () => {
it('should generate text foreground with Google Font', async () => {
// GIVEN a text config with Google font
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 100, 100)
// THEN SVG with text path should be created
expect(buffer).toBeInstanceOf(Buffer)
const svgBuffer = sharp.lastInput as Buffer
const svgContent = svgBuffer.toString()
expect(svgContent).toContain(' {
// GIVEN a text config without fontSize specified
const _sharp = (await import('sharp')).default as any
const opentype = (await import('opentype.js')).default
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating at 100x100
await generateForeground(config, 100, 100)
// THEN font size should default to 60% of height (60)
expect(opentype.parse).toHaveBeenCalled()
const mockFont = opentype.parse(new ArrayBuffer(0))
expect(mockFont.getPath).toHaveBeenCalledWith('A', 0, 0, 60)
})
it('should use custom font size when provided', async () => {
// GIVEN a text config with custom fontSize
const opentype = (await import('opentype.js')).default
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
fontSize: 48,
}
// WHEN generating the foreground
await generateForeground(config, 100, 100)
// THEN custom font size should be used
const mockFont = opentype.parse(new ArrayBuffer(0))
expect(mockFont.getPath).toHaveBeenCalledWith('A', 0, 0, 48)
})
it('should handle system fonts with fallback', async () => {
// GIVEN a system font that returns null
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Arial',
fontSource: 'system',
color: '#000000',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 100, 100)
// THEN placeholder rect should be rendered
expect(buffer).toBeInstanceOf(Buffer)
const svgBuffer = sharp.lastInput as Buffer
const svgContent = svgBuffer.toString()
expect(svgContent).toContain(' {
// GIVEN a custom font config with path
mockReadFile.mockResolvedValue(Buffer.from('font-data'))
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'CustomFont',
fontSource: 'custom',
fontPath: '/path/to/font.ttf',
color: '#FFFFFF',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 100, 100)
// THEN font should be loaded from specified path
expect(buffer).toBeInstanceOf(Buffer)
expect(mockReadFile).toHaveBeenCalledWith('/path/to/font.ttf')
})
it('should throw when custom font path is missing', async () => {
// GIVEN a custom font config without fontPath
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'CustomFont',
fontSource: 'custom',
color: '#FFFFFF',
}
// WHEN generating the foreground
// THEN it should throw font path required error
await expect(generateForeground(config, 100, 100)).rejects.toThrow(
'Font path is required for custom fonts',
)
})
it('should center text path on canvas', async () => {
// GIVEN a text config
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
await generateForeground(config, 200, 200)
// THEN SVG should have centering transform
const svgBuffer = sharp.lastInput as Buffer
const svgContent = svgBuffer.toString()
expect(svgContent).toContain('transform="translate(')
})
})
describe('image foregrounds', () => {
it('should generate image foreground', async () => {
// GIVEN an image foreground config with path
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'image',
imagePath: '/path/to/logo.png',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 100, 100)
// THEN image should be loaded from path
expect(buffer).toBeInstanceOf(Buffer)
expect(sharp).toHaveBeenCalledWith('/path/to/logo.png')
})
it('should resize with contain fit mode and transparent background', async () => {
// GIVEN an image foreground config
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'image',
imagePath: '/path/to/logo.png',
}
// WHEN generating the foreground at specific dimensions
await generateForeground(config, 200, 100)
// THEN image should be resized with contain fit and transparent background
expect(sharp.mockInstance.resize).toHaveBeenCalledWith(200, 100, {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
})
})
describe('unknown foreground type', () => {
it('should throw for unknown type', async () => {
// GIVEN an unknown foreground type
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config = {
type: 'unknown',
} as any
// WHEN generating the foreground
// THEN it should throw unknown type error
await expect(generateForeground(config, 100, 100)).rejects.toThrow(
'Unknown foreground type',
)
})
})
describe('output format', () => {
it('should output PNG format', async () => {
// GIVEN a valid foreground config
const svgContent = ''
mockReadFile.mockResolvedValue(svgContent)
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'svg',
svgPath: '/path/to/icon.svg',
}
// WHEN generating the foreground
await generateForeground(config, 100, 100)
// THEN output should be PNG format
expect(sharp.mockInstance.png).toHaveBeenCalled()
expect(sharp.mockInstance.toBuffer).toHaveBeenCalled()
})
})
})
describe('font source handling', () => {
it('should use loadGoogleFont for google source', async () => {
// GIVEN a text config with google font source
const { loadGoogleFont } = await import('../../utils/font_loader')
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
await generateForeground(config, 100, 100)
// THEN loadGoogleFont should be called with font family
expect(loadGoogleFont).toHaveBeenCalledWith('Roboto')
})
it('should use loadSystemFont for system source', async () => {
// GIVEN a text config with system font source
const { loadSystemFont } = await import('../../utils/font_loader')
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Arial',
fontSource: 'system',
color: '#FFFFFF',
}
// WHEN generating the foreground
await generateForeground(config, 100, 100)
// THEN loadSystemFont should be called with font family
expect(loadSystemFont).toHaveBeenCalledWith('Arial')
})
it('should throw for unknown font source', async () => {
// GIVEN a text config with unknown font source
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config = {
type: 'text',
text: 'A',
fontFamily: 'Test',
fontSource: 'unknown',
color: '#FFFFFF',
} as any
// WHEN generating the foreground
// THEN it should throw unknown font source error
await expect(generateForeground(config, 100, 100)).rejects.toThrow(
'Unknown font source',
)
})
})
describe('SVG path generation for text', () => {
it('should include path data in SVG', async () => {
// GIVEN a text config
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'AB',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FF0000',
}
// WHEN generating the foreground
await generateForeground(config, 100, 100)
// THEN SVG should contain path data from mock
const svgBuffer = sharp.lastInput as Buffer
const svgContent = svgBuffer.toString()
expect(svgContent).toContain('d="M0 0 L100 0 L100 50 L0 50 Z"')
})
it('should include canvas dimensions in SVG', async () => {
// GIVEN a text config
const sharp = (await import('sharp')).default as any
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'A',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground at 256x256
await generateForeground(config, 256, 256)
// THEN SVG should have matching dimensions
const svgBuffer = sharp.lastInput as Buffer
const svgContent = svgBuffer.toString()
expect(svgContent).toContain('width="256"')
expect(svgContent).toContain('height="256"')
})
it('should handle multi-character text', async () => {
// GIVEN a text config with multiple characters
const opentype = (await import('opentype.js')).default
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: 'ABC',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 200, 200)
// THEN buffer should be generated successfully
expect(buffer).toBeInstanceOf(Buffer)
// AND opentype should render all characters
const mockFont = opentype.parse(new ArrayBuffer(0))
expect(mockFont.getPath).toHaveBeenCalledWith(
'ABC',
0,
0,
expect.any(Number),
)
})
it('should handle unicode characters (CJK)', async () => {
// GIVEN a text config with Japanese characters
const opentype = (await import('opentype.js')).default
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: '日本',
fontFamily: 'Noto Sans JP',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 200, 200)
// THEN buffer should be generated successfully
expect(buffer).toBeInstanceOf(Buffer)
// AND opentype should render unicode text
const mockFont = opentype.parse(new ArrayBuffer(0))
expect(mockFont.getPath).toHaveBeenCalledWith(
'日本',
0,
0,
expect.any(Number),
)
})
it('should handle emoji characters', async () => {
// GIVEN a text config with emoji
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: '🚀',
fontFamily: 'Noto Color Emoji',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 200, 200)
// THEN buffer should be generated successfully
expect(buffer).toBeInstanceOf(Buffer)
})
it('should handle special characters', async () => {
// GIVEN a text config with special characters
const { generateForeground } = await import(
'../../generators/foreground_generator'
)
const config: ForegroundConfig = {
type: 'text',
text: '"\'&<>',
fontFamily: 'Roboto',
fontSource: 'google',
color: '#FFFFFF',
}
// WHEN generating the foreground
const buffer = await generateForeground(config, 200, 200)
// THEN buffer should be generated successfully
// AND special chars should be handled without errors
expect(buffer).toBeInstanceOf(Buffer)
})
})
})