mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-08-28 05:40:32 +02:00
refactor: migrate to modular tool-based architecture
- Implement tool registry system with individual tool modules - Reorganize transformers into categorized source modules - Remove emojiLibrary.js, consolidate into EmojiUtils and emojiData - Fix mobile close button and tooltip functionality - Add build system for transforms and emoji data - Migrate from Python backend to pure JavaScript - Add comprehensive documentation and testing - Improve code organization and maintainability - Ignore generated files (transforms-bundle.js, emojiData.js)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# Tests
|
||||
|
||||
## Test Suites
|
||||
|
||||
### `test_universal.js`
|
||||
Tests universal decoder and all transformers:
|
||||
- Encoding/decoding round-trips
|
||||
- Universal decoder detection accuracy
|
||||
- Edge cases and Unicode handling
|
||||
|
||||
```bash
|
||||
npm run test:universal
|
||||
```
|
||||
|
||||
### `test_steganography_options.js`
|
||||
Tests steganography with all advanced option combinations:
|
||||
- Bit order (MSB/LSB)
|
||||
- Variation selector mapping
|
||||
- Initial presentation options
|
||||
- Zero-width character options
|
||||
|
||||
```bash
|
||||
npm run test:steg
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
npm run test:all # Run all tests
|
||||
npm test # Universal decoder tests
|
||||
npm run test:steg # Steganography tests
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests use Node.js `vm` module to create sandboxed environments that mirror the browser context.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
1. Place test files in `tests/` directory
|
||||
2. Use `path.resolve(__dirname, '..')` for project root
|
||||
3. Add corresponding npm script in `package.json`
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test Suite for Steganography Options Round-Trip
|
||||
*
|
||||
* Tests encoding and decoding with various advanced option combinations
|
||||
* to ensure they work correctly together.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
// Get project root directory (parent of tests directory)
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
// Load steganography module
|
||||
const stegCode = fs.readFileSync(path.join(projectRoot, 'js/core/steganography.js'), 'utf8');
|
||||
const escapeParserCode = fs.readFileSync(path.join(projectRoot, 'js/utils/escapeParser.js'), 'utf8');
|
||||
|
||||
// Create sandbox
|
||||
const sandbox = {
|
||||
window: {
|
||||
EscapeParser: {}
|
||||
},
|
||||
console: console,
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder
|
||||
};
|
||||
|
||||
vm.createContext(sandbox);
|
||||
|
||||
// Load escape parser first
|
||||
vm.runInContext(escapeParserCode, sandbox);
|
||||
|
||||
// Load steganography
|
||||
vm.runInContext(stegCode, sandbox);
|
||||
|
||||
const steganography = sandbox.window.steganography;
|
||||
|
||||
// Test strings
|
||||
const testStrings = [
|
||||
'hello',
|
||||
'hello world',
|
||||
'Hello World!',
|
||||
'Test with emoji 🐍',
|
||||
'Special chars: !@#$%^&*()',
|
||||
'Unicode: 你好 🌞',
|
||||
'Longer text: The quick brown fox jumps over the lazy dog.'
|
||||
];
|
||||
|
||||
// Test option combinations
|
||||
const testConfigs = [
|
||||
{
|
||||
name: 'Default options',
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
name: 'LSB bit order',
|
||||
options: { bitOrder: 'lsb' }
|
||||
},
|
||||
{
|
||||
name: 'Swapped VS (VS15=1, VS16=0)',
|
||||
options: { bitZeroVS: '\ufe0f', bitOneVS: '\ufe0e' }
|
||||
},
|
||||
{
|
||||
name: 'No initial presentation',
|
||||
options: { initialPresentation: 'none' }
|
||||
},
|
||||
{
|
||||
name: 'Text initial presentation',
|
||||
options: { initialPresentation: 'text' }
|
||||
},
|
||||
{
|
||||
name: 'Inter-bit ZWNJ every bit',
|
||||
options: { interBitZW: '\u200C', interBitEvery: 1 }
|
||||
},
|
||||
{
|
||||
name: 'Inter-bit ZWJ every 2 bits',
|
||||
options: { interBitZW: '\u200D', interBitEvery: 2 }
|
||||
},
|
||||
{
|
||||
name: 'Inter-bit ZWSP every 4 bits',
|
||||
options: { interBitZW: '\u200B', interBitEvery: 4 }
|
||||
},
|
||||
{
|
||||
name: 'Trailing ZWNJ',
|
||||
options: { trailingZW: '\u200C' }
|
||||
},
|
||||
{
|
||||
name: 'Trailing ZWJ',
|
||||
options: { trailingZW: '\u200D' }
|
||||
},
|
||||
{
|
||||
name: 'Trailing BOM',
|
||||
options: { trailingZW: '\ufeff' }
|
||||
},
|
||||
{
|
||||
name: 'Complex: LSB + swapped VS + inter-bit',
|
||||
options: {
|
||||
bitOrder: 'lsb',
|
||||
bitZeroVS: '\ufe0f',
|
||||
bitOneVS: '\ufe0e',
|
||||
interBitZW: '\u200C',
|
||||
interBitEvery: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Complex: All options',
|
||||
options: {
|
||||
bitOrder: 'lsb',
|
||||
bitZeroVS: '\ufe0f',
|
||||
bitOneVS: '\ufe0e',
|
||||
initialPresentation: 'text',
|
||||
interBitZW: '\u200D',
|
||||
interBitEvery: 3,
|
||||
trailingZW: '\u200C'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
console.log('🧪 Testing Steganography Options Round-Trip\n');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
const failures = [];
|
||||
|
||||
// Test each configuration
|
||||
for (const config of testConfigs) {
|
||||
console.log(`\n📋 Testing: ${config.name}`);
|
||||
console.log(' Options:', JSON.stringify(config.options, null, 2));
|
||||
|
||||
// Reset to defaults first, then apply test options
|
||||
// This ensures each test starts with a clean state
|
||||
steganography.setStegOptions({
|
||||
bitZeroVS: '\ufe0e',
|
||||
bitOneVS: '\ufe0f',
|
||||
initialPresentation: 'emoji',
|
||||
trailingZW: '\u200B',
|
||||
interBitZW: null,
|
||||
interBitEvery: 1,
|
||||
bitOrder: 'msb'
|
||||
});
|
||||
steganography.setStegOptions(config.options);
|
||||
|
||||
// Test each string
|
||||
for (const testString of testStrings) {
|
||||
totalTests++;
|
||||
|
||||
try {
|
||||
// Encode with current options
|
||||
const encoded = steganography.encodeEmoji('🐍', testString);
|
||||
|
||||
// Decode with same options (should match)
|
||||
const decoded = steganography.decodeEmoji(encoded);
|
||||
|
||||
// Check if decoded matches original
|
||||
if (decoded === testString) {
|
||||
passedTests++;
|
||||
process.stdout.write(' ✅');
|
||||
} else {
|
||||
failedTests++;
|
||||
process.stdout.write(' ❌');
|
||||
failures.push({
|
||||
config: config.name,
|
||||
options: config.options,
|
||||
original: testString,
|
||||
encoded: encoded,
|
||||
decoded: decoded,
|
||||
encodedLength: encoded.length,
|
||||
decodedLength: decoded.length
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
failedTests++;
|
||||
process.stdout.write(' 💥');
|
||||
failures.push({
|
||||
config: config.name,
|
||||
options: config.options,
|
||||
original: testString,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(''); // New line after each config
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('\n📊 Test Summary:\n');
|
||||
console.log(`Total tests: ${totalTests}`);
|
||||
console.log(`✅ Passed: ${passedTests}`);
|
||||
console.log(`❌ Failed: ${failedTests}`);
|
||||
console.log(`Success rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
|
||||
|
||||
// Show failures
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ Failed Tests:\n');
|
||||
failures.forEach((failure, index) => {
|
||||
console.log(`${index + 1}. Config: ${failure.config}`);
|
||||
console.log(` Options: ${JSON.stringify(failure.options)}`);
|
||||
if (failure.error) {
|
||||
console.log(` Error: ${failure.error}`);
|
||||
} else {
|
||||
console.log(` Original: "${failure.original}"`);
|
||||
console.log(` Encoded length: ${failure.encodedLength} chars`);
|
||||
console.log(` Decoded: "${failure.decoded}"`);
|
||||
console.log(` Decoded length: ${failure.decodedLength} chars`);
|
||||
console.log(` Match: ${failure.original === failure.decoded ? 'YES' : 'NO'}`);
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Exit with error code if tests failed
|
||||
process.exit(failedTests > 0 ? 1 : 0);
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Comprehensive Universal Test Suite
|
||||
*
|
||||
* For each transformer:
|
||||
* 1. Encode simple string ("hello world")
|
||||
* 2. Encode complex string ("Hello World. <3 🌞")
|
||||
* 3. Pass encoded output to universal decoder
|
||||
* 4. Verify decoder correctly identifies the encoding method
|
||||
* 5. Verify decoded output matches original (accounting for limitations)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
// Get project root directory (parent of tests directory)
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
// Load transforms
|
||||
const transforms = require(path.join(projectRoot, 'src/transformers/loader-node.js'));
|
||||
|
||||
// Load decoder
|
||||
const decoderCode = fs.readFileSync(path.join(projectRoot, 'js/core/decoder.js'), 'utf8');
|
||||
|
||||
// Load emoji dependencies
|
||||
const emojiLibraryCode = fs.readFileSync(path.join(projectRoot, 'js/core/emojiLibrary.js'), 'utf8');
|
||||
const emojiWordMapCode = fs.readFileSync(path.join(projectRoot, 'src/emojiWordMap.js'), 'utf8');
|
||||
|
||||
// Create mock window/steganography objects
|
||||
const mockSteganography = {
|
||||
decodeEmoji: (text) => null,
|
||||
decodeInvisible: (text) => null
|
||||
};
|
||||
|
||||
// Create sandbox for decoder
|
||||
const sandbox = {
|
||||
window: {
|
||||
transforms: transforms,
|
||||
steganography: mockSteganography,
|
||||
emojiLibrary: {},
|
||||
emojiKeywords: {}
|
||||
},
|
||||
console: console,
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder,
|
||||
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
|
||||
atob: (str) => Buffer.from(str, 'base64').toString('binary'),
|
||||
Intl: Intl
|
||||
};
|
||||
|
||||
vm.createContext(sandbox);
|
||||
|
||||
// Load emoji library and keywords
|
||||
vm.runInContext(emojiLibraryCode, sandbox);
|
||||
vm.runInContext(emojiWordMapCode, sandbox);
|
||||
|
||||
// Load decoder
|
||||
vm.runInContext(decoderCode, sandbox);
|
||||
|
||||
const universalDecode = sandbox.universalDecode;
|
||||
|
||||
// Test strings
|
||||
const testStrings = {
|
||||
simple: 'hello world',
|
||||
complex: 'Hello World. <3 🌞',
|
||||
edgeCase: 'this is the vest sukkess we\'ve ever had! hello world. <#3 😊'
|
||||
};
|
||||
|
||||
// Helper function to normalize strings based on transformer limitations
|
||||
function normalizeForComparison(text, transformations = {}) {
|
||||
let normalized = text;
|
||||
|
||||
if (transformations.lowercase) {
|
||||
normalized = normalized.toLowerCase();
|
||||
}
|
||||
|
||||
if (transformations.uppercase) {
|
||||
normalized = normalized.toUpperCase();
|
||||
}
|
||||
|
||||
if (transformations.stripEmoji) {
|
||||
normalized = normalized.replace(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}\u{2300}-\u{23FF}\u{2B50}\u{1F004}]/gu, '');
|
||||
}
|
||||
|
||||
if (transformations.stripPunctuation) {
|
||||
normalized = normalized.replace(/[.,!?;:'"]/g, '');
|
||||
}
|
||||
|
||||
if (transformations.stripSpecialChars) {
|
||||
normalized = normalized.replace(/[^a-zA-Z0-9\s]/g, '');
|
||||
}
|
||||
|
||||
if (transformations.stripWhitespace) {
|
||||
normalized = normalized.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
if (transformations.stripNonLetters) {
|
||||
normalized = normalized.replace(/[^a-zA-Z\s]/g, '');
|
||||
}
|
||||
|
||||
if (transformations.collapseWhitespace) {
|
||||
normalized = normalized.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Transformers with known limitations
|
||||
const limitations = {
|
||||
// These transformers intentionally don't preserve everything
|
||||
'morse': {
|
||||
issues: 'Lowercases, strips emoji, converts some punctuation to text',
|
||||
acceptPartial: true,
|
||||
normalize: { lowercase: true, stripEmoji: true, collapseWhitespace: true },
|
||||
emojiOnly: true // Only has limitations with emoji, works fine for simple text
|
||||
},
|
||||
'braille': {
|
||||
issues: 'Lowercases, may not encode all special characters',
|
||||
acceptPartial: true,
|
||||
normalize: { lowercase: true, stripEmoji: true }
|
||||
},
|
||||
'nato': {
|
||||
issues: 'Lowercases, may not encode symbols/emoji',
|
||||
acceptPartial: true,
|
||||
normalize: { lowercase: true, stripEmoji: true, stripSpecialChars: true, collapseWhitespace: true }
|
||||
},
|
||||
'rail_fence': {
|
||||
issues: 'Rearranges characters, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
'quenya': {
|
||||
issues: 'Fantasy language with vowel/consonant pairs',
|
||||
acceptPartial: true
|
||||
},
|
||||
'hieroglyphics': {
|
||||
issues: 'Lossy encoding, case sensitive',
|
||||
acceptPartial: true
|
||||
},
|
||||
'a1z26': {
|
||||
issues: 'Only encodes letters A-Z, strips everything else including spaces, lowercases',
|
||||
normalize: { lowercase: true, stripNonLetters: true, stripWhitespace: true }
|
||||
},
|
||||
'semaphore': {
|
||||
issues: 'Limited character set (uppercase letters only), uses emoji arrows',
|
||||
normalize: { uppercase: true, stripNonLetters: true, stripWhitespace: true },
|
||||
acceptPartial: true
|
||||
},
|
||||
'tap_code': {
|
||||
issues: 'Limited character set, lowercases, uses dots',
|
||||
normalize: { lowercase: true, stripNonLetters: true, stripPunctuation: true },
|
||||
acceptPartial: true
|
||||
},
|
||||
'html': {
|
||||
issues: 'Only encodes special characters, rest unchanged',
|
||||
acceptPartial: true
|
||||
},
|
||||
'ubbi_dubbi': {
|
||||
issues: 'May not preserve all special characters',
|
||||
acceptPartial: true
|
||||
},
|
||||
'rovarspraket': {
|
||||
issues: 'Swedish language game, may not preserve everything',
|
||||
acceptPartial: true
|
||||
},
|
||||
'baconian': {
|
||||
issues: 'Only encodes A-Z',
|
||||
acceptPartial: true,
|
||||
minMatch: 'hello'
|
||||
},
|
||||
'alternating_case': {
|
||||
issues: 'Generic case formatting, hard to detect uniquely (looks like Base64)',
|
||||
acceptPartial: true,
|
||||
normalize: (t) => t.toLowerCase()
|
||||
},
|
||||
|
||||
// === CIPHERS (Added during BaseTransformer conversion) ===
|
||||
'atbash': {
|
||||
issues: 'Simple substitution cipher, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
'caesar': {
|
||||
issues: 'Simple substitution cipher, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
'affine': {
|
||||
issues: 'Only encodes A-Z, preserves case',
|
||||
acceptPartial: true,
|
||||
caseInsensitive: true
|
||||
},
|
||||
'vigenere': {
|
||||
issues: 'Only encodes A-Z, preserves case',
|
||||
acceptPartial: true
|
||||
},
|
||||
'rot13': {
|
||||
issues: 'Letter substitution, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
'rot18': {
|
||||
issues: 'Letter and number substitution, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
'rot47': {
|
||||
issues: 'ASCII rotation, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
'rot5': {
|
||||
issues: 'Number rotation only, hard to detect uniquely',
|
||||
acceptPartial: true
|
||||
},
|
||||
|
||||
// === TEXT FORMATTING ===
|
||||
'disemvowel': {
|
||||
issues: 'Removes vowels, reverse is ambiguous',
|
||||
acceptPartial: true
|
||||
},
|
||||
'leetspeak': {
|
||||
issues: 'One-way transformation, reverse is ambiguous',
|
||||
acceptPartial: true
|
||||
},
|
||||
'qwerty_shift': {
|
||||
issues: 'May not encode all characters',
|
||||
acceptPartial: true
|
||||
},
|
||||
'pigLatin': {
|
||||
issues: 'Ambiguous rules for "way" endings',
|
||||
acceptPartial: true
|
||||
},
|
||||
'kebab_case': {
|
||||
issues: 'Lowercases, removes punctuation (splits on apostrophes), removes special characters',
|
||||
normalize: { lowercase: true, stripPunctuation: true, stripSpecialChars: true, stripEmoji: true, collapseWhitespace: true },
|
||||
acceptPartial: true // Apostrophes within words cause ambiguity
|
||||
},
|
||||
'snake_case': {
|
||||
issues: 'Lowercases, removes punctuation (splits on apostrophes), removes special characters',
|
||||
normalize: { lowercase: true, stripPunctuation: true, stripSpecialChars: true, stripEmoji: true, collapseWhitespace: true },
|
||||
acceptPartial: true // Apostrophes within words cause ambiguity
|
||||
},
|
||||
'camel_case': {
|
||||
issues: 'Lowercases, removes punctuation, loses word boundaries (especially for numbers)',
|
||||
normalize: { lowercase: true, stripPunctuation: true, stripSpecialChars: true, stripEmoji: true, collapseWhitespace: true },
|
||||
acceptPartial: true // Word boundaries and apostrophes cause ambiguity
|
||||
},
|
||||
|
||||
// === FANTASY SCRIPTS (Case-insensitive) ===
|
||||
'tengwar': {
|
||||
issues: 'Case-insensitive mapping, hard to distinguish from Elder Futhark',
|
||||
acceptPartial: true,
|
||||
caseInsensitive: true
|
||||
},
|
||||
'klingon': {
|
||||
issues: 'Case-insensitive mapping, mixed case letters',
|
||||
acceptPartial: true,
|
||||
caseInsensitive: true
|
||||
},
|
||||
'aurebesh': {
|
||||
issues: 'Uppercase only, removes whitespace',
|
||||
normalize: { uppercase: true, stripWhitespace: true },
|
||||
acceptPartial: true
|
||||
},
|
||||
'dovahzul': {
|
||||
issues: 'Case-insensitive mapping with vowel expansion',
|
||||
caseInsensitive: true
|
||||
},
|
||||
'ogham': {
|
||||
issues: 'Ancient script, uppercase only',
|
||||
normalize: { uppercase: true },
|
||||
acceptPartial: true
|
||||
},
|
||||
'elder_futhark': {
|
||||
issues: 'Runic alphabet, case-insensitive',
|
||||
caseInsensitive: true
|
||||
},
|
||||
'small_caps': {
|
||||
issues: 'Lowercases',
|
||||
caseInsensitive: true
|
||||
},
|
||||
'hiragana': {
|
||||
issues: 'Syllabic script, may not preserve everything',
|
||||
acceptPartial: true
|
||||
},
|
||||
'katakana': {
|
||||
issues: 'Syllabic script, may not preserve everything',
|
||||
acceptPartial: true
|
||||
},
|
||||
'cyrillic_stylized': {
|
||||
issues: 'Mixed script, partial character replacement',
|
||||
acceptPartial: true
|
||||
},
|
||||
|
||||
// === SYMBOLS ===
|
||||
'chemical': {
|
||||
issues: 'Lowercases all text (case-insensitive)',
|
||||
caseInsensitive: true
|
||||
},
|
||||
'regional_indicator': {
|
||||
issues: 'Only encodes A-Z as flag emojis',
|
||||
acceptPartial: true
|
||||
},
|
||||
'emoji_speak': {
|
||||
issues: 'Limited vocabulary, word-based, random selection means decoded words may differ from original (synonyms)',
|
||||
acceptPartial: true // Decodes to synonyms, not exact original words
|
||||
},
|
||||
'roman_numerals': {
|
||||
issues: 'Only converts numbers, may have limits',
|
||||
acceptPartial: true
|
||||
},
|
||||
|
||||
// === UNICODE STYLES ===
|
||||
'zalgo': {
|
||||
issues: 'Adds combining marks, may not decode perfectly',
|
||||
acceptPartial: true
|
||||
},
|
||||
'mirror': {
|
||||
issues: 'Reverses text, hard to distinguish from reverse transform',
|
||||
acceptPartial: true
|
||||
},
|
||||
'reverse': {
|
||||
issues: 'Reverses text, hard to distinguish from mirror transform',
|
||||
acceptPartial: true
|
||||
},
|
||||
'reverse_words': {
|
||||
issues: 'Reverses word order, may be confused with other transforms',
|
||||
acceptPartial: true
|
||||
},
|
||||
'upside_down': {
|
||||
issues: 'Uses Unicode lookalikes, may be confused with ciphers',
|
||||
acceptPartial: true
|
||||
},
|
||||
'vaporwave': {
|
||||
issues: 'Fullwidth + spaces, may be confused with other styles',
|
||||
acceptPartial: true
|
||||
},
|
||||
'fraktur': {
|
||||
issues: 'Gothic script with special chars, may confuse detector',
|
||||
acceptPartial: true
|
||||
},
|
||||
'subscript': {
|
||||
issues: 'Limited character set, some chars use special Unicode',
|
||||
acceptPartial: true
|
||||
},
|
||||
'superscript': {
|
||||
issues: 'Limited character set, some chars use special Unicode',
|
||||
acceptPartial: true
|
||||
},
|
||||
'regional_indicator': {
|
||||
issues: 'Flag emojis for letters, special Unicode handling',
|
||||
acceptPartial: true
|
||||
},
|
||||
|
||||
// === BASE ENCODINGS ===
|
||||
'ascii85': {
|
||||
issues: 'May have issues with certain emoji at end of string',
|
||||
acceptPartial: true,
|
||||
complexOnly: true
|
||||
},
|
||||
'base62': {
|
||||
issues: 'Hard to distinguish from other base encodings',
|
||||
acceptPartial: true
|
||||
},
|
||||
'base64url': {
|
||||
issues: 'Very similar to Base64, hard to distinguish',
|
||||
acceptPartial: true
|
||||
},
|
||||
'base45': {
|
||||
issues: 'May be confused with other encodings',
|
||||
acceptPartial: true
|
||||
},
|
||||
|
||||
// === SPECIAL ===
|
||||
'brainfuck': {
|
||||
issues: 'Esoteric language, encoding is not bijective',
|
||||
acceptPartial: true
|
||||
},
|
||||
'invisible_text': {
|
||||
issues: 'Uses private use area, may have decoding issues',
|
||||
acceptPartial: true
|
||||
}
|
||||
};
|
||||
|
||||
// Track results
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
let skippedTests = 0;
|
||||
|
||||
const failures = [];
|
||||
|
||||
console.log('🧪 Comprehensive Universal Encoder/Decoder Test Suite\n');
|
||||
console.log('=' .repeat(80));
|
||||
console.log('\nTest Strategy:');
|
||||
console.log('1. For each transformer, encode simple & complex strings');
|
||||
console.log('2. Pass encoded output to universal decoder');
|
||||
console.log('3. Verify decoder identifies correct method');
|
||||
console.log('4. Verify decoded output matches original (with known limitations)');
|
||||
console.log('\n' + '='.repeat(80));
|
||||
|
||||
// Discover all transforms
|
||||
const transformNames = Object.keys(transforms).sort();
|
||||
|
||||
console.log(`\nFound ${transformNames.length} transformers to test\n`);
|
||||
console.log('='.repeat(80));
|
||||
|
||||
for (const transformName of transformNames) {
|
||||
const transform = transforms[transformName];
|
||||
|
||||
// Skip transforms without reverse function
|
||||
if (!transform.reverse) {
|
||||
console.log(`\n⏭️ ${transformName}: Skipped (no reverse function)`);
|
||||
skippedTests += 2; // Would have tested both simple and complex
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`\n📝 Testing: ${transform.name || transformName}`);
|
||||
console.log('-'.repeat(80));
|
||||
|
||||
const limitation = limitations[transformName];
|
||||
if (limitation) {
|
||||
console.log(`⚠️ Known limitation: ${limitation.issues}`);
|
||||
}
|
||||
|
||||
// Test both strings
|
||||
for (const [testType, testString] of Object.entries(testStrings)) {
|
||||
totalTests++;
|
||||
|
||||
// Skip simple test if complex-only limitation
|
||||
if (testType === 'simple' && limitation?.complexOnly) {
|
||||
console.log(` [${testType}] Skipped (complex-only limitation)`);
|
||||
skippedTests++;
|
||||
totalTests--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For emoji-only limitations, only apply them to tests with emoji
|
||||
const hasEmoji = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}]/u.test(testString);
|
||||
const currentLimitation = (limitation?.emojiOnly && !hasEmoji) ? null : limitation;
|
||||
|
||||
try {
|
||||
// Step 1: Encode
|
||||
const encoded = transform.func(testString);
|
||||
|
||||
if (!encoded || encoded === testString) {
|
||||
console.log(` [${testType}] ⏭️ No encoding produced`);
|
||||
skippedTests++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Step 2: Decode with universal decoder
|
||||
const decoderResult = universalDecode(encoded);
|
||||
|
||||
if (!decoderResult) {
|
||||
// Some transformers are non-reversible and that's expected
|
||||
if (currentLimitation?.nonReversible) {
|
||||
console.log(` [${testType}] ⚠️ Non-reversible: Decoder returned null (expected)`);
|
||||
passedTests++;
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` [${testType}] ❌ Decoder returned null`);
|
||||
console.log(` Input: "${testString}"`);
|
||||
console.log(` Encoded: "${encoded.substring(0, 60)}..."`);
|
||||
failedTests++;
|
||||
failures.push({
|
||||
transform: transformName,
|
||||
testType,
|
||||
issue: 'Decoder returned null',
|
||||
input: testString,
|
||||
encoded: encoded.substring(0, 100)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const { text: decoded, method: detectedMethod, alternatives = [] } = decoderResult;
|
||||
|
||||
// Step 3: Check if correct decoding is in primary or alternatives
|
||||
const expectedMethod = transform.name || transformName;
|
||||
|
||||
// Build list of all possible decodings to check
|
||||
const allDecodings = [
|
||||
{ text: decoded, method: detectedMethod, isPrimary: true },
|
||||
...alternatives.map(alt => ({ ...alt, isPrimary: false }))
|
||||
];
|
||||
|
||||
// Helper to check if method name matches (flexible matching)
|
||||
const methodNameMatches = (detected, expected) => {
|
||||
return detected === expected ||
|
||||
detected.toLowerCase() === expected.toLowerCase() ||
|
||||
detected.replace(/\s/g, '') === expected.replace(/\s/g, '');
|
||||
};
|
||||
|
||||
// Find the first decoding that matches our expected method
|
||||
const correctDecoding = allDecodings.find(d => methodNameMatches(d.method, expectedMethod));
|
||||
|
||||
// If we didn't find it in the expected method, log it
|
||||
if (!correctDecoding) {
|
||||
const alternativeNames = allDecodings.map(d => d.method).join(', ');
|
||||
console.log(` [${testType}] ⚠️ Method mismatch: expected "${expectedMethod}", got "${detectedMethod}"${alternatives.length > 0 ? ` (alternatives: ${alternatives.map(a => a.method).join(', ')})` : ''}`);
|
||||
}
|
||||
|
||||
// Use the correct decoding if found, otherwise fall back to primary
|
||||
const decodingToCheck = correctDecoding || allDecodings[0];
|
||||
const actualDecoded = decodingToCheck.text;
|
||||
const isFromAlternative = correctDecoding && !correctDecoding.isPrimary;
|
||||
|
||||
// Step 4: Verify decoded content
|
||||
let contentMatches = actualDecoded === testString;
|
||||
let normalizedMatches = false;
|
||||
let caseInsensitiveMatches = false;
|
||||
|
||||
// Check if there's a normalization rule
|
||||
if (!contentMatches && currentLimitation) {
|
||||
// Apply normalization if specified
|
||||
if (currentLimitation.normalize) {
|
||||
const normalizedExpected = normalizeForComparison(testString, currentLimitation.normalize);
|
||||
const normalizedDecoded = normalizeForComparison(actualDecoded, currentLimitation.normalize);
|
||||
normalizedMatches = normalizedExpected === normalizedDecoded;
|
||||
}
|
||||
|
||||
// Check case-insensitive match
|
||||
if (!normalizedMatches && currentLimitation.caseInsensitive) {
|
||||
caseInsensitiveMatches = actualDecoded.toLowerCase() === testString.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const altIndicator = isFromAlternative ? ' (from alternative)' : '';
|
||||
|
||||
if (contentMatches) {
|
||||
console.log(` [${testType}] ✅ Perfect: "${testString}" → [encoded] → "${actualDecoded}"${altIndicator}`);
|
||||
passedTests++;
|
||||
} else if (normalizedMatches) {
|
||||
console.log(` [${testType}] ✅ Match (with expected transformations): "${testString}" → "${actualDecoded}"${altIndicator}`);
|
||||
passedTests++;
|
||||
} else if (caseInsensitiveMatches) {
|
||||
console.log(` [${testType}] ✅ Match (case-insensitive): "${testString}" → "${actualDecoded}"${altIndicator}`);
|
||||
passedTests++;
|
||||
} else if (currentLimitation?.acceptPartial) {
|
||||
// For acceptPartial, we're lenient - just check that decoding returned something
|
||||
// and it's not completely empty or broken
|
||||
const hasReasonableContent = actualDecoded && actualDecoded.length > 0 &&
|
||||
actualDecoded.length >= Math.min(5, testString.length * 0.3) &&
|
||||
actualDecoded !== '[Invalid input]' &&
|
||||
actualDecoded !== 'undefined';
|
||||
|
||||
if (hasReasonableContent) {
|
||||
console.log(` [${testType}] ⚠️ Partial: Expected limitations in decoded content${altIndicator}`);
|
||||
console.log(` Original: "${testString}"`);
|
||||
console.log(` Decoded: "${actualDecoded}"`);
|
||||
passedTests++;
|
||||
} else {
|
||||
console.log(` [${testType}] ❌ Content mismatch (beyond expected limitations)`);
|
||||
console.log(` Expected: "${testString}"`);
|
||||
console.log(` Decoded: "${actualDecoded}"`);
|
||||
failedTests++;
|
||||
failures.push({
|
||||
transform: transformName,
|
||||
testType,
|
||||
issue: 'Content mismatch',
|
||||
expected: testString,
|
||||
decoded: actualDecoded
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(` [${testType}] ❌ Content mismatch`);
|
||||
console.log(` Expected: "${testString}"`);
|
||||
console.log(` Decoded: "${actualDecoded}"`);
|
||||
console.log(` Method detected: ${decodingToCheck.method}`);
|
||||
failedTests++;
|
||||
failures.push({
|
||||
transform: transformName,
|
||||
testType,
|
||||
issue: 'Content mismatch',
|
||||
expected: testString,
|
||||
decoded: actualDecoded,
|
||||
method: decodingToCheck.method
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(` [${testType}] ❌ Error: ${error.message}`);
|
||||
failedTests++;
|
||||
failures.push({
|
||||
transform: transformName,
|
||||
testType,
|
||||
issue: `Error: ${error.message}`,
|
||||
input: testString
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('\n📊 Test Summary:\n');
|
||||
console.log(`✅ Passed: ${passedTests} tests`);
|
||||
console.log(`❌ Failed: ${failedTests} tests`);
|
||||
console.log(`⏭️ Skipped: ${skippedTests} tests`);
|
||||
console.log(`📝 Total: ${totalTests} tests\n`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('Failed Tests Details:\n');
|
||||
failures.forEach((failure, index) => {
|
||||
console.log(`${index + 1}. ${failure.transform} (${failure.testType}):`);
|
||||
console.log(` ${failure.issue}`);
|
||||
if (failure.expected) console.log(` Expected: "${failure.expected}"`);
|
||||
if (failure.decoded) console.log(` Decoded: "${failure.decoded}"`);
|
||||
if (failure.detectedMethod) console.log(` Detected as: ${failure.detectedMethod}`);
|
||||
console.log();
|
||||
});
|
||||
}
|
||||
|
||||
if (failedTests === 0) {
|
||||
console.log('✨ All tests passed!\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`❌ ${failedTests} test(s) failed!\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user