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": ["