From efb72beab8fd92fb93954214a80b463efe209e03 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Mon, 5 Jan 2026 09:27:50 +0000 Subject: [PATCH] Add test --- .../src/core/tests/iconValidation.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 frontend/src/core/tests/iconValidation.test.ts diff --git a/frontend/src/core/tests/iconValidation.test.ts b/frontend/src/core/tests/iconValidation.test.ts new file mode 100644 index 000000000..60056f764 --- /dev/null +++ b/frontend/src/core/tests/iconValidation.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('Icon validation', () => { + it('should only use icons that exist in the bundle', () => { + const usedIcons = new Set(); + const srcDir = path.join(__dirname, '..', '..'); + + // Load the icon bundle + const iconSetPath = path.join(srcDir, 'assets', 'material-symbols-icons.json'); + const iconSet = JSON.parse(fs.readFileSync(iconSetPath, 'utf8')); + const availableIcons = new Set(Object.keys(iconSet.icons || {})); + + // Recursively scan all .tsx and .ts files + function scanDirectory(dir: string) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + // Skip node_modules, assets, and test-fixtures + if (file === 'node_modules' || file === 'assets' || file === 'test-fixtures') { + return; + } + scanDirectory(filePath); + } else if ((file.endsWith('.tsx') || file.endsWith('.ts')) && !file.endsWith('.test.ts') && !file.endsWith('.test.tsx')) { + const content = fs.readFileSync(filePath, 'utf8'); + const relPath = path.relative(srcDir, filePath); + + // Match LocalIcon usage: + const localIconMatches = content.match(/]*icon="([^"]+)"/g); + if (localIconMatches) { + localIconMatches.forEach(match => { + const iconMatch = match.match(/icon="([^"]+)"/); + if (iconMatch) { + const iconName = iconMatch[1].replace('material-symbols:', ''); + usedIcons.add(iconName); + } + }); + } + + // Match React.createElement(LocalIcon, { icon: 'icon-name', ... }) + const createElementMatches = content.match(/React\.createElement\(LocalIcon,\s*\{[^}]*icon:\s*['"]([^'"]+)['"]/g); + if (createElementMatches) { + createElementMatches.forEach(match => { + const iconMatch = match.match(/icon:\s*['"]([^'"]+)['"]/); + if (iconMatch) { + const iconName = iconMatch[1].replace('material-symbols:', ''); + usedIcons.add(iconName); + } + }); + } + + // Match Icon component usage: + const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); + if (iconMatches) { + iconMatches.forEach(match => { + const iconMatch = match.match(/icon="material-symbols:([^"]+)"/); + if (iconMatch) { + usedIcons.add(iconMatch[1]); + } + }); + } + + // Match icon strings with common Material Symbols suffixes + const iconStringMatches = content.match(/['"]([a-z][a-z0-9-]*(?:-rounded|-outline|-sharp))['"][,\s})]/g); + if (iconStringMatches) { + iconStringMatches.forEach(match => { + const iconMatch = match.match(/['"]([a-z][a-z0-9-]*(?:-rounded|-outline|-sharp))['"][,\s})]/); + if (iconMatch && iconMatch[1]) { + const iconName = iconMatch[1]; + // Skip common false positives + if (!iconName.includes('/') && + !iconName.startsWith('--') && + iconName.length < 50) { + usedIcons.add(iconName); + } + } + }); + } + } + }); + } + + scanDirectory(srcDir); + + // Check for missing icons + const missingIcons: string[] = []; + usedIcons.forEach(iconName => { + if (!availableIcons.has(iconName)) { + missingIcons.push(iconName); + } + }); + + // Fail if any icons are missing + if (missingIcons.length > 0) { + const errorMessage = `Found ${missingIcons.length} icon(s) that don't exist in Material Symbols:\n` + + missingIcons.map(icon => ` - "${icon}"`).join('\n') + '\n\n' + + 'Run "npm run generate-icons" to update the bundle, or fix the icon names.\n' + + 'Search available icons at: https://fonts.google.com/icons'; + + expect(missingIcons, errorMessage).toEqual([]); + } + + // Log summary + console.log(`✅ Validated ${usedIcons.size} icon references - all exist in bundle`); + }); +});