composer-ui-automation.ts•9.78 kB
/**
* Composer UI Automation Module
* Extends RealBrowserManager with specific UI automation methods for EuConquisto Composer
* @version 2.0.0
* @created 2025-07-02
*/
import { Page } from 'playwright';
export interface ComposerSelectors {
// Authentication
jwtRedirectUrl: string;
// Navigation
newCompositionButton: string;
saveButton: string;
// Composer UI
composerEditor: string;
elementSidebar: string;
// localStorage
storageKey: string;
}
export class ComposerUIAutomation {
private selectors: ComposerSelectors = {
// JWT redirect server
jwtRedirectUrl: 'http://localhost:8080/composer',
// UI selectors based on testing results
newCompositionButton: 'button:has-text("Nova composição")',
saveButton: 'button[title*="Salvar composição"]',
// Composer interface
composerEditor: '[data-testid="composer-editor"], #composer-editor, .composer-content',
elementSidebar: '[data-testid="element-sidebar"], .element-sidebar, .sidebar',
// localStorage key
storageKey: 'rdp-composer-data'
};
/**
* Authenticate using JWT redirect server
*/
async authenticateViaJWT(page: Page): Promise<boolean> {
try {
console.log('🔐 Authenticating via JWT redirect server...');
// Navigate to JWT redirect server
await page.goto(this.selectors.jwtRedirectUrl, {
waitUntil: 'networkidle',
timeout: 30000
});
// Wait for redirect to composer
await page.waitForURL('**/composer.euconquisto.com/**', {
timeout: 15000
});
console.log('✅ Authentication successful');
return true;
} catch (error) {
console.error('❌ Authentication failed:', error);
return false;
}
}
/**
* Create a new blank composition
*/
async createBlankComposition(page: Page): Promise<boolean> {
try {
console.log('📝 Creating blank composition...');
// Wait for page to be fully loaded
await page.waitForLoadState('networkidle');
// Click "Nova composição" button
await page.waitForSelector(this.selectors.newCompositionButton, {
state: 'visible',
timeout: 10000
});
await page.click(this.selectors.newCompositionButton);
// Wait for navigation context destruction (known issue)
await page.waitForTimeout(3000);
// Wait for composer editor to load
await page.waitForSelector(this.selectors.composerEditor, {
state: 'visible',
timeout: 15000
});
console.log('✅ Blank composition created');
return true;
} catch (error) {
console.error('❌ Failed to create blank composition:', error);
return false;
}
}
/**
* Extract current composition data from localStorage
*/
async extractLocalStorageData(page: Page): Promise<any> {
try {
console.log('📊 Extracting localStorage data...');
const data = await page.evaluate((storageKey) => {
const stored = localStorage.getItem(storageKey);
return stored ? JSON.parse(stored) : null;
}, this.selectors.storageKey);
if (data) {
console.log('✅ localStorage data extracted');
console.log(` - Composition ID: ${data.composition?.id || 'N/A'}`);
console.log(` - Elements count: ${data.composition?.elements?.length || 0}`);
} else {
console.log('⚠️ No data found in localStorage');
}
return data;
} catch (error) {
console.error('❌ Failed to extract localStorage data:', error);
return null;
}
}
/**
* Inject enriched composition data into localStorage
*/
async injectCompositionData(page: Page, compositionData: any): Promise<boolean> {
try {
console.log('💉 Injecting composition data into localStorage...');
const injected = await page.evaluate((storageKey, data) => {
try {
localStorage.setItem(storageKey, JSON.stringify(data));
console.log('localStorage injection complete');
return true;
} catch (e) {
console.error('localStorage injection failed:', e);
return false;
}
}, this.selectors.storageKey, compositionData);
if (injected) {
console.log('✅ Composition data injected successfully');
// Try to trigger content loading
await this.triggerContentLoading(page);
}
return injected;
} catch (error) {
console.error('❌ Failed to inject composition data:', error);
return false;
}
}
/**
* Trigger content loading from localStorage
* This is the investigation area - different strategies to try
*/
async triggerContentLoading(page: Page): Promise<boolean> {
console.log('🔄 Attempting to trigger content loading...');
try {
// Strategy 1: Dispatch storage event
await page.evaluate(() => {
window.dispatchEvent(new StorageEvent('storage', {
key: 'rdp-composer-data',
newValue: localStorage.getItem('rdp-composer-data'),
url: window.location.href,
storageArea: localStorage
}));
});
// Wait a bit to see if content loads
await page.waitForTimeout(2000);
// Strategy 2: Page reload (fallback)
// Commented out for now - can be enabled if needed
// await page.reload({ waitUntil: 'networkidle' });
console.log('✅ Content loading triggered');
return true;
} catch (error) {
console.error('⚠️ Content loading trigger failed:', error);
return false;
}
}
/**
* Save the composition
*/
async saveComposition(page: Page): Promise<string | null> {
try {
console.log('💾 Saving composition...');
// Wait for save button to be enabled
await page.waitForSelector(this.selectors.saveButton, {
state: 'visible',
timeout: 10000
});
// Get current URL before save
const urlBefore = page.url();
// Click save button
await page.click(this.selectors.saveButton);
// Wait for URL to change (indicates save complete)
await page.waitForFunction(
(currentUrl) => window.location.href !== currentUrl,
urlBefore,
{ timeout: 10000 }
);
// Get new URL with encoded composition
const savedUrl = page.url();
console.log('✅ Composition saved successfully');
console.log(`📎 URL: ${savedUrl}`);
// Extract file_uid if present
const fileUidMatch = savedUrl.match(/file_uid=([a-f0-9-]+)/);
if (fileUidMatch) {
console.log(`🔑 File UID: ${fileUidMatch[1]}`);
}
return savedUrl;
} catch (error) {
console.error('❌ Failed to save composition:', error);
return null;
}
}
/**
* Merge blank composition structure with generated content
*/
mergeCompositionData(blankStructure: any, generatedContent: any): any {
console.log('🔀 Merging composition data...');
// Start with the blank structure as base
const merged = JSON.parse(JSON.stringify(blankStructure));
// Update with generated content
if (generatedContent.composition) {
merged.composition = {
...merged.composition,
...generatedContent.composition,
// Preserve any system-generated IDs from blank structure
id: merged.composition?.id || generatedContent.composition.id,
// Merge elements array
elements: generatedContent.composition.elements || []
};
}
console.log('✅ Composition data merged');
console.log(` - Title: ${merged.composition?.title}`);
console.log(` - Elements: ${merged.composition?.elements?.length}`);
return merged;
}
/**
* Complete workflow: authenticate, create, enrich, save
*/
async executeCompleteWorkflow(
page: Page,
generatedComposition: any
): Promise<{ success: boolean; url?: string; error?: string }> {
try {
console.log('🚀 Starting complete browser automation workflow...');
// Step 1: Authenticate
const authSuccess = await this.authenticateViaJWT(page);
if (!authSuccess) {
return { success: false, error: 'Authentication failed' };
}
// Step 2: Create blank composition
const createSuccess = await this.createBlankComposition(page);
if (!createSuccess) {
return { success: false, error: 'Failed to create blank composition' };
}
// Step 3: Extract blank structure
const blankStructure = await this.extractLocalStorageData(page);
if (!blankStructure) {
return { success: false, error: 'Failed to extract blank structure' };
}
// Step 4: Merge with generated content
const enrichedData = this.mergeCompositionData(blankStructure, generatedComposition);
// Step 5: Inject enriched data
const injectSuccess = await this.injectCompositionData(page, enrichedData);
if (!injectSuccess) {
return { success: false, error: 'Failed to inject composition data' };
}
// Step 6: Save composition
const savedUrl = await this.saveComposition(page);
if (!savedUrl) {
return { success: false, error: 'Failed to save composition' };
}
console.log('🎉 Workflow completed successfully!');
return { success: true, url: savedUrl };
} catch (error) {
console.error('❌ Workflow failed:', error);
return { success: false, error: error.message };
}
}
}