improved-composition-lifecycle.ts•19.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;
}
}
}