/ src / utils / browser-notifications.js
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  }