#!/usr/bin/env node /** * Visual Page Analyzer * * This script opens the routing page in a visible browser and provides * interactive analysis tools to identify the Add button location. */ import { chromium } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import readline from 'readline'; // 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('šŸ” Visual Page Analyzer'); console.log('======================\n'); if (!PASSWORD) { console.error('āŒ UNIFI_PASSWORD must be set in ~/.env'); process.exit(1); } const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); function question(prompt) { return new Promise((resolve) => { rl.question(prompt, resolve); }); } (async () => { const browser = await chromium.launch({ headless: false, slowMo: 100 }); 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...'); await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' }); await page.waitForTimeout(10000); // Wait for URL to be correct await page.waitForURL('**/settings/routing**', { timeout: 20000 }).catch(() => {}); await page.waitForTimeout(5000); console.log(` Current URL: ${page.url()}`); // 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('\n3. Page Analysis Tools Available:'); console.log('='.repeat(80)); // Highlight all buttons await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button, [role="button"]')); buttons.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') { // Add highlight btn.style.outline = '3px solid red'; btn.style.outlineOffset = '2px'; btn.setAttribute('data-analyzer-index', index); } } }); }); // Get button information const buttonInfo = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button, [role="button"]')); return buttons.map((btn, index) => { const rect = btn.getBoundingClientRect(); const styles = window.getComputedStyle(btn); if (rect.width > 0 && rect.height > 0 && styles.display !== 'none') { return { index, text: btn.textContent?.trim() || '', className: btn.className || '', id: btn.id || '', ariaLabel: btn.getAttribute('aria-label') || '', dataTestId: btn.getAttribute('data-testid') || '', position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, iconOnly: !btn.textContent?.trim() && btn.querySelector('svg') !== null, enabled: !btn.disabled, selector: btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`, }; } return null; }).filter(b => b !== null); }); console.log(`\nšŸ“Š Found ${buttonInfo.length} buttons on page:`); buttonInfo.forEach((btn, i) => { console.log(`\n${i + 1}. Button ${btn.index}:`); console.log(` Text: "${btn.text}"`); console.log(` Class: ${btn.className.substring(0, 80)}`); console.log(` ID: ${btn.id || 'none'}`); console.log(` Aria Label: ${btn.ariaLabel || 'none'}`); console.log(` Position: (${btn.position.x}, ${btn.position.y}) ${btn.position.width}x${btn.position.height}`); console.log(` Icon Only: ${btn.iconOnly}`); console.log(` Enabled: ${btn.enabled}`); console.log(` Selector: ${btn.selector}`); }); // Highlight tables await page.evaluate(() => { const tables = Array.from(document.querySelectorAll('table')); tables.forEach((table, index) => { table.style.outline = '3px solid blue'; table.style.outlineOffset = '2px'; table.setAttribute('data-analyzer-table-index', index); }); }); const tableInfo = await page.evaluate(() => { const tables = Array.from(document.querySelectorAll('table')); return tables.map((table, index) => { const rect = table.getBoundingClientRect(); const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || ''); const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length; return { index, headers, rowCount: rows, position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, buttonsInTable: Array.from(table.querySelectorAll('button')).length, }; }); }); if (tableInfo.length > 0) { console.log(`\nšŸ“‹ Found ${tableInfo.length} tables:`); tableInfo.forEach((table, i) => { console.log(`\n${i + 1}. Table ${table.index}:`); console.log(` Headers: ${table.headers.join(', ')}`); console.log(` Rows: ${table.rowCount}`); console.log(` Buttons in Table: ${table.buttonsInTable}`); console.log(` Position: (${table.position.x}, ${table.position.y})`); }); } else { console.log('\nšŸ“‹ No tables found on page'); } // Find route-related text const routeTexts = await page.evaluate(() => { const texts = []; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let node; while (node = walker.nextNode()) { const text = node.textContent?.trim() || ''; if (text && (text.includes('Static Routes') || text.includes('Route') || text.includes('Add'))) { const parent = node.parentElement; if (parent) { const rect = parent.getBoundingClientRect(); texts.push({ text: text.substring(0, 100), tag: parent.tagName, className: parent.className?.substring(0, 80) || '', position: { x: Math.round(rect.x), y: Math.round(rect.y) }, }); } } } return texts.slice(0, 20); }); if (routeTexts.length > 0) { console.log(`\nšŸ“ Route-Related Text Found (${routeTexts.length}):`); routeTexts.forEach((text, i) => { console.log(`\n${i + 1}. "${text.text}"`); console.log(` Tag: ${text.tag}, Class: ${text.className}`); console.log(` Position: (${text.position.x}, ${text.position.y})`); }); } console.log('\n\nšŸŽÆ Interactive Testing:'); console.log('='.repeat(80)); console.log('Buttons are highlighted in RED'); console.log('Tables are highlighted in BLUE'); console.log('\nYou can now:'); console.log('1. Visually inspect the page in the browser'); console.log('2. Test clicking buttons to see what they do'); console.log('3. Identify the Add Route button location'); console.log('\nPress Enter to test clicking buttons, or type "exit" to close...\n'); let testing = true; while (testing) { const input = await question('Enter button number to test (1-' + buttonInfo.length + '), "screenshot" to save, or "exit": '); if (input.toLowerCase() === 'exit') { testing = false; break; } if (input.toLowerCase() === 'screenshot') { await page.screenshot({ path: 'analyzer-screenshot.png', fullPage: true }); console.log('āœ… Screenshot saved: analyzer-screenshot.png'); continue; } const buttonNum = parseInt(input); if (buttonNum >= 1 && buttonNum <= buttonInfo.length) { const btn = buttonInfo[buttonNum - 1]; console.log(`\nTesting button ${buttonNum}: "${btn.text}"`); console.log(`Selector: ${btn.selector}`); try { // Highlight the button await page.evaluate((index) => { const btn = document.querySelector(`[data-analyzer-index="${index}"]`); if (btn) { btn.style.backgroundColor = 'yellow'; btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, btn.index); await page.waitForTimeout(1000); // Try clicking const selector = btn.id ? `#${btn.id}` : `button:nth-of-type(${btn.index + 1})`; await page.click(selector, { timeout: 5000 }).catch(async (error) => { console.log(` āš ļø Regular click failed: ${error.message}`); // Try JavaScript click await page.evaluate((index) => { const btn = document.querySelector(`[data-analyzer-index="${index}"]`); if (btn) btn.click(); }, btn.index); }); await page.waitForTimeout(3000); // Check for form 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(` Use selector: ${btn.selector}`); console.log(` Or ID: ${btn.id || 'none'}`); console.log(` Or class: ${btn.className.split(' ')[0]}`); testing = false; break; } else { // Check for menu const hasMenu = await page.locator('[role="menu"], [role="listbox"]').first().isVisible({ timeout: 2000 }).catch(() => false); if (hasMenu) { console.log(' āš ļø Menu appeared (not form)'); const menuItems = await page.evaluate(() => { const menu = document.querySelector('[role="menu"], [role="listbox"]'); if (!menu) return []; return Array.from(menu.querySelectorAll('[role="menuitem"], [role="option"], li, div')).map(item => ({ text: item.textContent?.trim() || '', tag: item.tagName, })).filter(item => item.text.length > 0); }); console.log(` Menu items: ${menuItems.map(m => `"${m.text}"`).join(', ')}`); } else { console.log(' āŒ No form or menu appeared'); } } } catch (error) { console.log(` āŒ Error: ${error.message}`); } } else { console.log('Invalid button number'); } } console.log('\nāœ… Analysis complete. Closing browser...'); } catch (error) { console.error('āŒ Error:', error.message); await page.screenshot({ path: 'analyzer-error.png', fullPage: true }); } finally { rl.close(); await browser.close(); } })();