feat: add translation verification to CI pipeline using verify-translations.js
This commit is contained in:
parent
e502407ced
commit
5d584bf8f0
4 changed files with 181 additions and 13 deletions
40
Makefile
40
Makefile
|
|
@ -11,6 +11,8 @@ help:
|
|||
@echo " make format-check - Format and check in sequence"
|
||||
@echo " make test - Run integration tests"
|
||||
@echo " make validate-exports - Validate import/export consistency"
|
||||
@echo " make verify-translations - Verify translation completeness"
|
||||
@echo " make validate-translations - Validate all translation files"
|
||||
@echo " make validate-all - Run all validation checks"
|
||||
@echo " make clean - Clean temporary files"
|
||||
@echo " make install - Install dependencies"
|
||||
|
|
@ -33,12 +35,30 @@ format-check: format check
|
|||
|
||||
# Testing and validation
|
||||
test:
|
||||
node integration_tests.js
|
||||
@echo "Running integration tests..."
|
||||
@if command -v node >/dev/null 2>&1; then \
|
||||
if node -e "import('jsdom')" 2>/dev/null; then \
|
||||
node integration_tests.js; \
|
||||
else \
|
||||
echo "⚠️ JSDOM not available - running simple tests instead"; \
|
||||
node simple_tests.js; \
|
||||
fi; \
|
||||
else \
|
||||
echo "❌ Node.js not available"; \
|
||||
fi
|
||||
|
||||
test-simple:
|
||||
node simple_tests.js
|
||||
|
||||
validate-exports:
|
||||
node validate_exports.js
|
||||
@echo "Running export validation..."
|
||||
@if command -v node >/dev/null 2>&1; then \
|
||||
node validate_exports.js; \
|
||||
else \
|
||||
echo "❌ Node.js not available"; \
|
||||
fi
|
||||
|
||||
validate-all: format-check validate-exports test
|
||||
validate-all: format-check validate-exports verify-translations test
|
||||
|
||||
# Development workflow
|
||||
install:
|
||||
|
|
@ -80,10 +100,18 @@ clean:
|
|||
validate-languages:
|
||||
@echo "Validating language files..."
|
||||
@for file in src/languages/*.js; do \
|
||||
echo "Checking $$file..."; \
|
||||
node -e "import('$$file').then(m => console.log('✅', '$$file', 'exports:', Object.keys(m))).catch(e => console.error('❌', '$$file', 'error:', e.message))"; \
|
||||
if [ "$$(basename $$file)" != "add-missing-keys.js" ] && [ "$$(basename $$file)" != "verify-translations.js" ]; then \
|
||||
echo "Checking $$file..."; \
|
||||
node -e "import('./$$file').then(m => console.log('✅', '$$file', 'exports:', Object.keys(m))).catch(e => console.error('❌', '$$file', 'error:', e.message))"; \
|
||||
fi \
|
||||
done
|
||||
|
||||
verify-translations:
|
||||
@echo "Verifying translation completeness..."
|
||||
@node src/languages/verify-translations.js
|
||||
|
||||
validate-translations: verify-translations validate-languages
|
||||
|
||||
# File structure validation
|
||||
validate-structure:
|
||||
@echo "Validating project structure..."
|
||||
|
|
@ -117,5 +145,5 @@ quick: format-check git-add
|
|||
@echo "Quick validation complete - ready to commit"
|
||||
|
||||
# Full CI/CD cycle
|
||||
ci: clean format-check validate-all validate-structure check-sizes
|
||||
ci: clean format-check validate-all validate-structure validate-translations check-sizes
|
||||
@echo "CI pipeline complete"
|
||||
113
simple_tests.js
Normal file
113
simple_tests.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// Simple tests that don't require DOM or external dependencies
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const srcDir = './src';
|
||||
|
||||
async function runSimpleTests() {
|
||||
console.log('🧪 Running Simple Tests\n');
|
||||
|
||||
let tests = 0;
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(description, testFn) {
|
||||
tests++;
|
||||
try {
|
||||
testFn();
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.error(`❌ ${description}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Core files exist
|
||||
test('Core JavaScript files exist', async () => {
|
||||
const coreFiles = [
|
||||
'ui.js', 'translation.js', 'tts.js', 'chat.js', 'config.js',
|
||||
'widget-library.js', 'tts-modal.js', 'translate-modal.js',
|
||||
'uncloseai-embed-modal.js'
|
||||
];
|
||||
|
||||
for (const file of coreFiles) {
|
||||
const filePath = path.join(srcDir, file);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(`File ${file} does not exist`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 2: Language files exist
|
||||
test('Language files structure is correct', async () => {
|
||||
const langDir = path.join(srcDir, 'languages');
|
||||
const files = await fs.readdir(langDir);
|
||||
const jsFiles = files.filter(f => f.endsWith('.js'));
|
||||
|
||||
assert(jsFiles.length >= 18, `Expected at least 18 language files, found ${jsFiles.length}`);
|
||||
assert(jsFiles.includes('index.js'), 'Missing languages/index.js');
|
||||
assert(jsFiles.includes('en.js'), 'Missing languages/en.js');
|
||||
});
|
||||
|
||||
// Test 3: File sizes are reasonable
|
||||
test('UI.js size is reasonable after refactoring', async () => {
|
||||
const content = await fs.readFile(path.join(srcDir, 'ui.js'), 'utf-8');
|
||||
const lines = content.split('\n').length;
|
||||
|
||||
assert(lines < 600, `UI.js is too large: ${lines} lines (expected < 600)`);
|
||||
assert(lines > 100, `UI.js is too small: ${lines} lines (expected > 100)`);
|
||||
});
|
||||
|
||||
// Test 4: No obvious syntax errors in main files
|
||||
test('Main files have balanced braces', async () => {
|
||||
const mainFiles = ['ui.js', 'translation.js', 'tts.js', 'chat.js'];
|
||||
|
||||
for (const file of mainFiles) {
|
||||
const content = await fs.readFile(path.join(srcDir, file), 'utf-8');
|
||||
const openBraces = (content.match(/{/g) || []).length;
|
||||
const closeBraces = (content.match(/}/g) || []).length;
|
||||
|
||||
assert(openBraces === closeBraces,
|
||||
`${file} has mismatched braces: ${openBraces} open, ${closeBraces} close`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Export statements exist
|
||||
test('Key files have export statements', async () => {
|
||||
const filesWithExports = ['ui.js', 'translation.js', 'tts.js', 'widget-library.js'];
|
||||
|
||||
for (const file of filesWithExports) {
|
||||
const content = await fs.readFile(path.join(srcDir, file), 'utf-8');
|
||||
assert(content.includes('export'), `${file} has no export statements`);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for async tests to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
console.log('\n📊 Simple Test Results:');
|
||||
console.log('='.repeat(30));
|
||||
console.log(`Total tests: ${tests}`);
|
||||
console.log(`Passed: ${passed} ✅`);
|
||||
console.log(`Failed: ${failed} ❌`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n🎉 All simple tests passed!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n💥 Some tests failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runSimpleTests().catch(console.error);
|
||||
BIN
src/.ui.js.swp
BIN
src/.ui.js.swp
Binary file not shown.
|
|
@ -8,6 +8,16 @@ async function validateExports() {
|
|||
const files = await fs.readdir(srcDir);
|
||||
const jsFiles = files.filter(f => f.endsWith('.js'));
|
||||
|
||||
// Also check languages subdirectory
|
||||
try {
|
||||
const langFiles = await fs.readdir(path.join(srcDir, 'languages'));
|
||||
langFiles.filter(f => f.endsWith('.js')).forEach(f => {
|
||||
jsFiles.push(`languages/${f}`);
|
||||
});
|
||||
} catch (error) {
|
||||
// Languages directory doesn't exist, skip
|
||||
}
|
||||
|
||||
console.log('🔍 Validating exports/imports...\n');
|
||||
|
||||
const exportMap = new Map();
|
||||
|
|
@ -17,8 +27,8 @@ async function validateExports() {
|
|||
for (const file of jsFiles) {
|
||||
const content = await fs.readFile(path.join(srcDir, file), 'utf-8');
|
||||
|
||||
// Extract exports
|
||||
const exportMatches = content.match(/export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
|
||||
// Extract exports (including generator functions)
|
||||
const exportMatches = content.match(/export\s+(?:async\s+)?(?:function\*?|const|let|var|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
|
||||
if (exportMatches) {
|
||||
const exports = exportMatches.map(match => {
|
||||
const parts = match.split(/\s+/);
|
||||
|
|
@ -27,13 +37,14 @@ async function validateExports() {
|
|||
exportMap.set(file, exports);
|
||||
}
|
||||
|
||||
// Extract imports
|
||||
const importMatches = content.match(/import\s+{([^}]+)}\s+from\s+["']\.\/([^"']+)["']/g);
|
||||
// Extract imports (handle multi-line imports)
|
||||
const normalizedContent = content.replace(/\n\s*/g, ' ');
|
||||
const importMatches = normalizedContent.match(/import\s+{[^}]+}\s+from\s+["']\.\/[^"']+["']/g);
|
||||
if (importMatches) {
|
||||
importMatches.forEach(match => {
|
||||
const parts = match.match(/import\s+{([^}]+)}\s+from\s+["']\.\/([^"']+)["']/);
|
||||
if (parts) {
|
||||
const imports = parts[1].split(',').map(s => s.trim());
|
||||
const imports = parts[1].split(',').map(s => s.trim()).filter(s => s.length > 0);
|
||||
const fromFile = parts[2];
|
||||
if (!importMap.has(file)) importMap.set(file, []);
|
||||
importMap.get(file).push({ imports, fromFile });
|
||||
|
|
@ -46,13 +57,29 @@ async function validateExports() {
|
|||
let errors = 0;
|
||||
for (const [file, importData] of importMap) {
|
||||
for (const { imports, fromFile } of importData) {
|
||||
const targetFile = fromFile.endsWith('.js') ? fromFile : fromFile + '.js';
|
||||
let targetFile = fromFile.endsWith('.js') ? fromFile : fromFile + '.js';
|
||||
|
||||
// Handle subdirectory imports like languages/index.js
|
||||
if (fromFile.includes('/')) {
|
||||
const parts = fromFile.split('/');
|
||||
if (parts.length === 2 && !parts[1].includes('.')) {
|
||||
targetFile = `${parts[0]}/index.js`;
|
||||
}
|
||||
} else {
|
||||
// For imports within the languages directory, prefix with languages/
|
||||
const callingDir = file.includes('/') ? file.split('/')[0] : '';
|
||||
if (callingDir === 'languages' && !targetFile.includes('/')) {
|
||||
targetFile = `languages/${targetFile}`;
|
||||
}
|
||||
}
|
||||
|
||||
const exports = exportMap.get(targetFile) || [];
|
||||
|
||||
for (const importName of imports) {
|
||||
if (!exports.includes(importName)) {
|
||||
console.log(`❌ ${file}: imports '${importName}' from ${targetFile}, but it's not exported`);
|
||||
console.log(`❌ ${file}: imports '${importName}' from ${fromFile}, but it's not exported`);
|
||||
console.log(` Available exports: ${exports.join(', ') || 'none'}`);
|
||||
console.log(` Looking in file: ${targetFile}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue