browser-notifications.js
1 /** 2 * Browser Notification Utilities 3 * Creates floating, draggable messages for headed browser automation 4 */ 5 6 // Safe: object keys are from hardcoded position/style strings; innerHTML runs in Playwright-controlled pages 7 /* eslint-disable security/detect-object-injection */ 8 9 /** 10 * Shows a draggable floating notification in the browser 11 * @param {import('playwright').Page} page - Playwright page instance 12 * @param {string} message - Message to display 13 * @param {Object} options - Display options 14 * @param {string} [options.position='top-right'] - Position: 'top-right', 'top-left', 'bottom-right', 'bottom-left' 15 * @param {string} [options.backgroundColor='#4A90E2'] - Background color 16 * @param {string} [options.textColor='white'] - Text color 17 * @returns {Promise<void>} 18 */ 19 export async function showFloatingMessage(page, message, options = {}) { 20 const { position = 'top-right', backgroundColor = '#4A90E2', textColor = 'white' } = options; 21 22 // Calculate position based on option 23 const positions = { 24 'top-right': 'top: 20px; right: 20px;', 25 'top-left': 'top: 20px; left: 20px;', 26 'bottom-right': 'bottom: 20px; right: 20px;', 27 'bottom-left': 'bottom: 20px; left: 20px;', 28 }; 29 30 const positionStyle = positions[position] || positions['top-right']; 31 32 /* eslint-disable no-undef */ 33 await page.evaluate( 34 ({ msg, bgColor, txtColor, pos }) => { 35 // Remove any existing notification 36 const existing = document.getElementById('claude-notification'); 37 if (existing) { 38 existing.remove(); 39 } 40 41 // Create notification element 42 const notification = document.createElement('div'); 43 notification.id = 'claude-notification'; 44 // nosemgrep: javascript.browser.security.insecure-document-method.insecure-document-method 45 notification.innerHTML = ` 46 <div style=" 47 display: flex; 48 align-items: center; 49 gap: 12px; 50 "> 51 <div style=" 52 width: 8px; 53 height: 8px; 54 background: ${txtColor}; 55 border-radius: 50%; 56 animation: pulse 2s infinite; 57 "></div> 58 <span style="flex: 1;">${msg}</span> 59 </div> 60 `; 61 62 // Apply styles 63 Object.assign(notification.style, { 64 position: 'fixed', 65 background: bgColor, 66 color: txtColor, 67 padding: '16px 20px', 68 borderRadius: '8px', 69 boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', 70 zIndex: '999999', 71 fontFamily: 'system-ui, -apple-system, sans-serif', 72 fontSize: '14px', 73 fontWeight: '500', 74 cursor: 'move', 75 userSelect: 'none', 76 maxWidth: '400px', 77 minWidth: '250px', 78 }); 79 80 // Set position from style string 81 pos.split(';').forEach(style => { 82 const [key, value] = style.split(':').map(s => s.trim()); 83 if (key && value) { 84 notification.style[key] = value; 85 } 86 }); 87 88 // Add pulse animation 89 const style = document.createElement('style'); 90 style.textContent = ` 91 @keyframes pulse { 92 0%, 100% { opacity: 1; } 93 50% { opacity: 0.3; } 94 } 95 `; 96 document.head.appendChild(style); 97 98 // Make draggable 99 let isDragging = false; 100 let currentX; 101 let currentY; 102 let initialX; 103 let initialY; 104 let xOffset = 0; 105 let yOffset = 0; 106 107 notification.addEventListener('mousedown', e => { 108 initialX = e.clientX - xOffset; 109 initialY = e.clientY - yOffset; 110 isDragging = true; 111 }); 112 113 document.addEventListener('mousemove', e => { 114 if (isDragging) { 115 e.preventDefault(); 116 currentX = e.clientX - initialX; 117 currentY = e.clientY - initialY; 118 xOffset = currentX; 119 yOffset = currentY; 120 121 notification.style.transform = `translate(${currentX}px, ${currentY}px)`; 122 } 123 }); 124 125 document.addEventListener('mouseup', () => { 126 isDragging = false; 127 }); 128 129 // Add to page 130 document.body.appendChild(notification); 131 }, 132 { 133 msg: message, 134 bgColor: backgroundColor, 135 txtColor: textColor, 136 pos: positionStyle, 137 } 138 ); 139 /* eslint-enable no-undef */ 140 } 141 142 /** 143 * Hides the floating notification 144 * @param {import('playwright').Page} page - Playwright page instance 145 * @returns {Promise<void>} 146 */ 147 export async function hideFloatingMessage(page) { 148 /* eslint-disable no-undef */ 149 await page.evaluate(() => { 150 const notification = document.getElementById('claude-notification'); 151 if (notification) { 152 notification.remove(); 153 } 154 }); 155 /* eslint-enable no-undef */ 156 } 157 158 /** 159 * Shows a waiting message and waits for user action 160 * @param {import('playwright').Page} page - Playwright page instance 161 * @param {string} waitingFor - What we're waiting for (e.g., "user login", "user review") 162 * @param {Function} checkCondition - Async function that returns true when condition is met 163 * @param {number} [pollInterval=1000] - How often to check condition (ms) 164 * @returns {Promise<void>} 165 */ 166 export async function waitForUser(page, waitingFor, checkCondition, pollInterval = 1000) { 167 await showFloatingMessage(page, `⏳ Waiting for ${waitingFor}...`, { 168 backgroundColor: '#FF9500', 169 }); 170 171 // Poll until condition is met 172 while (!(await checkCondition())) { 173 await new Promise(resolve => setTimeout(resolve, pollInterval)); 174 } 175 176 await hideFloatingMessage(page); 177 }