diff --git a/chat/public/builder.html b/chat/public/builder.html index 9cc39f7..2313648 100644 --- a/chat/public/builder.html +++ b/chat/public/builder.html @@ -55,10 +55,6 @@ flex: 1; overflow-y: auto; min-height: 0; - -webkit-overflow-scrolling: touch; - scroll-behavior: auto; - transform: translateZ(0); - will-change: scroll-position; } .app-shell.builder-single>* { diff --git a/chat/public/builder.js b/chat/public/builder.js index 4ac2a55..f95bf05 100644 --- a/chat/public/builder.js +++ b/chat/public/builder.js @@ -1520,7 +1520,7 @@ function closeHistoryModal() { if (el.historyModal) el.historyModal.style.display = 'none'; } -function scrollChatToBottom() { +function scrollChatToBottom(force = false) { if (!el.chatArea) return; const target = el.chatArea; @@ -1529,48 +1529,91 @@ function scrollChatToBottom() { target.scrollTop = target.scrollHeight; }; - // Use multiple scroll techniques in sequence for reliability - // 1. Immediate scroll + // Use multiple techniques to ensure scroll happens after content is rendered + // 1. Immediate scroll attempt doScroll(); - // 2. After DOM updates - setTimeout(doScroll, 50); - setTimeout(doScroll, 100); - setTimeout(doScroll, 200); - setTimeout(doScroll, 500); + // 2. After short delays to allow DOM updates + setTimeout(() => { doScroll(); }, 10); + setTimeout(() => { doScroll(); }, 50); + setTimeout(() => { doScroll(); }, 100); + setTimeout(() => { doScroll(); }, 250); + setTimeout(() => { doScroll(); }, 500); - // 3. After content fully renders - setTimeout(doScroll, 1000); + // 3. Longer delay for content to fully settle (especially on page load) + setTimeout(() => { doScroll(); }, 1000); + setTimeout(() => { doScroll(); }, 2000); - // 4. requestAnimationFrame for smooth scrolling + // 4. Use requestAnimationFrame for smooth scrolling requestAnimationFrame(() => { doScroll(); - requestAnimationFrame(doScroll); + requestAnimationFrame(() => { + doScroll(); + requestAnimationFrame(() => { + doScroll(); + }); + }); }); - // 5. Scroll to last message element if it exists (most reliable method) - setTimeout(() => { - const lastMessage = target.querySelector('.message:last-child'); - if (lastMessage) { - lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' }); - } else { - // Fallback to regular scroll - doScroll(); + // 5. Wait for ALL images to load before scrolling + const images = target.querySelectorAll('img'); + let imagesToLoad = 0; + + images.forEach((img) => { + if (!img.complete) { + imagesToLoad++; + img.addEventListener('load', () => { + imagesToLoad--; + doScroll(); + // Scroll again after a short delay to ensure layout is updated + setTimeout(() => doScroll(), 100); + }, { once: true }); + + img.addEventListener('error', () => { + imagesToLoad--; + doScroll(); + }, { once: true }); } - }, 100); - - // 6. ResizeObserver for dynamic content - if (typeof ResizeObserver !== 'undefined') { - const resizeObserver = new ResizeObserver(() => doScroll()); - resizeObserver.observe(target); - setTimeout(() => resizeObserver.disconnect(), 2000); + }); + + // If there are images loading, scroll again after they all complete + if (imagesToLoad > 0) { + const checkImagesLoaded = setInterval(() => { + if (imagesToLoad === 0) { + clearInterval(checkImagesLoaded); + doScroll(); + setTimeout(() => doScroll(), 100); + setTimeout(() => doScroll(), 500); + } + }, 100); + + // Stop checking after 5 seconds to avoid infinite interval + setTimeout(() => clearInterval(checkImagesLoaded), 5000); } - - // 7. MutationObserver for DOM changes + + // 6. Use ResizeObserver to detect when content height changes + if (typeof ResizeObserver !== 'undefined') { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === target) { + doScroll(); + } + } + }); + resizeObserver.observe(target); + + // Stop observing after a while + setTimeout(() => resizeObserver.disconnect(), 3000); + } + + // 7. Also scroll when any content finishes rendering (using MutationObserver) if (typeof MutationObserver !== 'undefined') { - const observer = new MutationObserver(() => doScroll()); + const observer = new MutationObserver(() => { + doScroll(); + }); observer.observe(target, { childList: true, subtree: true }); - setTimeout(() => observer.disconnect(), 2000); + // Stop observing after a while to avoid memory leaks + setTimeout(() => observer.disconnect(), 3000); } } @@ -1984,22 +2027,7 @@ function renderMessages(session) { } } - // Scroll to bottom after DOM updates scrollChatToBottom(); - - // Additional scroll with longer delay to ensure all content is rendered - setTimeout(() => { - scrollChatToBottom(); - // Force scroll by accessing scrollHeight directly - if (el.chatArea) { - el.chatArea.scrollTop = el.chatArea.scrollHeight; - // Scroll last element into view as fallback - const lastMessage = el.chatArea.querySelector('.message:last-child'); - if (lastMessage) { - lastMessage.scrollIntoView({ block: 'end' }); - } - } - }, 200); updateExportButtonVisibility(session); } diff --git a/external-wp-testing-plan.md b/external-wp-testing-plan.md new file mode 100644 index 0000000..7f61830 --- /dev/null +++ b/external-wp-testing-plan.md @@ -0,0 +1,963 @@ +# PluginCompass External WordPress Testing System + +## Executive Summary + +A production-ready testing system that uses an externally hosted WordPress site with WP-CLI to verify plugin functionality. Supports high concurrency (20+ sessions) through multisite subsite isolation, with two testing modes (automated WP-CLI commands and visual browser testing). + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PluginCompass CLI Users │ +│ (20+ Concurrent Sessions) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Test Orchestrator Layer │ +│ - Session Queue Manager (prevents conflicts) │ +│ - Subsite Provisioning (auto-creates test sites) │ +│ - Resource Cleanup (automatic teardown) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ External WordPress Multisite │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Session 1 │ │ Session 2 │ │ Session N │ │ +│ │ Subsite │ │ Subsite │ │ Subsite │ │ +│ │ /test-abc │ │ /test-def │ │ /test-xyz │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ Shared: Core WP, Plugins, Themes (isolated per subsite) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Core Components + +### 1. Test Orchestrator (`test_orchestrator.js`) + +Central controller managing all test sessions. + +**Responsibilities:** +- Queue incoming test requests +- Provision subsites automatically +- Manage plugin installations +- Coordinate cleanup +- Prevent cross-session contamination + +**Configuration:** +```javascript +const TEST_CONFIG = { + // External WP Site Connection + wpHost: process.env.TEST_WP_HOST || 'testsite.example.com', + wpSshUser: process.env.TEST_WP_SSH_USER || 'wordpress', + wpSshKey: process.env.TEST_WP_SSH_KEY || '~/.ssh/wp-test-key', + wpPath: process.env.TEST_WP_PATH || '/var/www/html', + + // Multisite Settings + enableMultisite: true, + subsitePrefix: 'test', + subsiteDomain: 'testsite.example.com', + + // Concurrency + maxConcurrentTests: 20, + queueTimeout: 300000, // 5 minutes + testTimeout: 600000, // 10 minutes + + // Cleanup + autoCleanup: true, + cleanupDelay: 3600000, // 1 hour after test completion + + // Testing Modes + enableVisualTesting: process.env.ENABLE_VISUAL_TESTING === 'true', + visualTestBrowser: 'chromium', // chromium, firefox, webkit +}; +``` + +--- + +### 2. New Tool: `test_plugin_external_wp` + +**Tool Name:** `test_plugin_external_wp` + +**Description:** Deploys and tests plugin on external WordPress site with full isolation. Automatically provisions subsite, installs dependencies, runs verification tests, and provides detailed results. + +**Parameters:** +```json +{ + "name": "test_plugin_external_wp", + "parameters": { + "type": "object", + "properties": { + "plugin_path": { + "type": "string", + "description": "Local path to generated plugin directory" + }, + "test_mode": { + "type": "string", + "enum": ["cli", "visual", "both"], + "default": "cli", + "description": "Testing mode: cli (WP-CLI commands), visual (browser automation), or both" + }, + "required_plugins": { + "type": "array", + "description": "Other plugins that must be installed for this plugin to work", + "items": { + "type": "object", + "properties": { + "plugin_slug": { "type": "string" }, + "version": { "type": "string" }, + "source": { + "type": "string", + "enum": ["wordpress.org", "url", "local"], + "default": "wordpress.org" + }, + "source_url": { "type": "string" }, + "activate": { "type": "boolean", "default": true } + }, + "required": ["plugin_slug"] + } + }, + "test_scenarios": { + "type": "array", + "description": "Specific test scenarios to verify", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "type": { + "type": "string", + "enum": ["endpoint", "admin_page", "ajax", "shortcode", "hook", "visual", "custom"] + }, + "url": { "type": "string" }, + "selector": { "type": "string" }, + "expected_text": { "type": "string" }, + "wp_cli_command": { "type": "string" }, + "assertions": { + "type": "object", + "properties": { + "status_code": { "type": "number" }, + "contains": { "type": "array", "items": { "type": "string" } }, + "not_contains": { "type": "array", "items": { "type": "string" } }, + "wp_cli_success": { "type": "boolean" } + } + } + }, + "required": ["name", "type"] + } + }, + "visual_tests": { + "type": "array", + "description": "Visual testing scenarios (only used when test_mode is 'visual' or 'both')", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "url": { "type": "string" }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["goto", "click", "fill", "wait", "screenshot", "evaluate"] + }, + "selector": { "type": "string" }, + "value": { "type": "string" }, + "timeout": { "type": "number" } + } + } + }, + "assertions": { + "type": "object", + "properties": { + "visible": { "type": "array", "items": { "type": "string" } }, + "hidden": { "type": "array", "items": { "type": "string" } }, + "text_contains": { "type": "array", "items": { "type": "string" } }, + "no_console_errors": { "type": "boolean" } + } + } + } + } + }, + "timeout": { + "type": "number", + "default": 300, + "description": "Test timeout in seconds" + }, + "auto_fix": { + "type": "boolean", + "default": true, + "description": "Automatically retry and fix on test failure" + }, + "max_fix_attempts": { + "type": "number", + "default": 3, + "description": "Maximum auto-fix attempts before giving up" + } + }, + "required": ["plugin_path"] + } +} +``` + +**Return Value:** +```javascript +{ + "ok": true, + "session_id": "sess_abc123", + "subsite_url": "https://testsite.example.com/test-abc123", + "test_results": { + "mode": "both", + "cli_tests": { + "passed": 5, + "failed": 0, + "results": [ + { + "name": "Plugin activates without errors", + "status": "passed", + "command": "wp plugin activate test-plugin", + "output": "Success: Activated 1 of 1 plugins.", + "duration": 2340 + }, + { + "name": "Custom login endpoint responds", + "status": "passed", + "command": "wp eval 'echo wp_remote_retrieve_body(wp_remote_get(home_url(\"/custom-login\")));'", + "assertions": { + "contains": ["=8.0", + "source": "wordpress.org", + "activate": true + }, + { + "plugin_slug": "advanced-custom-fields", + "version": "6.2.0", + "source": "wordpress.org", + "activate": true + }, + { + "plugin_slug": "custom-premium-plugin", + "source": "url", + "source_url": "https://example.com/plugins/custom.zip", + "activate": true + } + ] +} +``` + +**Installation Process:** +```javascript +class PluginInstaller { + async installPlugin(subsiteUrl, pluginSpec) { + const { plugin_slug, source, source_url, activate } = pluginSpec; + + switch (source) { + case 'wordpress.org': + await this.runWpCli(` + wp --url=${subsiteUrl} plugin install ${plugin_slug} --activate + `); + break; + + case 'url': + // Download and install from URL + await this.runWpCli(` + wp --url=${subsiteUrl} plugin install ${source_url} --activate + `); + break; + + case 'local': + // Copy from local path to subsite + const targetPath = `${this.config.wpPath}/wp-content/plugins/${plugin_slug}`; + await this.ssh.copyFile(source_url, targetPath); + if (activate) { + await this.runWpCli(` + wp --url=${subsiteUrl} plugin activate ${plugin_slug} + `); + } + break; + } + } +} +``` + +--- + +## Queue Management for High Concurrency + +### Session Queue System + +**Challenge:** Even with multisite, some operations (like subsite creation) can have race conditions. + +**Solution:** Queue system with parallel execution lanes + +```javascript +class TestQueue { + constructor(maxConcurrent = 20) { + this.maxConcurrent = maxConcurrent; + this.activeSessions = new Map(); + this.pendingQueue = []; + this.lanes = new Array(maxConcurrent).fill(null); // Execution lanes + } + + async enqueue(testRequest) { + const sessionId = this.generateSessionId(); + + // Find available lane + const laneIndex = this.findAvailableLane(); + + if (laneIndex === -1) { + // All lanes busy, add to pending + return new Promise((resolve) => { + this.pendingQueue.push({ + sessionId, + request: testRequest, + resolve, + enqueuedAt: Date.now() + }); + }); + } + + // Execute immediately in available lane + return this.executeInLane(laneIndex, sessionId, testRequest); + } + + async executeInLane(laneIndex, sessionId, request) { + this.lanes[laneIndex] = sessionId; + + try { + const result = await this.runTest(sessionId, request); + return result; + } finally { + this.lanes[laneIndex] = null; + this.processQueue(); + } + } + + processQueue() { + if (this.pendingQueue.length === 0) return; + + const laneIndex = this.findAvailableLane(); + if (laneIndex === -1) return; + + const next = this.pendingQueue.shift(); + + // Check for timeout + if (Date.now() - next.enqueuedAt > TEST_CONFIG.queueTimeout) { + next.resolve({ + ok: false, + error: 'Test queue timeout - too many concurrent tests' + }); + this.processQueue(); + return; + } + + this.executeInLane(laneIndex, next.sessionId, next.request) + .then(next.resolve) + .catch(error => next.resolve({ ok: false, error: error.message })); + } +} +``` + +--- + +## Complete Workflow + +### Step-by-Step Process + +``` +User Request + ↓ +AI Generates Plugin Code + ↓ +AI Calls test_plugin_external_wp + ↓ +┌─────────────────────────────────────┐ +│ 1. QUEUE & PREPARE │ +│ - Add to test queue │ +│ - Wait for available lane │ +│ - Generate session ID │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 2. PROVISION SUBSITE │ +│ - Create multisite subsite │ +│ - Configure permalinks │ +│ - Set up test database tables │ +│ Duration: 3-5 seconds │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 3. INSTALL DEPENDENCIES │ +│ - Install WordPress (if needed) │ +│ - Install WooCommerce │ +│ - Install other required plugins │ +│ - Activate all │ +│ Duration: 10-30 seconds │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 4. DEPLOY TEST PLUGIN │ +│ - Copy plugin files via SFTP │ +│ - Activate plugin │ +│ - Check for activation errors │ +│ Duration: 2-5 seconds │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 5. RUN TESTS │ +│ CLI Mode: │ +│ - WP-CLI verification commands │ +│ - HTTP endpoint checks │ +│ - Database state validation │ +│ - Error log scanning │ +│ │ +│ Visual Mode (if enabled): │ +│ - Launch headless browser │ +│ - Navigate to test URLs │ +│ - Execute user interactions │ +│ - Capture screenshots │ +│ - Check JavaScript console │ +│ Duration: 10-60 seconds │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 6. EVALUATE & AUTO-FIX (if needed) │ +│ IF tests failed AND auto_fix=true:│ +│ - Analyze failure patterns │ +│ - Generate fix suggestions │ +│ - Modify plugin code │ +│ - Re-deploy and re-test │ +│ - Max 3 attempts │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 7. RETURN RESULTS │ +│ - Compile test results │ +│ - Include screenshots (visual) │ +│ - Provide fix recommendations │ +│ - Schedule cleanup │ +└─────────────────────────────────────┘ + ↓ +AI Receives Results + ↓ +IF passed → Complete task +IF failed → Fix issues and re-call tool + ↓ +┌─────────────────────────────────────┐ +│ 8. CLEANUP (async, after delay) │ +│ - Delete subsite │ +│ - Remove plugin files │ +│ - Clear test data │ +│ - Free up lane │ +└─────────────────────────────────────┘ +``` + +--- + +## Error Handling & Auto-Fix + +### Common Failures & Fixes + +```javascript +const AUTO_FIX_PATTERNS = [ + { + pattern: /Rewrite rules not working/i, + check: async (subsite) => { + const rules = await runWpCli(`wp --url=${subsite} rewrite list`); + return !rules.includes('custom-login'); + }, + fix: async (pluginPath) => { + // Add flush_rewrite_rules() to activation hook + await editFile(pluginPath + '/includes/activation.php', + 'function activate_plugin() {', + 'function activate_plugin() {\n flush_rewrite_rules();' + ); + } + }, + { + pattern: /Undefined function.*wc_/i, + check: async (subsite) => { + const logs = await runWpCli(`tail -n 20 /var/log/wp-errors.log`); + return logs.includes('Call to undefined function wc_'); + }, + fix: async (pluginPath) => { + // Add WooCommerce dependency check + await editFile(pluginPath + '/main.php', + ' { + const html = await runWpCli(`wp --url=${subsite} eval 'echo wp_remote_retrieve_body(wp_remote_get(home_url("/custom-login")));'`); + return !html.includes('custom-login.css'); + }, + fix: async (pluginPath) => { + // Ensure CSS is enqueued + await editFile(pluginPath + '/includes/enqueue.php', + '', + 'wp_enqueue_style(\'custom-login-css\', plugins_url(\'css/custom-login.css\', __FILE__));' + ); + } + } +]; +``` + +--- + +## Configuration & Environment Variables + +### Required Environment Variables + +```bash +# WordPress Test Site Connection +TEST_WP_HOST=testsite.example.com +TEST_WP_SSH_USER=wordpress +TEST_WP_SSH_KEY_PATH=/path/to/ssh/key +TEST_WP_PATH=/var/www/html + +# Multisite Configuration +TEST_WP_MULTISITE=true +TEST_WP_SUBSITE_DOMAIN=testsite.example.com + +# Testing Configuration +TEST_ENABLE_VISUAL=true +TEST_VISUAL_BROWSER=chromium +TEST_MAX_CONCURRENT=20 +TEST_TIMEOUT=300 +TEST_AUTO_CLEANUP=true +TEST_CLEANUP_DELAY=3600 + +# Optional: Browser Testing (for visual mode) +TEST_BROWSER_HEADLESS=true +TEST_BROWSER_TIMEOUT=30000 +TEST_SCREENSHOT_PATH=/var/results/screenshots +``` + +--- + +## Implementation Phases + +### Phase 1: Core Infrastructure (Week 1) +- [ ] Set up multisite WordPress +- [ ] Implement SSH/SFTP connection layer +- [ ] Create `test_plugin_external_wp` tool +- [ ] Build subsite provisioning system +- [ ] Basic CLI test mode + +### Phase 2: Testing Modes (Week 2) +- [ ] Implement CLI test scenarios +- [ ] Add visual testing with Playwright +- [ ] Create queue management system +- [ ] Build dependency installer + +### Phase 3: Auto-Fix & Polish (Week 3) +- [ ] Implement auto-fix patterns +- [ ] Add retry logic +- [ ] Error classification +- [ ] Result formatting for AI + +### Phase 4: Production Ready (Week 4) +- [ ] Concurrent session testing +- [ ] Performance optimization +- [ ] Documentation +- [ ] Monitoring & alerting + +--- + +## Example Usage in AI Workflow + +### Example 1: Custom Login Plugin + +**User:** "Create a plugin that adds a custom login page at /vendor-login" + +**AI Actions:** +1. Generates plugin code +2. Calls `test_plugin_external_wp`: + +```javascript +{ + "plugin_path": "./vendor-login-plugin", + "test_mode": "both", + "required_plugins": [], + "test_scenarios": [ + { + "name": "Endpoint accessible", + "type": "endpoint", + "url": "/vendor-login", + "assertions": { + "status_code": 200, + "contains": ["payment_gateways->payment_gateways()));'", + "assertions": { + "contains": ["custom_gateway"] + } + }, + { + "name": "Checkout loads without errors", + "type": "endpoint", + "url": "/checkout", + "assertions": { + "status_code": 200, + "not_contains": ["Fatal error", "Warning"] + } + } + ], + "visual_tests": [ + { + "name": "Gateway appears in checkout", + "url": "/checkout", + "actions": [ + { "action": "goto", "url": "/product/test-product" }, + { "action": "click", "selector": ".add_to_cart_button" }, + { "action": "wait", "timeout": 2000 }, + { "action": "goto", "url": "/checkout" }, + { "action": "screenshot", "name": "checkout-gateway" } + ], + "assertions": { + "visible": ["#payment_method_custom_gateway"], + "no_console_errors": true + } + } + ] +} +``` + +--- + +## Monitoring & Maintenance + +### Health Checks + +```javascript +// Periodic health check +async function healthCheck() { + const checks = { + ssh: await checkSshConnection(), + wp_cli: await checkWpCli(), + multisite: await checkMultisiteEnabled(), + disk_space: await checkDiskSpace(), + queue_length: testQueue.pendingQueue.length + }; + + if (Object.values(checks).some(c => !c.ok)) { + await alertAdmin(checks); + } +} +``` + +### Metrics to Track +- Test queue length and wait times +- Average test duration per mode +- Failure rate by plugin type +- Auto-fix success rate +- Resource usage (disk, memory) + +--- + +## Summary + +This system provides: + +1. **Full Isolation:** Each test runs in its own multisite subsite +2. **High Concurrency:** Queue system handles 20+ simultaneous tests +3. **Two Testing Modes:** + - CLI mode: Fast, deterministic WP-CLI testing + - Visual mode: Real browser automation with screenshots +4. **Automatic Setup:** Provisions subsite, installs dependencies, deploys plugin +5. **Auto-Fix:** Attempts to fix common failures automatically +6. **Proof of Work:** Returns screenshots, logs, and detailed test results +7. **Cleanup:** Automatic teardown after test completion + +**Result:** Users receive plugins that are proven to work on a real WordPress environment, with objective evidence of functionality.