feat: Implement complete style detection and preservation foundation
- Add StyleDetectionEngine with one-layer-deep nested element analysis - Add HTMLPreservationEngine for direct HTML manipulation without lossy conversion - Implement structure-preserving content parsing that maintains element positions - Add multi-property element support for links (href + content), images (src + alt), buttons - Create comprehensive test suite with real DOM element validation - Replace markdown-based system foundation with HTML-first architecture - Preserve all element attributes (classes, IDs, data-*, aria-*) during editing - Generate human-readable style names from detected nested elements - Support template extraction with multiple insertion points for complex elements Foundation complete for Phase 2 style-aware editor interface per CLASSES.md specification.
This commit is contained in:
337
lib/src/utils/test-runner.js
Normal file
337
lib/src/utils/test-runner.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Simple test runner for style preservation system
|
||||
* Tests our implementation with actual DOM elements
|
||||
*/
|
||||
|
||||
import { styleDetectionEngine } from './style-detection.js';
|
||||
import { htmlPreservationEngine } from './html-preservation.js';
|
||||
|
||||
/**
|
||||
* Run all style detection tests with real DOM elements
|
||||
*/
|
||||
export function runStyleDetectionTests() {
|
||||
console.log('🧪 Running Style Detection Tests');
|
||||
console.log('================================');
|
||||
|
||||
const results = [];
|
||||
|
||||
// Test 1: Demo Example 1 - Styled Strong Element
|
||||
results.push(testExample1());
|
||||
|
||||
// Test 2: Demo Example 2 - Styled Link Element
|
||||
results.push(testExample2());
|
||||
|
||||
// Test 3: Demo Example 4 - Multiple Styled Elements
|
||||
results.push(testExample4());
|
||||
|
||||
// Test 4: Demo Example 5 - Button with Data Attributes
|
||||
results.push(testExample5());
|
||||
|
||||
// Summary
|
||||
const passed = results.filter(r => r.passed).length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`\n📊 Test Results: ${passed}/${total} passed`);
|
||||
|
||||
if (passed === total) {
|
||||
console.log('✅ All style detection tests passed!');
|
||||
} else {
|
||||
console.log('❌ Some tests failed - see details above');
|
||||
}
|
||||
|
||||
return { passed, total, results };
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Example 1: <p>Hello <strong class="emph">world</strong> and welcome!</p>
|
||||
*/
|
||||
function testExample1() {
|
||||
console.log('\n🔍 Test 1: Styled Strong Element');
|
||||
|
||||
try {
|
||||
// Create test element
|
||||
const container = document.createElement('p');
|
||||
container.className = 'insertr';
|
||||
container.innerHTML = 'Hello <strong class="emph">world</strong> and welcome!';
|
||||
|
||||
// Detect styles
|
||||
const detectedStyles = styleDetectionEngine.detectStyles(container);
|
||||
|
||||
// Validate results
|
||||
const hasEmphStyle = detectedStyles.has('strong_emph');
|
||||
const emphStyle = detectedStyles.get('strong_emph');
|
||||
const correctName = emphStyle?.name === 'Emphasis';
|
||||
const correctTag = emphStyle?.tagName === 'strong';
|
||||
const correctClasses = emphStyle?.classes?.includes('emph');
|
||||
|
||||
const passed = hasEmphStyle && correctName && correctTag && correctClasses;
|
||||
|
||||
console.log(` Detected ${detectedStyles.size} style(s)`);
|
||||
console.log(` Found "Emphasis" style: ${hasEmphStyle ? '✅' : '❌'}`);
|
||||
console.log(` Correct name: ${correctName ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'Example 1', passed, details: { hasEmphStyle, correctName, correctTag, correctClasses } };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'Example 1', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Example 2: <p>Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
|
||||
*/
|
||||
function testExample2() {
|
||||
console.log('\n🔍 Test 2: Styled Link Element');
|
||||
|
||||
try {
|
||||
const container = document.createElement('p');
|
||||
container.className = 'insertr';
|
||||
container.innerHTML = 'Visit our <a class="fancy" href="#about">about page</a> for more info.';
|
||||
|
||||
const detectedStyles = styleDetectionEngine.detectStyles(container);
|
||||
|
||||
const hasFancyStyle = detectedStyles.has('a_fancy');
|
||||
const fancyStyle = detectedStyles.get('a_fancy');
|
||||
const correctName = fancyStyle?.name === 'Fancy Link';
|
||||
const hasHref = fancyStyle?.attributes?.href === '#about';
|
||||
|
||||
const passed = hasFancyStyle && correctName && hasHref;
|
||||
|
||||
console.log(` Detected ${detectedStyles.size} style(s)`);
|
||||
console.log(` Found "Fancy Link" style: ${hasFancyStyle ? '✅' : '❌'}`);
|
||||
console.log(` Preserved href attribute: ${hasHref ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'Example 2', passed, details: { hasFancyStyle, correctName, hasHref } };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'Example 2', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Example 4: Multiple styled elements
|
||||
*/
|
||||
function testExample4() {
|
||||
console.log('\n🔍 Test 4: Multiple Styled Elements');
|
||||
|
||||
try {
|
||||
const container = document.createElement('p');
|
||||
container.className = 'insertr';
|
||||
container.innerHTML = 'Welcome to <strong class="brand">Acme Corp</strong> where we create <span class="highlight">innovative</span> solutions for <em class="emph">modern</em> businesses.';
|
||||
|
||||
const detectedStyles = styleDetectionEngine.detectStyles(container);
|
||||
|
||||
const hasBrand = detectedStyles.has('strong_brand');
|
||||
const hasHighlight = detectedStyles.has('span_highlight');
|
||||
const hasEmphItalic = detectedStyles.has('em_emph');
|
||||
const correctCount = detectedStyles.size === 3;
|
||||
|
||||
const passed = hasBrand && hasHighlight && hasEmphItalic && correctCount;
|
||||
|
||||
console.log(` Detected ${detectedStyles.size} style(s) (expected 3)`);
|
||||
console.log(` Found "Brand" style: ${hasBrand ? '✅' : '❌'}`);
|
||||
console.log(` Found "Highlight" style: ${hasHighlight ? '✅' : '❌'}`);
|
||||
console.log(` Found "Emphasis Italic" style: ${hasEmphItalic ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'Example 4', passed, details: { hasBrand, hasHighlight, hasEmphItalic, correctCount } };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'Example 4', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Example 5: Button with data attributes
|
||||
*/
|
||||
function testExample5() {
|
||||
console.log('\n🔍 Test 5: Button with Data Attributes');
|
||||
|
||||
try {
|
||||
const container = document.createElement('p');
|
||||
container.className = 'insertr';
|
||||
container.innerHTML = 'Ready to start? <button class="btn" data-action="signup" data-analytics="cta-main">Sign Up Now</button> and begin your journey!';
|
||||
|
||||
const detectedStyles = styleDetectionEngine.detectStyles(container);
|
||||
|
||||
const hasButtonStyle = detectedStyles.has('button_btn');
|
||||
const buttonStyle = detectedStyles.get('button_btn');
|
||||
const correctName = buttonStyle?.name === 'Button Style';
|
||||
const hasDataAction = buttonStyle?.attributes?.['data-action'] === 'signup';
|
||||
const hasDataAnalytics = buttonStyle?.attributes?.['data-analytics'] === 'cta-main';
|
||||
|
||||
const passed = hasButtonStyle && correctName && hasDataAction && hasDataAnalytics;
|
||||
|
||||
console.log(` Detected ${detectedStyles.size} style(s)`);
|
||||
console.log(` Found "Button Style": ${hasButtonStyle ? '✅' : '❌'}`);
|
||||
console.log(` Preserved data-action: ${hasDataAction ? '✅' : '❌'}`);
|
||||
console.log(` Preserved data-analytics: ${hasDataAnalytics ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'Example 5', passed, details: { hasButtonStyle, correctName, hasDataAction, hasDataAnalytics } };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'Example 5', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run HTML preservation tests
|
||||
*/
|
||||
export function runHTMLPreservationTests() {
|
||||
console.log('\n🧪 Running HTML Preservation Tests');
|
||||
console.log('==================================');
|
||||
|
||||
const results = [];
|
||||
|
||||
results.push(testHTMLExtraction());
|
||||
results.push(testHTMLApplication());
|
||||
results.push(testAttributePreservation());
|
||||
|
||||
const passed = results.filter(r => r.passed).length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`\n📊 HTML Preservation Results: ${passed}/${total} passed`);
|
||||
|
||||
return { passed, total, results };
|
||||
}
|
||||
|
||||
function testHTMLExtraction() {
|
||||
console.log('\n🔍 HTML Extraction Test');
|
||||
|
||||
try {
|
||||
const element = document.createElement('p');
|
||||
element.className = 'insertr test';
|
||||
element.id = 'test-element';
|
||||
element.innerHTML = 'Hello <strong class="emph">world</strong>!';
|
||||
|
||||
const extracted = htmlPreservationEngine.extractForEditing(element);
|
||||
|
||||
const hasHTML = extracted.html === 'Hello <strong class="emph">world</strong>!';
|
||||
const hasText = extracted.text === 'Hello world!';
|
||||
const hasAttributes = extracted.containerAttributes.class === 'insertr test';
|
||||
const detectsNesting = extracted.hasNestedElements === true;
|
||||
|
||||
const passed = hasHTML && hasText && hasAttributes && detectsNesting;
|
||||
|
||||
console.log(` HTML extraction: ${hasHTML ? '✅' : '❌'}`);
|
||||
console.log(` Text extraction: ${hasText ? '✅' : '❌'}`);
|
||||
console.log(` Attribute preservation: ${hasAttributes ? '✅' : '❌'}`);
|
||||
console.log(` Nesting detection: ${detectsNesting ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'HTML Extraction', passed };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'HTML Extraction', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function testHTMLApplication() {
|
||||
console.log('\n🔍 HTML Application Test');
|
||||
|
||||
try {
|
||||
const element = document.createElement('p');
|
||||
element.className = 'insertr original';
|
||||
|
||||
const newHTML = 'Updated <strong class="emph">content</strong>!';
|
||||
const success = htmlPreservationEngine.applyFromEditing(element, newHTML);
|
||||
|
||||
const appliedCorrectly = element.innerHTML === newHTML;
|
||||
const preservedClass = element.className === 'insertr original';
|
||||
|
||||
const passed = success && appliedCorrectly && preservedClass;
|
||||
|
||||
console.log(` Application success: ${success ? '✅' : '❌'}`);
|
||||
console.log(` Content applied: ${appliedCorrectly ? '✅' : '❌'}`);
|
||||
console.log(` Container class preserved: ${preservedClass ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'HTML Application', passed };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'HTML Application', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function testAttributePreservation() {
|
||||
console.log('\n🔍 Attribute Preservation Test');
|
||||
|
||||
try {
|
||||
const element = document.createElement('div');
|
||||
const originalAttrs = {
|
||||
class: 'insertr test',
|
||||
id: 'unique-id',
|
||||
'data-value': '123'
|
||||
};
|
||||
|
||||
// Apply original attributes
|
||||
Object.entries(originalAttrs).forEach(([key, value]) => {
|
||||
element.setAttribute(key, value);
|
||||
});
|
||||
|
||||
// Extract and restore
|
||||
const extracted = htmlPreservationEngine.extractElementAttributes(element);
|
||||
const newElement = document.createElement('div');
|
||||
htmlPreservationEngine.restoreElementAttributes(newElement, extracted);
|
||||
|
||||
const classRestored = newElement.getAttribute('class') === 'insertr test';
|
||||
const idRestored = newElement.getAttribute('id') === 'unique-id';
|
||||
const dataRestored = newElement.getAttribute('data-value') === '123';
|
||||
|
||||
const passed = classRestored && idRestored && dataRestored;
|
||||
|
||||
console.log(` Class attribute: ${classRestored ? '✅' : '❌'}`);
|
||||
console.log(` ID attribute: ${idRestored ? '✅' : '❌'}`);
|
||||
console.log(` Data attribute: ${dataRestored ? '✅' : '❌'}`);
|
||||
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
return { test: 'Attribute Preservation', passed };
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
console.log(` Result: ❌ FAILED`);
|
||||
return { test: 'Attribute Preservation', passed: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all tests
|
||||
*/
|
||||
export function runAllTests() {
|
||||
console.log('🚀 Running All Style Preservation System Tests');
|
||||
console.log('==============================================');
|
||||
|
||||
const styleResults = runStyleDetectionTests();
|
||||
const htmlResults = runHTMLPreservationTests();
|
||||
|
||||
const totalPassed = styleResults.passed + htmlResults.passed;
|
||||
const totalTests = styleResults.total + htmlResults.total;
|
||||
|
||||
console.log(`\n🎯 Overall Results: ${totalPassed}/${totalTests} tests passed`);
|
||||
|
||||
if (totalPassed === totalTests) {
|
||||
console.log('🎉 All tests passed! Style preservation system is working correctly.');
|
||||
} else {
|
||||
console.log('⚠️ Some tests failed. Review implementation before proceeding.');
|
||||
}
|
||||
|
||||
return {
|
||||
passed: totalPassed === totalTests,
|
||||
details: { styleResults, htmlResults }
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user