@semantest/chrome-extension
Version:
Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework
389 lines (388 loc) • 14.1 kB
JavaScript
// Web-Buddy content script for DOM automation
// Handles automation commands from the background script
import { webBuddyStorage } from './storage';
// Store test data for E2E testing
window.extensionTestData = {
lastReceivedMessage: null,
lastResponse: null,
webSocketMessages: []
};
// Generate session ID for tracking user interactions
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const currentDomain = window.location.hostname;
const currentUrl = window.location.href;
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
console.log('📨 Content script received message:', message);
// Store for E2E testing
window.extensionTestData.lastReceivedMessage = message;
let response;
try {
// Handle different event types
if (message.type === 'automationRequested') {
response = await handleAutomationRequest(message);
}
else if (message.type === 'ping') {
response = handlePingMessage(message);
}
else if (message.type === 'storageRequest') {
response = await handleStorageRequest(message);
}
else {
// Handle legacy action-based messages for backward compatibility
response = await handleLegacyAction(message);
}
}
catch (error) {
console.error('❌ Error in content script:', error);
response = {
correlationId: message.correlationId,
status: 'error',
error: error.message || 'Unknown content script error',
timestamp: new Date().toISOString()
};
}
// Store response for E2E testing
window.extensionTestData.lastResponse = response;
// Persist automation pattern if it was successful
if (response.status === 'success' && message.type === 'automationRequested') {
await persistAutomationPattern(message, response);
}
sendResponse(response);
return true; // Keep message channel open for async responses
});
// Helper function to persist automation patterns
async function persistAutomationPattern(message, response) {
try {
const { payload } = message;
const { action, parameters } = payload;
const pattern = {
url: currentUrl,
domain: currentDomain,
action: action,
selector: parameters.selector || '',
parameters: parameters,
success: response.status === 'success',
contextHash: generateContextHash(),
userConfirmed: false // Will be updated when user confirms the pattern
};
await webBuddyStorage.saveAutomationPattern(pattern);
console.log('💾 Automation pattern persisted:', action);
}
catch (error) {
console.error('❌ Failed to persist automation pattern:', error);
}
}
// Helper function to save user interactions
async function saveUserInteraction(eventType, target, success, context = {}) {
try {
const interaction = {
sessionId,
url: currentUrl,
domain: currentDomain,
eventType,
target,
success,
context: {
...context,
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
}
};
await webBuddyStorage.saveUserInteraction(interaction);
console.log('📊 User interaction saved:', eventType);
}
catch (error) {
console.error('❌ Failed to save user interaction:', error);
}
}
// Generate context hash for pattern matching
function generateContextHash() {
const context = {
domain: currentDomain,
path: window.location.pathname,
title: document.title,
bodyClasses: Array.from(document.body.classList).sort().join(' '),
metaDescription: document.querySelector('meta[name="description"]')?.content || '',
elementCount: document.querySelectorAll('*').length
};
return btoa(JSON.stringify(context)).slice(0, 16);
}
// Enhanced automation handler with pattern matching
async function handleAutomationRequest(message) {
const { payload, correlationId } = message;
const { action, parameters } = payload;
console.log(`🎯 Executing automation: ${action}`, parameters);
// Check for existing patterns for this action
const existingPatterns = await webBuddyStorage.getAutomationPatterns({
domain: currentDomain,
action: action,
successOnly: true,
limit: 5
});
if (existingPatterns.length > 0) {
console.log(`📚 Found ${existingPatterns.length} existing patterns for ${action}`);
// Try to apply the most successful pattern first
for (const pattern of existingPatterns) {
try {
const result = await applyAutomationPattern(pattern, parameters, correlationId);
if (result.status === 'success') {
console.log('✅ Applied existing pattern successfully');
return result;
}
}
catch (error) {
console.log('⚠️ Existing pattern failed, trying next...');
}
}
}
// Fall back to standard action handling
return handleStandardAction(action, parameters, correlationId);
}
// Apply an existing automation pattern
async function applyAutomationPattern(pattern, newParameters, correlationId) {
// Merge pattern parameters with new parameters (new parameters take precedence)
const mergedParameters = { ...pattern.parameters, ...newParameters };
return handleStandardAction(pattern.action, mergedParameters, correlationId);
}
// Standard action handler (existing logic)
async function handleStandardAction(action, parameters, correlationId) {
try {
switch (action) {
case 'fillInput':
return await handleFillInput(parameters, correlationId);
case 'clickElement':
return await handleClickElement(parameters, correlationId);
case 'getText':
return await handleGetText(parameters, correlationId);
case 'testAction':
return await handleTestAction(parameters, correlationId);
default:
throw new Error(`Unknown automation action: ${action}`);
}
}
catch (error) {
return {
correlationId,
status: 'error',
error: error.message,
timestamp: new Date().toISOString()
};
}
}
async function handleFillInput(parameters, correlationId) {
const { selector, value } = parameters;
const element = document.querySelector(selector);
if (!element) {
// Save failed interaction
await saveUserInteraction('fillInput', selector, false, {
error: 'Element not found',
value: value
});
throw new Error(`Element not found: ${selector}`);
}
if (element.tagName.toLowerCase() !== 'input' && element.tagName.toLowerCase() !== 'textarea') {
// Save failed interaction
await saveUserInteraction('fillInput', selector, false, {
error: 'Invalid element type',
actualTag: element.tagName.toLowerCase(),
value: value
});
throw new Error(`Element is not an input or textarea: ${selector}`);
}
// Perform the action
const oldValue = element.value;
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
// Save successful interaction
await saveUserInteraction('fillInput', selector, true, {
oldValue: oldValue,
newValue: value,
elementTag: element.tagName.toLowerCase(),
elementType: element.type || 'text'
});
return {
correlationId,
status: 'success',
data: {
action: 'fillInput',
selector,
value,
elementTag: element.tagName.toLowerCase(),
oldValue: oldValue
},
timestamp: new Date().toISOString()
};
}
async function handleClickElement(parameters, correlationId) {
const { selector } = parameters;
const element = document.querySelector(selector);
if (!element) {
// Save failed interaction
await saveUserInteraction('clickElement', selector, false, {
error: 'Element not found'
});
throw new Error(`Element not found: ${selector}`);
}
// Capture element info before click
const elementInfo = {
tag: element.tagName.toLowerCase(),
text: element.textContent?.slice(0, 100) || '',
className: element.className,
id: element.id,
href: element.href || undefined,
disabled: element.disabled || false
};
// Perform the click
element.click();
// Save successful interaction
await saveUserInteraction('clickElement', selector, true, {
elementInfo: elementInfo,
clickType: 'automated'
});
return {
correlationId,
status: 'success',
data: {
action: 'clickElement',
selector,
elementTag: element.tagName.toLowerCase(),
elementInfo: elementInfo
},
timestamp: new Date().toISOString()
};
}
async function handleGetText(parameters, correlationId) {
const { selector } = parameters;
const element = document.querySelector(selector);
if (!element) {
// Save failed interaction
await saveUserInteraction('getText', selector, false, {
error: 'Element not found'
});
throw new Error(`Element not found: ${selector}`);
}
const text = element.textContent || element.innerHTML;
// Save successful interaction
await saveUserInteraction('getText', selector, true, {
textLength: text.length,
elementTag: element.tagName.toLowerCase(),
hasText: !!element.textContent,
hasHtml: !!element.innerHTML
});
return {
correlationId,
status: 'success',
data: {
action: 'getText',
selector,
text,
elementTag: element.tagName.toLowerCase()
},
timestamp: new Date().toISOString()
};
}
async function handleTestAction(parameters, correlationId) {
// Simple test action for E2E verification
// Save test interaction
await saveUserInteraction('testAction', 'test-target', true, {
message: parameters.message || 'Test action executed successfully',
parameters: parameters
});
return {
correlationId,
status: 'success',
data: {
action: 'testAction',
message: parameters.message || 'Test action executed successfully',
timestamp: new Date().toISOString(),
url: window.location.href,
title: document.title
},
timestamp: new Date().toISOString()
};
}
function handlePingMessage(message) {
return {
type: 'pong',
correlationId: message.correlationId,
payload: {
originalMessage: message.payload || 'ping',
url: window.location.href,
title: document.title,
timestamp: new Date().toISOString()
}
};
}
async function handleStorageRequest(message) {
const { action, payload, correlationId } = message;
console.log(`💾 Handling storage request: ${action}`);
try {
switch (action) {
case 'getStorageStats':
const stats = await webBuddyStorage.getStorageStats();
return {
correlationId,
success: true,
stats: stats,
timestamp: new Date().toISOString()
};
case 'getAutomationPatterns':
const patterns = await webBuddyStorage.getAutomationPatterns({
domain: currentDomain,
limit: payload.limit || 10
});
return {
correlationId,
success: true,
patterns: patterns,
timestamp: new Date().toISOString()
};
case 'clearOldData':
await webBuddyStorage.clearOldData(payload.days || 30);
return {
correlationId,
success: true,
message: `Cleared data older than ${payload.days || 30} days`,
timestamp: new Date().toISOString()
};
default:
throw new Error(`Unknown storage action: ${action}`);
}
}
catch (error) {
console.error('❌ Storage request failed:', error);
return {
correlationId,
success: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
async function handleLegacyAction(message) {
// Handle legacy action-based messages for backward compatibility
const { action, correlationId } = message;
console.log(`🔄 Handling legacy action: ${action}`);
// Save legacy interaction
await saveUserInteraction('legacyAction', action, true, {
actionType: action,
message: message
});
return {
correlationId,
status: 'success',
data: `Legacy action ${action} executed successfully (placeholder)`,
timestamp: new Date().toISOString()
};
}
// Notify background script that content script is ready
chrome.runtime.sendMessage({
type: "CONTENT_SCRIPT_READY",
url: window.location.href,
timestamp: new Date().toISOString()
});
console.log('🚀 Web-Buddy content script loaded on:', window.location.href);