#!/usr/bin/env node /** * Comprehensive Add Button Finder * * This script uses multiple strategies to find the Add button: * 1. Waits for page to fully load * 2. Maps all page sections * 3. Finds all tables and their associated buttons * 4. Identifies buttons in table headers/toolbars * 5. Tests each potential button to see what it does */ import { chromium } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // Load environment variables const envPath = join(homedir(), '.env'); function loadEnvFile(filePath) { try { const envFile = readFileSync(filePath, 'utf8'); const envVars = envFile.split('\n').filter( (line) => line.includes('=') && !line.trim().startsWith('#') ); for (const line of envVars) { const [key, ...values] = line.split('='); if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) { let value = values.join('=').trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } process.env[key.trim()] = value; } } return true; } catch { return false; } } loadEnvFile(envPath); const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1'; const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api'; const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD; console.log('πŸ” Comprehensive Add Button Finder'); console.log('==================================\n'); if (!PASSWORD) { console.error('❌ UNIFI_PASSWORD must be set in ~/.env'); process.exit(1); } (async () => { const browser = await chromium.launch({ headless: false }); const context = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await context.newPage(); try { console.log('1. Logging in...'); await page.goto(UDM_URL, { waitUntil: 'networkidle' }); await page.waitForSelector('input[type="text"]'); await page.fill('input[type="text"]', USERNAME); await page.fill('input[type="password"]', PASSWORD); await page.click('button[type="submit"]'); await page.waitForTimeout(5000); console.log('2. Navigating to Routing page...'); // Wait for dashboard to fully load first await page.waitForTimeout(5000); // Navigate directly await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'domcontentloaded' }); // Wait for URL to change (handle redirects) await page.waitForURL('**/settings/routing**', { timeout: 20000 }).catch(() => { console.log(' ⚠️ URL redirect not detected, continuing...'); }); await page.waitForTimeout(5000); // Wait for page to be interactive await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); await page.waitForLoadState('domcontentloaded'); console.log(` Current URL: ${page.url()}`); // Check if we're actually on routing page const pageText = await page.textContent('body').catch(() => ''); if (!pageText.includes('Route') && !pageText.includes('routing')) { console.log(' ⚠️ Page may not be fully loaded, waiting more...'); await page.waitForTimeout(10000); } // Wait for routes API try { await page.waitForResponse(response => response.url().includes('/rest/routing') || response.url().includes('/trafficroutes'), { timeout: 15000 } ); console.log(' Routes API loaded'); } catch (error) { console.log(' Routes API not detected'); } await page.waitForTimeout(5000); console.log('3. Analyzing page structure...\n'); // Get comprehensive page analysis const analysis = await page.evaluate(() => { const result = { tables: [], buttons: [], sections: [], potentialAddButtons: [], }; // Find all tables const tables = Array.from(document.querySelectorAll('table')); tables.forEach((table, index) => { const rect = table.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || ''); const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length; const tableText = table.textContent || ''; // Find buttons in/around this table const tableButtons = []; let current = table; for (let i = 0; i < 3; i++) { const buttons = Array.from(current.querySelectorAll('button, [role="button"]')); buttons.forEach(btn => { const btnRect = btn.getBoundingClientRect(); if (btnRect.width > 0 && btnRect.height > 0) { const styles = window.getComputedStyle(btn); if (styles.display !== 'none' && styles.visibility !== 'hidden') { tableButtons.push({ text: btn.textContent?.trim() || '', className: btn.className || '', id: btn.id || '', ariaLabel: btn.getAttribute('aria-label') || '', position: { x: btnRect.x, y: btnRect.y }, isInHeader: table.querySelector('thead')?.contains(btn) || false, isInToolbar: btn.closest('[class*="toolbar" i], [class*="header" i], [class*="action" i]') !== null, }); } } }); current = current.parentElement; if (!current) break; } result.tables.push({ index, headers, rowCount: rows, hasRouteText: tableText.includes('Route') || tableText.includes('Static'), buttonCount: tableButtons.length, buttons: tableButtons, position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, }); } }); // Find all buttons with full context const allButtons = Array.from(document.querySelectorAll('button, [role="button"]')); allButtons.forEach((btn, index) => { const rect = btn.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const styles = window.getComputedStyle(btn); if (styles.display !== 'none' && styles.visibility !== 'hidden') { const text = btn.textContent?.trim() || ''; const className = btn.className || ''; const id = btn.id || ''; const ariaLabel = btn.getAttribute('aria-label') || ''; // Check if near a table let nearTable = null; let current = btn; for (let i = 0; i < 5; i++) { if (current.tagName === 'TABLE') { nearTable = { index: Array.from(document.querySelectorAll('table')).indexOf(current), isInHeader: current.querySelector('thead')?.contains(btn) || false, }; break; } current = current.parentElement; if (!current) break; } // Check parent context let parent = btn.parentElement; let parentContext = ''; for (let i = 0; i < 3; i++) { if (parent) { const parentText = parent.textContent?.trim() || ''; if (parentText.includes('Route') || parentText.includes('Static')) { parentContext = 'ROUTE_CONTEXT'; break; } parent = parent.parentElement; } } const buttonInfo = { index, text, className: className.substring(0, 100), id, ariaLabel, position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, iconOnly: !text && btn.querySelector('svg') !== null, nearTable, hasRouteContext: parentContext === 'ROUTE_CONTEXT', }; result.buttons.push(buttonInfo); // Identify potential Add buttons if (buttonInfo.iconOnly || text.toLowerCase().includes('add') || text.toLowerCase().includes('create') || className.toLowerCase().includes('add') || ariaLabel.toLowerCase().includes('add')) { result.potentialAddButtons.push(buttonInfo); } } } }); return result; }); console.log('πŸ“Š Page Analysis Results:'); console.log('='.repeat(80)); console.log(`\nπŸ“‹ Tables Found: ${analysis.tables.length}`); analysis.tables.forEach((table, i) => { console.log(`\n${i + 1}. Table ${table.index}:`); console.log(` Headers: ${table.headers.join(', ')}`); console.log(` Rows: ${table.rowCount}`); console.log(` Has Route Text: ${table.hasRouteText}`); console.log(` Buttons: ${table.buttonCount}`); if (table.buttons.length > 0) { console.log(` Button Details:`); table.buttons.forEach((btn, j) => { console.log(` ${j + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`); console.log(` In Header: ${btn.isInHeader}, In Toolbar: ${btn.isInToolbar}`); console.log(` Position: (${btn.position.x}, ${btn.position.y})`); }); } }); console.log(`\nπŸ”˜ All Buttons: ${analysis.buttons.length}`); analysis.buttons.forEach((btn, i) => { console.log(`\n${i + 1}. Button ${btn.index}:`); console.log(` Text: "${btn.text}"`); console.log(` Class: ${btn.className}`); console.log(` Icon Only: ${btn.iconOnly}`); console.log(` Near Table: ${btn.nearTable ? `Table ${btn.nearTable.index}` : 'No'}`); console.log(` Has Route Context: ${btn.hasRouteContext}`); console.log(` Position: (${btn.position.x}, ${btn.position.y})`); }); console.log(`\n🎯 Potential Add Buttons: ${analysis.potentialAddButtons.length}`); analysis.potentialAddButtons.forEach((btn, i) => { console.log(`\n${i + 1}. "${btn.text}" - ${btn.className}`); console.log(` Near Table: ${btn.nearTable ? `Table ${btn.nearTable.index}` : 'No'}`); console.log(` Has Route Context: ${btn.hasRouteContext}`); console.log(` Selector: ${btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`}`); }); // Test clicking potential buttons console.log(`\nπŸ§ͺ Testing Potential Add Buttons:`); console.log('='.repeat(80)); for (const btn of analysis.potentialAddButtons.slice(0, 5)) { try { console.log(`\nTesting: "${btn.text}" (${btn.className.substring(0, 50)})`); let selector = btn.id ? `#${btn.id}` : `button:has-text("${btn.text}")`; if (!btn.text && btn.className) { const firstClass = btn.className.split(' ')[0]; selector = `button.${firstClass}`; } const button = await page.locator(selector).first(); if (await button.isVisible({ timeout: 2000 })) { console.log(` βœ… Button is visible`); // Try clicking await button.click({ timeout: 5000 }).catch(async (error) => { console.log(` ⚠️ Regular click failed: ${error.message}`); // Try JavaScript click await page.evaluate((id) => { const el = document.getElementById(id); if (el) el.click(); }, btn.id).catch(() => {}); }); await page.waitForTimeout(3000); // Check if form appeared const hasForm = await page.locator('input[name="name"], input[name="destination"], input[placeholder*="destination" i]').first().isVisible({ timeout: 2000 }).catch(() => false); if (hasForm) { console.log(` βœ…βœ…βœ… FORM APPEARED! This is the Add button! βœ…βœ…βœ…`); console.log(` Selector: ${selector}`); console.log(` ID: ${btn.id || 'none'}`); console.log(` Class: ${btn.className}`); break; } else { // Check if menu appeared const hasMenu = await page.locator('[role="menu"], [role="listbox"]').first().isVisible({ timeout: 2000 }).catch(() => false); if (hasMenu) { console.log(` ⚠️ Menu appeared (not form) - this might be a settings button`); // Close menu await page.keyboard.press('Escape'); await page.waitForTimeout(1000); } else { console.log(` ❌ No form or menu appeared`); } } } else { console.log(` ❌ Button not visible`); } } catch (error) { console.log(` ❌ Error testing button: ${error.message}`); } } console.log('\n\n⏸️ Page is open in browser. Inspect manually if needed.'); console.log('Press Ctrl+C to close...\n'); await page.waitForTimeout(60000); } catch (error) { console.error('❌ Error:', error.message); await page.screenshot({ path: 'find-add-error.png', fullPage: true }); } finally { await browser.close(); } })();