Spaces:
Sleeping
Sleeping
| import type { ThemeExtractionResult } from '@/schema/theme-extraction'; | |
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | |
| import { | |
| createBoundaryValueTheme, | |
| createInvalidColorTheme, | |
| createLowContrastTheme, | |
| createPerformanceTestTheme, | |
| createValidTheme, | |
| } from './test-helpers'; | |
| import { ThemeCustomizer } from './theme-customizer'; | |
| /** | |
| * 高コントラストテーマデータを生成 | |
| */ | |
| function createHighContrastTheme(): ThemeExtractionResult { | |
| const theme = createValidTheme(); | |
| return { | |
| ...theme, | |
| colors: { | |
| ...theme.colors, | |
| primary_text_color: '#000000', | |
| primary_background_color: '#ffffff', | |
| secondary_text_color: '#000000', | |
| secondary_background_color: '#ffffff', | |
| inverse_text_color: '#ffffff', | |
| tertiary_background_color: '#000000', | |
| }, | |
| }; | |
| } | |
| function extractVariableValue(css: string, variableName: string): string | null { | |
| const regex = new RegExp(`${variableName}:\s*([^;]+)`); | |
| const match = css.match(regex); | |
| return match ? match[1].trim() : null; | |
| } | |
| describe('ThemeCustomizer', () => { | |
| describe('初期化・設定', () => { | |
| describe('コンストラクタオプション', () => { | |
| it('should initialize with default options when no options provided', () => { | |
| // Arrange | |
| // (no specific arrangement needed) | |
| // Act | |
| const customizer = new ThemeCustomizer(); | |
| const stats = customizer.getCacheStats(); | |
| // Assert | |
| expect(stats.maxSize).toBe(100); | |
| }); | |
| it('should override default options with provided options', () => { | |
| // Arrange | |
| const options = { | |
| enableCache: false, | |
| validateColors: false, | |
| checkAccessibility: false, | |
| performanceMode: true, | |
| }; | |
| // Act | |
| const customizer = new ThemeCustomizer(options); | |
| const theme = createValidTheme(); | |
| const result = customizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result.cacheHit).toBe(false); // キャッシュ無効なので常にfalse | |
| }); | |
| describe.each([ | |
| [true, true, true, false], | |
| [false, false, false, true], | |
| [true, false, true, false], | |
| [false, true, false, true], | |
| ])( | |
| 'option combinations: cache=%s, validate=%s, accessibility=%s, performance=%s', | |
| (enableCache, validateColors, checkAccessibility, performanceMode) => { | |
| it('should work with given option combination', () => { | |
| // Arrange | |
| const options = { enableCache, validateColors, checkAccessibility, performanceMode }; | |
| const customizer = new ThemeCustomizer(options); | |
| const theme = createValidTheme(); | |
| // Act | |
| const result = customizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result).toBeDefined(); | |
| expect(result.fullCSS).toBeDefined(); | |
| }); | |
| }, | |
| ); | |
| }); | |
| }); | |
| describe('CSS生成機能', () => { | |
| let customizer: ThemeCustomizer; | |
| beforeEach(() => { | |
| customizer = new ThemeCustomizer({ | |
| enableCache: true, | |
| validateColors: true, | |
| checkAccessibility: true, | |
| }); | |
| }); | |
| describe('正常系', () => { | |
| it('should generate complete CSS with valid theme', () => { | |
| // Arrange | |
| const validTheme = createValidTheme(); | |
| // Act | |
| const result = customizer.generateThemedCSS(validTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(result.themeCSS).toBeDefined(); | |
| expect(result.baseCSS).toBeDefined(); | |
| expect(result.cacheHit).toBe(false); | |
| expect(result.generationTimeMs).toBeGreaterThan(0); | |
| }); | |
| it('should include extracted color variables in generated CSS', () => { | |
| // Arrange | |
| const validTheme = createValidTheme(); | |
| // Act | |
| const result = customizer.generateThemedCSS(validTheme); | |
| // Assert | |
| expect(extractVariableValue(result.themeCSS, '--extracted-primary-color')).toBe(validTheme.colors.primary_color); | |
| expect(extractVariableValue(result.themeCSS, '--extracted-secondary-color')).toBe(validTheme.colors.secondary_color); | |
| }); | |
| it('should include fallback values for theme variables', () => { | |
| // Arrange | |
| const validTheme = createValidTheme(); | |
| // Act | |
| const result = customizer.generateThemedCSS(validTheme); | |
| // Assert | |
| expect(result.themeCSS).toContain('var(--extracted-primary-color, #5a6c7d)'); | |
| expect(result.themeCSS).toContain('var(--extracted-primary-background-color, white)'); | |
| }); | |
| it('should include legacy color mapping variables', () => { | |
| // Arrange | |
| const validTheme = createValidTheme(); | |
| // Act | |
| const result = customizer.generateThemedCSS(validTheme); | |
| // Assert | |
| expect(result.themeCSS).toContain('--legacy-primary-text: var(--theme-primary-text)'); | |
| expect(result.themeCSS).toContain('--section-white-bg: var(--theme-primary-bg)'); | |
| }); | |
| }); | |
| describe('異常系', () => { | |
| it('should return fallback CSS when validation fails', () => { | |
| // Arrange | |
| const invalidTheme = createInvalidColorTheme(); | |
| const customizerWithValidation = new ThemeCustomizer({ validateColors: true }); | |
| // Act | |
| const result = customizerWithValidation.generateThemedCSS(invalidTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(result.themeCSS).toBe(''); // テーマCSSは空 | |
| expect(result.baseCSS).toBeDefined(); // ベースCSSは提供される | |
| }); | |
| it('should generate CSS with invalid colors when validation disabled', () => { | |
| // Arrange | |
| const invalidTheme = createInvalidColorTheme(); | |
| const customizerNoValidation = new ThemeCustomizer({ validateColors: false }); | |
| // Act | |
| const result = customizerNoValidation.generateThemedCSS(invalidTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(result.themeCSS).toContain('--extracted-primary-color: invalid-color'); | |
| }); | |
| }); | |
| describe('境界値・エッジケース', () => { | |
| it('should handle theme with empty color values', () => { | |
| // Arrange | |
| const themeWithEmptyColors = createValidTheme(); | |
| themeWithEmptyColors.colors.primary_color = ''; | |
| const customizerNoValidation = new ThemeCustomizer({ validateColors: false }); | |
| // Act | |
| const result = customizerNoValidation.generateThemedCSS(themeWithEmptyColors); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(result.themeCSS).toContain('--extracted-primary-color: '); | |
| }); | |
| it('should handle theme with all required color properties', () => { | |
| // Arrange | |
| const minimalTheme = createValidTheme(); | |
| // Act | |
| const result = customizer.generateThemedCSS(minimalTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(Object.keys(minimalTheme.colors)).toHaveLength(15); // 15色全て確認 | |
| }); | |
| }); | |
| }); | |
| describe('バリデーション機能', () => { | |
| let validator: ThemeCustomizer; | |
| beforeEach(() => { | |
| validator = new ThemeCustomizer({ | |
| validateColors: true, | |
| checkAccessibility: true, | |
| }); | |
| }); | |
| describe('カラーコード検証', () => { | |
| it('should accept valid HEX color codes', () => { | |
| // Arrange | |
| const validTheme = createValidTheme(); | |
| // Act | |
| const validation = validator.validateTheme(validTheme); | |
| // Assert | |
| expect(validation.isValid).toBe(true); | |
| expect(validation.errors).toHaveLength(0); | |
| }); | |
| describe.each([ | |
| ['#000000', true], | |
| ['#FFFFFF', true], | |
| ['#5a6c7d', true], | |
| ['#F59E0B', true], | |
| ['#ggg', false], | |
| ['rgb(255,0,0)', false], | |
| ['#ff', false], | |
| ['#1234567', false], | |
| ['red', false], | |
| ['', false], | |
| ['invalid-color', false], | |
| ['#gggggg', false], | |
| ])('color validation: %s should be %s', (color, isValid) => { | |
| it(`should ${isValid ? 'accept' : 'reject'} color: ${color}`, () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| theme.colors.primary_color = color; | |
| // Act | |
| const validation = validator.validateTheme(theme); | |
| // Assert | |
| expect(validation.isValid).toBe(isValid); | |
| if (!isValid) { | |
| expect(validation.errors.some((err) => err.includes('primary_color'))).toBe(true); | |
| } | |
| }); | |
| }); | |
| it('should detect multiple invalid color codes', () => { | |
| // Arrange | |
| const invalidTheme = createInvalidColorTheme(); | |
| // Act | |
| const validation = validator.validateTheme(invalidTheme); | |
| // Assert | |
| expect(validation.isValid).toBe(false); | |
| expect(validation.errors).toContain('primary_color: 無効なカラーコード形式 (invalid-color)'); | |
| expect(validation.errors).toContain('secondary_color: 無効なカラーコード形式 (#gggggg)'); | |
| }); | |
| }); | |
| describe('アクセシビリティ検証', () => { | |
| it('should not generate warnings for high contrast combinations', () => { | |
| // Arrange | |
| const highContrastTheme = createHighContrastTheme(); | |
| // Act | |
| const validation = validator.validateTheme(highContrastTheme); | |
| // Assert | |
| const contrastWarnings = validation.warnings.filter((w) => w.includes('コントラスト比')); | |
| expect(contrastWarnings).toHaveLength(0); | |
| }); | |
| it('should generate warnings for low contrast combinations', () => { | |
| // Arrange | |
| const lowContrastTheme = createLowContrastTheme(); | |
| // Act | |
| const validation = validator.validateTheme(lowContrastTheme); | |
| // Assert | |
| expect(validation.warnings.length).toBeGreaterThan(0); | |
| expect(validation.warnings.some((w) => w.includes('コントラスト比が低すぎます'))).toBe(true); | |
| }); | |
| it('should check multiple color combinations for accessibility', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| theme.colors.primary_text_color = '#dddddd'; | |
| theme.colors.primary_background_color = '#ffffff'; | |
| theme.colors.secondary_text_color = '#eeeeee'; | |
| theme.colors.secondary_background_color = '#ffffff'; | |
| // Act | |
| const validation = validator.validateTheme(theme); | |
| // Assert | |
| const contrastWarnings = validation.warnings.filter((w) => w.includes('コントラスト比')); | |
| expect(contrastWarnings.length).toBeGreaterThanOrEqual(1); | |
| }); | |
| it('should handle accessibility check errors gracefully', () => { | |
| // Arrange | |
| const themeWithInvalidColors = createValidTheme(); | |
| themeWithInvalidColors.colors.primary_text_color = 'invalid'; | |
| const validatorNoColorCheck = new ThemeCustomizer({ | |
| validateColors: false, | |
| checkAccessibility: true, | |
| }); | |
| // Act | |
| const validation = validatorNoColorCheck.validateTheme(themeWithInvalidColors); | |
| // Assert | |
| expect(validation).toBeDefined(); | |
| // エラーが発生してもvalidationオブジェクトは返される | |
| }); | |
| }); | |
| describe('複合検証', () => { | |
| it('should handle themes with both color and accessibility issues', () => { | |
| // Arrange | |
| const problematicTheme = createValidTheme(); | |
| problematicTheme.colors.primary_color = 'invalid'; | |
| problematicTheme.colors.primary_text_color = '#cccccc'; | |
| problematicTheme.colors.primary_background_color = '#ffffff'; | |
| // Act | |
| const validation = validator.validateTheme(problematicTheme); | |
| // Assert | |
| expect(validation.isValid).toBe(false); | |
| expect(validation.errors.length).toBeGreaterThan(0); | |
| expect(validation.warnings.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| }); | |
| describe('キャッシュ機能', () => { | |
| let cachedCustomizer: ThemeCustomizer; | |
| let nonCachedCustomizer: ThemeCustomizer; | |
| beforeEach(() => { | |
| cachedCustomizer = new ThemeCustomizer({ enableCache: true }); | |
| nonCachedCustomizer = new ThemeCustomizer({ enableCache: false }); | |
| }); | |
| describe('基本動作', () => { | |
| it('should cache result on first generation and hit cache on second', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| // Act | |
| const result1 = cachedCustomizer.generateThemedCSS(theme); | |
| const result2 = cachedCustomizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result1.cacheHit).toBe(false); | |
| expect(result2.cacheHit).toBe(true); | |
| expect(result2.fullCSS).toBe(result1.fullCSS); | |
| }); | |
| it('should not hit cache for different themes', () => { | |
| // Arrange | |
| const theme1 = createValidTheme(); | |
| const theme2 = createValidTheme(); | |
| theme2.colors.primary_color = '#ff0000'; | |
| // Act | |
| const result1 = cachedCustomizer.generateThemedCSS(theme1); | |
| const result2 = cachedCustomizer.generateThemedCSS(theme2); | |
| // Assert | |
| expect(result1.cacheHit).toBe(false); | |
| expect(result2.cacheHit).toBe(false); | |
| expect(result2.fullCSS).not.toBe(result1.fullCSS); | |
| }); | |
| it('should not use cache when caching is disabled', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| // Act | |
| const result1 = nonCachedCustomizer.generateThemedCSS(theme); | |
| const result2 = nonCachedCustomizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result1.cacheHit).toBe(false); | |
| expect(result2.cacheHit).toBe(false); | |
| }); | |
| it('should clear cache successfully', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| cachedCustomizer.generateThemedCSS(theme); | |
| // Act | |
| cachedCustomizer.clearCache(); | |
| const stats = cachedCustomizer.getCacheStats(); | |
| const result = cachedCustomizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(stats.size).toBe(0); | |
| expect(result.cacheHit).toBe(false); | |
| }); | |
| }); | |
| describe('TTL管理', () => { | |
| it('should respect cache TTL expiration', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| const shortTTLCustomizer = new ThemeCustomizer({ enableCache: true }); | |
| // TTLを短く設定するためにプライベートプロパティにアクセス | |
| // 理由: TTL期限切れロジックをテストするため、実行時間内でテスト可能な短い値に設定 | |
| // 代替手段: 依存性注入やテスト用設定があれば理想的だが、現在の設計では直接アクセスが最適 | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const originalTTL = (shortTTLCustomizer as any).CACHE_TTL_MS; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (shortTTLCustomizer as any).CACHE_TTL_MS = 1; // 1ms | |
| // Act | |
| const result1 = shortTTLCustomizer.generateThemedCSS(theme); | |
| // Wait for TTL expiration | |
| return new Promise((resolve) => { | |
| setTimeout(() => { | |
| const result2 = shortTTLCustomizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result1.cacheHit).toBe(false); | |
| expect(result2.cacheHit).toBe(false); // TTL切れでキャッシュミス | |
| // Restore original TTL | |
| // 理由: テスト後の状態復元のため、変更したプライベートプロパティを元に戻す | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (shortTTLCustomizer as any).CACHE_TTL_MS = originalTTL; | |
| resolve(undefined); | |
| }, 10); | |
| }); | |
| }); | |
| }); | |
| describe('サイズ制限', () => { | |
| it('should respect maximum cache size limit', () => { | |
| // Arrange | |
| const maxSize = 3; | |
| const limitedCustomizer = new ThemeCustomizer({ enableCache: true }); | |
| // 理由: キャッシュサイズ制限ロジックをテストするため、テスト用の小さな値に動的変更 | |
| // 代替手段: コンストラクタオプションで設定可能にする設計変更が理想的 | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (limitedCustomizer as any).MAX_CACHE_SIZE = maxSize; | |
| // Act - キャッシュサイズを超えるまでテーマを生成 | |
| const themes = []; | |
| for (let i = 0; i < maxSize + 2; i++) { | |
| const theme = createValidTheme(); | |
| theme.colors.primary_color = `#${i.toString().padStart(6, '0')}`; | |
| themes.push(theme); | |
| limitedCustomizer.generateThemedCSS(theme); | |
| } | |
| // Assert | |
| const stats = limitedCustomizer.getCacheStats(); | |
| expect(stats.size).toBeLessThanOrEqual(maxSize); | |
| expect(stats.maxSize).toBe(maxSize); | |
| // 最初のテーマはキャッシュから削除されているはず | |
| const firstThemeResult = limitedCustomizer.generateThemedCSS(themes[0]); | |
| expect(firstThemeResult.cacheHit).toBe(false); | |
| }); | |
| it('should provide accurate cache statistics', () => { | |
| // Arrange | |
| const theme1 = createValidTheme(); | |
| const theme2 = createValidTheme(); | |
| theme2.colors.primary_color = '#ff0000'; | |
| // Act | |
| cachedCustomizer.generateThemedCSS(theme1); | |
| cachedCustomizer.generateThemedCSS(theme2); | |
| const stats = cachedCustomizer.getCacheStats(); | |
| // Assert | |
| expect(stats.size).toBe(2); | |
| expect(stats.maxSize).toBe(100); | |
| }); | |
| }); | |
| }); | |
| describe('パフォーマンス', () => { | |
| let performanceCustomizer: ThemeCustomizer; | |
| beforeEach(() => { | |
| performanceCustomizer = new ThemeCustomizer({ | |
| enableCache: true, | |
| validateColors: true, | |
| checkAccessibility: true, | |
| }); | |
| }); | |
| it('should complete CSS generation within 100ms', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| // Act | |
| const result = performanceCustomizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result.generationTimeMs).toBeLessThan(100); | |
| expect(result.generationTimeMs).toBeGreaterThan(0); | |
| }); | |
| it('should process cache hits significantly faster', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| performanceCustomizer.generateThemedCSS(theme); // 初回実行でキャッシュ | |
| // Act | |
| const result = performanceCustomizer.generateThemedCSS(theme); | |
| // Assert | |
| expect(result.cacheHit).toBe(true); | |
| expect(result.generationTimeMs).toBeLessThan(10); // キャッシュヒットは10ms以内 | |
| }); | |
| it('should handle multiple themes efficiently', () => { | |
| // Arrange | |
| const themes = Array.from({ length: 10 }, (_, i) => { | |
| const theme = createValidTheme(); | |
| theme.colors.primary_color = `#${i.toString().padStart(6, '0')}`; | |
| return theme; | |
| }); | |
| // Act | |
| const startTime = performance.now(); | |
| const results = themes.map((theme) => performanceCustomizer.generateThemedCSS(theme)); | |
| const totalTime = performance.now() - startTime; | |
| // Assert | |
| expect(totalTime).toBeLessThan(500); // 10テーマで500ms以内 | |
| expect(results).toHaveLength(10); | |
| results.forEach((result) => { | |
| expect(result.fullCSS).toBeDefined(); | |
| }); | |
| }); | |
| }); | |
| describe('エラーハンドリング', () => { | |
| describe('バリデーションエラー', () => { | |
| it('should return fallback CSS when validation fails', () => { | |
| // Arrange | |
| const invalidTheme = createInvalidColorTheme(); | |
| const validatingCustomizer = new ThemeCustomizer({ validateColors: true }); | |
| // Act | |
| const result = validatingCustomizer.generateThemedCSS(invalidTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(result.themeCSS).toBe(''); // テーマCSSは空 | |
| expect(result.baseCSS).toBeDefined(); // ベースCSSは提供される | |
| expect(result.cacheHit).toBe(false); | |
| }); | |
| it('should generate CSS with invalid colors when validation disabled', () => { | |
| // Arrange | |
| const invalidTheme = createInvalidColorTheme(); | |
| const nonValidatingCustomizer = new ThemeCustomizer({ validateColors: false }); | |
| // Act | |
| const result = nonValidatingCustomizer.generateThemedCSS(invalidTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(result.themeCSS).toContain('--extracted-primary-color: invalid-color'); | |
| }); | |
| }); | |
| describe('実行時エラー', () => { | |
| it('should handle errors during CSS generation gracefully', () => { | |
| // Arrange | |
| const problematicTheme = createValidTheme(); | |
| const customizer = new ThemeCustomizer(); | |
| // Mock implementation to force an error | |
| // 理由: エラーハンドリングロジックをテストするため、意図的にエラーを発生させる必要 | |
| // 代替手段: 依存性注入があれば理想的だが、現在の設計では直接モックが最適 | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const originalMethod = (customizer as any).combineCSS; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (customizer as any).combineCSS = vi.fn(() => { | |
| throw new Error('CSS combination failed'); | |
| }); | |
| // Act | |
| const result = customizer.generateThemedCSS(problematicTheme); | |
| // Assert | |
| expect(result.fullCSS).toBeDefined(); // フォールバックが返される | |
| expect(result.themeCSS).toBe(''); | |
| // Restore original method | |
| // 理由: テスト後の状態復元のため、モックしたメソッドを元に戻す | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (customizer as any).combineCSS = originalMethod; | |
| }); | |
| }); | |
| describe('フォールバック動作', () => { | |
| it('should always return a usable CSS structure even on complete failure', () => { | |
| // Arrange | |
| const customizer = new ThemeCustomizer({ validateColors: true }); | |
| const extremelyInvalidTheme = { | |
| // 理由: 完全に無効な構造でのフォールバック動作をテストするため | |
| // 代替手段: 個別の無効プロパティテストも可能だが、最悪ケースのテストには全体無効化が必要 | |
| // 用途: validateTheme()でObject.entries()が空オブジェクトを処理する際のフォールバック確認 | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| colors: {} as any, // 完全に無効な構造 | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| design: {} as any, | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| brand: {} as any, | |
| analysis_notes: '', | |
| }; | |
| // Act | |
| const result = customizer.generateThemedCSS(extremelyInvalidTheme); | |
| // Assert | |
| expect(result).toBeDefined(); | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(typeof result.fullCSS).toBe('string'); | |
| expect(result.fullCSS.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| }); | |
| describe('デバッグ機能とユーティリティ', () => { | |
| let debugCustomizer: ThemeCustomizer; | |
| beforeEach(() => { | |
| debugCustomizer = new ThemeCustomizer({ | |
| enableCache: true, | |
| validateColors: true, | |
| checkAccessibility: true, | |
| }); | |
| }); | |
| describe('デバッグ情報提供', () => { | |
| it('should provide comprehensive debug information', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| // Act | |
| const debug = debugCustomizer.debugTheme(theme); | |
| // Assert | |
| expect(debug.validation).toBeDefined(); | |
| expect(debug.validation.isValid).toBeDefined(); | |
| expect(debug.cacheKey).toBeDefined(); | |
| expect(typeof debug.cacheKey).toBe('string'); | |
| expect(debug.generatedVariables).toBeDefined(); | |
| expect(debug.generatedVariables).toContain('--extracted-primary-color'); | |
| }); | |
| it('should provide debug info for invalid themes too', () => { | |
| // Arrange | |
| const invalidTheme = createInvalidColorTheme(); | |
| // Act | |
| const debug = debugCustomizer.debugTheme(invalidTheme); | |
| // Assert | |
| expect(debug.validation).toBeDefined(); | |
| expect(debug.validation.isValid).toBe(false); | |
| expect(debug.validation.errors.length).toBeGreaterThan(0); | |
| expect(debug.cacheKey).toBeDefined(); | |
| }); | |
| }); | |
| describe('キャッシュ統計', () => { | |
| it('should provide accurate cache statistics', () => { | |
| // Arrange | |
| const themes = [createValidTheme(), createHighContrastTheme(), createLowContrastTheme()]; | |
| // Act | |
| themes.forEach((theme) => debugCustomizer.generateThemedCSS(theme)); | |
| const stats = debugCustomizer.getCacheStats(); | |
| // Assert | |
| expect(stats.size).toBe(3); | |
| expect(stats.maxSize).toBeDefined(); | |
| expect(stats.maxSize).toBe(100); | |
| expect(typeof stats.size).toBe('number'); | |
| expect(typeof stats.maxSize).toBe('number'); | |
| }); | |
| it('should track cache size changes correctly', () => { | |
| // Arrange | |
| const theme = createValidTheme(); | |
| // Act & Assert | |
| const initialStats = debugCustomizer.getCacheStats(); | |
| expect(initialStats.size).toBe(0); | |
| debugCustomizer.generateThemedCSS(theme); | |
| const afterGenerationStats = debugCustomizer.getCacheStats(); | |
| expect(afterGenerationStats.size).toBe(1); | |
| debugCustomizer.clearCache(); | |
| const afterClearStats = debugCustomizer.getCacheStats(); | |
| expect(afterClearStats.size).toBe(0); | |
| }); | |
| }); | |
| describe('ユーティリティ関数のカバレッジ', () => { | |
| it('should handle hash generation for different inputs', () => { | |
| // Arrange | |
| const theme1 = createValidTheme(); | |
| const theme2 = createValidTheme(); | |
| theme2.colors.primary_color = '#ff0000'; | |
| // Act | |
| const debug1 = debugCustomizer.debugTheme(theme1); | |
| const debug2 = debugCustomizer.debugTheme(theme2); | |
| // Assert | |
| expect(debug1.cacheKey).not.toBe(debug2.cacheKey); | |
| expect(debug1.cacheKey).toBe(debugCustomizer.debugTheme(theme1).cacheKey); // 同じ入力で同じハッシュ | |
| }); | |
| it('should handle color utility functions with edge cases', () => { | |
| // Arrange | |
| const themeWithEdgeCaseColors = createValidTheme(); | |
| themeWithEdgeCaseColors.colors.primary_text_color = '#000000'; | |
| themeWithEdgeCaseColors.colors.primary_background_color = '#ffffff'; | |
| // Act | |
| const validation = debugCustomizer.validateTheme(themeWithEdgeCaseColors); | |
| // Assert | |
| expect(validation).toBeDefined(); | |
| // エッジケース(最高コントラスト)でも正常に動作する | |
| }); | |
| }); | |
| describe('プライベートメソッドのテスト(パブリックメソッド経由)', () => { | |
| it('should calculate luminance values correctly through contrast ratio check', () => { | |
| // Arrange - HEX色の輝度計算をテストするため、既知のコントラスト比を持つ色を使用 | |
| const blackOnWhiteTheme = createValidTheme(); | |
| blackOnWhiteTheme.colors.primary_text_color = '#000000'; | |
| blackOnWhiteTheme.colors.primary_background_color = '#ffffff'; | |
| const grayOnWhiteTheme = createValidTheme(); | |
| grayOnWhiteTheme.colors.primary_text_color = '#777777'; | |
| grayOnWhiteTheme.colors.primary_background_color = '#ffffff'; | |
| // Act | |
| const blackOnWhiteValidation = debugCustomizer.validateTheme(blackOnWhiteTheme); | |
| const grayOnWhiteValidation = debugCustomizer.validateTheme(grayOnWhiteTheme); | |
| // Assert - 黒/白は高コントラスト、グレー/白は低コントラスト | |
| const blackWarnings = blackOnWhiteValidation.warnings.filter((w) => w.includes('コントラスト比')); | |
| const grayWarnings = grayOnWhiteValidation.warnings.filter((w) => w.includes('コントラスト比')); | |
| expect(blackWarnings).toHaveLength(0); // 高コントラストなので警告なし | |
| expect(grayWarnings.length).toBeGreaterThan(0); // 低コントラストなので警告あり | |
| }); | |
| it('should convert HEX to RGB correctly through contrast calculation', () => { | |
| // Arrange - RGB変換をテストするため、様々なHEX値でテーマを作成 | |
| const themes = [ | |
| { hex: '#000000', expectHighContrast: true }, | |
| { hex: '#ffffff', expectLowContrast: true }, | |
| { hex: '#ff0000', expectMediumContrast: true }, | |
| { hex: '#cccccc', expectLowContrast: true }, | |
| ].map(({ hex, expectHighContrast, expectLowContrast, expectMediumContrast }) => { | |
| const theme = createValidTheme(); | |
| theme.colors.primary_text_color = hex; | |
| theme.colors.primary_background_color = '#ffffff'; | |
| return { theme, expectHighContrast, expectLowContrast, expectMediumContrast }; | |
| }); | |
| // Act & Assert | |
| themes.forEach(({ theme }) => { | |
| const validation = debugCustomizer.validateTheme(theme); | |
| // RGB変換が正常に動作していることを間接的に確認 | |
| // (hexToRgb/getLuminanceが正常でないとvalidateThemeが例外を投げる) | |
| expect(validation).toBeDefined(); | |
| expect(validation.isValid).toBeDefined(); | |
| }); | |
| }); | |
| it('should generate consistent hash values for identical inputs', () => { | |
| // Arrange - simpleHashメソッドのテスト | |
| const theme = createValidTheme(); | |
| // Act - 同じテーマで複数回ハッシュ生成 | |
| const debug1 = debugCustomizer.debugTheme(theme); | |
| const debug2 = debugCustomizer.debugTheme(theme); | |
| const debug3 = debugCustomizer.debugTheme(theme); | |
| // Assert - 同じ入力に対して常に同じハッシュが生成される | |
| expect(debug1.cacheKey).toBe(debug2.cacheKey); | |
| expect(debug2.cacheKey).toBe(debug3.cacheKey); | |
| expect(debug1.cacheKey.length).toBeGreaterThan(0); | |
| }); | |
| it('should generate different hash values for different inputs', () => { | |
| // Arrange - 異なるテーマでハッシュ値の違いをテスト | |
| const themes = Array.from({ length: 5 }, (_, i) => { | |
| const theme = createValidTheme(); | |
| theme.colors.primary_color = `#${i.toString().padStart(6, '0')}`; | |
| return theme; | |
| }); | |
| // Act | |
| const hashes = themes.map((theme) => debugCustomizer.debugTheme(theme).cacheKey); | |
| // Assert - 全て異なるハッシュ値が生成される | |
| const uniqueHashes = new Set(hashes); | |
| expect(uniqueHashes.size).toBe(themes.length); | |
| }); | |
| }); | |
| describe('境界値テストケース', () => { | |
| it('should handle boundary value theme data', () => { | |
| // Arrange | |
| const boundaryTheme = createBoundaryValueTheme(); | |
| // Act | |
| const result = debugCustomizer.generateThemedCSS(boundaryTheme); | |
| const validation = debugCustomizer.validateTheme(boundaryTheme); | |
| // Assert | |
| expect(result).toBeDefined(); | |
| expect(validation).toBeDefined(); | |
| // 境界値でも処理が完了する | |
| }); | |
| it('should handle performance stress test with large data', () => { | |
| // Arrange | |
| const performanceTheme = createPerformanceTestTheme(); | |
| const customizer = new ThemeCustomizer({ | |
| validateColors: false, // パフォーマンステストなのでバリデーション無効 | |
| checkAccessibility: false, | |
| }); | |
| // Act | |
| const startTime = performance.now(); | |
| const result = customizer.generateThemedCSS(performanceTheme); | |
| const endTime = performance.now(); | |
| // Assert | |
| expect(result).toBeDefined(); | |
| expect(result.fullCSS).toBeDefined(); | |
| expect(endTime - startTime).toBeLessThan(1000); // 大量データでも1秒以内 | |
| }); | |
| }); | |
| }); | |
| }); | |