Skip to main content
Glama
improved-composition-lifecycle.ts19.3 kB
/** * Improved Composition Lifecycle with Enhanced DOM Selectors * * @version 0.2.0 * @created 2025-06-27 * @author Claude Code (TASK-001 Implementation) * * Improvements: * - Enhanced selector strategies with more fallbacks * - Better error handling and logging * - Robust wait conditions * - Visual debugging capabilities * - Network error handling */ import { z } from "zod"; import { chromium, Browser, Page } from "playwright"; /** * Enhanced browser automation client with improved selectors */ export class ImprovedComposerClient { private browser: Browser | null = null; private page: Page | null = null; private readonly baseURL = "https://composer.euconquisto.com/#/embed"; private readonly orgId = process.env.EUCONQUISTO_ORG_ID || "36c92686-c494-ec11-a22a-dc984041c95d"; private readonly jwtToken = process.env.EUCONQUISTO_JWT_TOKEN || ""; constructor() { if (!this.jwtToken) { throw new Error('EUCONQUISTO_JWT_TOKEN environment variable is required'); } } /** * Initialize browser with enhanced configuration */ async initialize(): Promise<void> { try { this.browser = await chromium.launch({ headless: process.env.NODE_ENV === 'production', // Visible in development args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', // Help with potential CORS issues '--disable-features=VizDisplayCompositor' ], timeout: 60000 }); this.page = await this.browser.newPage(); // Enhanced timeouts and settings this.page.setDefaultTimeout(45000); this.page.setDefaultNavigationTimeout(90000); // Add console logging for debugging this.page.on('console', msg => console.log(`PAGE LOG: ${msg.text()}`)); this.page.on('pageerror', error => console.error(`PAGE ERROR: ${error.message}`)); console.log('✅ Enhanced browser automation initialized'); } catch (error) { console.error('❌ Failed to initialize browser:', error); throw error; } } /** * Navigate with enhanced error handling */ async navigateToComposer(path: string = 'home'): Promise<void> { if (!this.page) { throw new Error('Browser not initialized. Call initialize() first.'); } const embedURL = `${this.baseURL}/auth-with-token/pt_br/${path}/${this.orgId}/${this.jwtToken}`; try { console.log('🌐 Navigating to Composer...'); console.log(`🔗 URL: ${embedURL.substring(0, 80)}...`); // Navigate with multiple strategies const response = await this.page.goto(embedURL, { waitUntil: 'domcontentloaded', timeout: 90000 }); if (!response || !response.ok()) { throw new Error(`Navigation failed with status: ${response?.status()}`); } // Wait for authentication to complete with multiple conditions await Promise.race([ this.page.waitForSelector('body', { timeout: 30000 }), this.page.waitForLoadState('networkidle', { timeout: 45000 }), this.page.waitForFunction(() => (globalThis as any).document.readyState === 'complete', { timeout: 30000 }) ]); // Take debug screenshot await this.page.screenshot({ path: `debug-composer-${Date.now()}.png`, fullPage: true }); console.log('✅ Successfully navigated and authenticated'); } catch (error) { console.error('❌ Navigation failed:', error); // Take error screenshot for debugging if (this.page) { await this.page.screenshot({ path: `error-navigation-${Date.now()}.png`, fullPage: true }); } throw error; } } /** * Enhanced "Create New Composition" with modern selector strategies */ async createNewComposition(): Promise<{ success: boolean; message: string; compositionId?: string }> { if (!this.page) { throw new Error('Browser not initialized'); } try { console.log('🎯 Creating new composition with enhanced selectors...'); // Wait for page to be fully loaded await this.page.waitForLoadState('networkidle'); // Enhanced selector strategies for "Nova composição" button const newCompositionSelectors = [ // Data attributes (most reliable) '[data-testid="new-composition"]', '[data-cy="new-composition"]', '[data-id="new-composition"]', // Specific text matching 'button:has-text("Nova composição")', 'a:has-text("Nova composição")', // CSS class patterns '.new-composition', '.new-composition-btn', '.btn-new-composition', '.create-composition', '.composition-create', // Role and aria attributes 'button[role="button"]:has-text("Nova")', '[aria-label*="Nova composição"]', '[aria-label*="Criar composição"]', '[aria-label*="New composition"]', // Generic button patterns with text 'button:text("Nova composição")', 'button:text("Criar")', 'button:text("Novo")', 'a:text("Nova composição")', // Icon + text combinations 'button:has(i):has-text("Nova")', 'button:has(.fa-plus):has-text("Nova")', 'button:has(.icon-plus)', // Material Design patterns '.mat-button:has-text("Nova")', '.mat-raised-button:has-text("Nova")', '.mdc-button:has-text("Nova")', // Bootstrap patterns '.btn:has-text("Nova")', '.btn-primary:has-text("Nova")', '.btn-success:has-text("Nova")' ]; let clicked = false; let successfulSelector = ''; // Try each selector with individual error handling for (const selector of newCompositionSelectors) { try { console.log(`🔍 Trying selector: ${selector}`); const element = await this.page.waitForSelector(selector, { timeout: 3000 }); if (element) { // Verify element is visible and clickable const isVisible = await element.isVisible(); const isEnabled = await element.isEnabled(); if (isVisible && isEnabled) { // Scroll element into view await element.scrollIntoViewIfNeeded(); // Click with enhanced options await element.click({ timeout: 5000 }); clicked = true; successfulSelector = selector; console.log(`✅ Successfully clicked with selector: ${selector}`); break; } else { console.log(`⚠️ Element found but not clickable: visible=${isVisible}, enabled=${isEnabled}`); } } } catch (error) { console.log(`❌ Selector failed: ${selector} - ${error instanceof Error ? error.message : 'Unknown error'}`); continue; } } // Fallback: Manual text search if (!clicked) { console.log('🔄 Trying fallback text search...'); const allButtons = await this.page.locator('button, a, [role="button"]').all(); console.log(`Found ${allButtons.length} clickable elements`); for (let i = 0; i < allButtons.length; i++) { try { const element = allButtons[i]; const text = (await element.textContent())?.toLowerCase() || ''; if (text.includes('nova') || text.includes('composição') || text.includes('criar') || text.includes('novo') || text.includes('create') || text.includes('new')) { console.log(`🎯 Found potential button with text: "${text}"`); const isVisible = await element.isVisible(); if (isVisible) { await element.scrollIntoViewIfNeeded(); await element.click(); clicked = true; successfulSelector = `text-search-${i}`; console.log(`✅ Clicked via text search: "${text}"`); break; } } } catch (error) { continue; // Skip to next element } } } if (!clicked) { // Take screenshot for debugging await this.page.screenshot({ path: `debug-no-composition-button-${Date.now()}.png`, fullPage: true }); throw new Error('Could not find "Nova composição" button with any selector strategy'); } // Wait for navigation or modal console.log('⏱️ Waiting for navigation or modal...'); await Promise.race([ this.page.waitForURL(/.*/, { timeout: 10000 }), this.page.waitForSelector('.modal, .dialog, [role="dialog"]', { timeout: 10000 }), this.page.waitForTimeout(3000) // Minimum wait ]); // Extract composition ID from URL const currentURL = this.page.url(); const compositionId = this.extractCompositionIdFromURL(currentURL); // Take success screenshot await this.page.screenshot({ path: `debug-composition-created-${Date.now()}.png`, fullPage: true }); return { success: true, message: `New composition created successfully using selector: ${successfulSelector}`, compositionId: compositionId || 'new' }; } catch (error) { console.error('❌ Failed to create new composition:', error); // Take error screenshot await this.page.screenshot({ path: `error-create-composition-${Date.now()}.png`, fullPage: true }); return { success: false, message: `Failed to create new composition: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Enhanced "Save Composition" with modern selector strategies */ async saveComposition(): Promise<{ success: boolean; message: string; compositionURL?: string }> { if (!this.page) { throw new Error('Browser not initialized'); } try { console.log('💾 Saving composition with enhanced selectors...'); // Enhanced selector strategies for "Salvar" button const saveSelectors = [ // Data attributes '[data-testid="save-composition"]', '[data-testid="save"]', '[data-cy="save"]', '[data-id="save"]', // Specific text matching 'button:has-text("Salvar")', 'button:has-text("Save")', 'button:has-text("Guardar")', // CSS class patterns '.save-composition', '.save-btn', '.btn-save', '.composition-save', // Role and aria attributes 'button[aria-label*="salvar"]', 'button[aria-label*="save"]', 'button[title*="Salvar"]', 'button[title*="Save"]', // Form submission patterns 'button[type="submit"]', 'input[type="submit"]', 'button[form]', // Icon patterns 'button:has(.fa-save)', 'button:has(.icon-save)', 'button:has(.material-icons):has-text("save")', // Material Design '.mat-button:has-text("Salvar")', '.mdc-button:has-text("Salvar")', // Bootstrap '.btn-primary:has-text("Salvar")', '.btn-success:has-text("Salvar")' ]; let saved = false; let successfulSelector = ''; for (const selector of saveSelectors) { try { console.log(`🔍 Trying save selector: ${selector}`); const element = await this.page.waitForSelector(selector, { timeout: 3000 }); if (element) { const isVisible = await element.isVisible(); const isEnabled = await element.isEnabled(); if (isVisible && isEnabled) { await element.scrollIntoViewIfNeeded(); await element.click({ timeout: 5000 }); saved = true; successfulSelector = selector; console.log(`✅ Successfully saved with selector: ${selector}`); break; } } } catch (error) { console.log(`❌ Save selector failed: ${selector} - ${error instanceof Error ? error.message : 'Unknown error'}`); continue; } } // Fallback: Text search for save buttons if (!saved) { console.log('🔄 Trying fallback save text search...'); const allButtons = await this.page.locator('button, input[type="submit"], [role="button"]').all(); for (const element of allButtons) { try { const text = (await element.textContent())?.toLowerCase() || ''; const value = (await element.getAttribute('value'))?.toLowerCase() || ''; if (text.includes('salvar') || text.includes('save') || text.includes('guardar') || value.includes('salvar') || value.includes('save')) { console.log(`🎯 Found save button with text: "${text || value}"`); const isVisible = await element.isVisible(); if (isVisible) { await element.scrollIntoViewIfNeeded(); await element.click(); saved = true; successfulSelector = 'text-search-save'; console.log(`✅ Saved via text search`); break; } } } catch (error) { continue; } } } if (!saved) { // Keyboard shortcut fallback console.log('🔄 Trying keyboard shortcut (Ctrl+S)...'); await this.page.keyboard.press('Meta+S'); // Mac await this.page.waitForTimeout(1000); saved = true; successfulSelector = 'keyboard-shortcut'; } // Wait for save operation to complete await this.page.waitForTimeout(3000); // Get the updated URL const currentURL = this.page.url(); // Take success screenshot await this.page.screenshot({ path: `debug-composition-saved-${Date.now()}.png`, fullPage: true }); return { success: true, message: `Composition saved successfully using: ${successfulSelector}`, compositionURL: currentURL }; } catch (error) { console.error('❌ Failed to save composition:', error); await this.page.screenshot({ path: `error-save-composition-${Date.now()}.png`, fullPage: true }); return { success: false, message: `Failed to save composition: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Extract composition ID from URL with enhanced patterns */ private extractCompositionIdFromURL(url: string): string | null { try { // Multiple URL patterns to try const patterns = [ /\/composer\/([A-Za-z0-9+/=]+)/, /\/composition\/([A-Za-z0-9+/=]+)/, /\/edit\/([A-Za-z0-9+/=]+)/, /id=([A-Za-z0-9+/=]+)/, /compositionId=([A-Za-z0-9+/=]+)/ ]; for (const pattern of patterns) { const match = url.match(pattern); if (match) { return match[1]; } } return null; } catch (error) { console.error('Error extracting composition ID:', error); return null; } } /** * Enhanced close with cleanup */ async close(): Promise<void> { try { if (this.page) { await this.page.close(); this.page = null; } if (this.browser) { await this.browser.close(); this.browser = null; } console.log('✅ Browser automation closed cleanly'); } catch (error) { console.error('❌ Error closing browser:', error); } } } /** * Enhanced composition lifecycle tools with error recovery */ export class EnhancedCompositionLifecycleTools { private static client: ImprovedComposerClient | null = null; /** * Get or create enhanced client instance */ private static async getClient(): Promise<ImprovedComposerClient> { if (!this.client) { this.client = new ImprovedComposerClient(); await this.client.initialize(); } return this.client; } /** * Enhanced create new composition tool */ static async createNewComposition(params: { navigate?: boolean } = {}) { try { const client = await this.getClient(); if (params.navigate !== false) { await client.navigateToComposer(); } const result = await client.createNewComposition(); return { content: [{ type: "text", text: JSON.stringify({ success: result.success, message: result.message, compositionId: result.compositionId, nextStep: result.success ? "Use save-composition to persist the composition" : "Check error logs and screenshots", timestamp: new Date().toISOString() }, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, message: `Failed to create composition: ${error instanceof Error ? error.message : 'Unknown error'}`, troubleshooting: "Check debug screenshots and ensure JWT token is valid", timestamp: new Date().toISOString() }, null, 2) }] }; } } /** * Enhanced save composition tool */ static async saveComposition(params: { returnURL?: boolean } = {}) { try { const client = await this.getClient(); const result = await client.saveComposition(); const responseData: any = { success: result.success, message: result.message, timestamp: new Date().toISOString() }; if (params.returnURL !== false && result.compositionURL) { responseData.compositionURL = result.compositionURL; } return { content: [{ type: "text", text: JSON.stringify(responseData, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, message: `Failed to save composition: ${error instanceof Error ? error.message : 'Unknown error'}`, troubleshooting: "Check debug screenshots and browser console logs", timestamp: new Date().toISOString() }, null, 2) }] }; } } /** * Enhanced cleanup */ static async cleanup(): Promise<void> { if (this.client) { await this.client.close(); this.client = null; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server