From 784956a31c38c3c8c32490a05972e512148a78cc Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Wed, 14 Jan 2026 17:26:27 -0600 Subject: [PATCH] feat: add Puppeteer integration for AI agent development (#6736) Add Puppeteer utilities and scripts to enable AI agents to interactively test and debug the documentation site during development. Dependencies: puppeteer, pixelmatch, pngjs Scripts: debug:browser, debug:screenshot, debug:inspect Tools: 20+ helper functions, example scripts, comprehensive documentation Enables AI agents to visually debug pages, test components, monitor performance, and detect issues during development. Co-authored-by: Claude --- package.json | 8 +- scripts/puppeteer/.gitignore | 17 + scripts/puppeteer/QUICK-REFERENCE.md | 244 ++++++++ scripts/puppeteer/README.md | 562 ++++++++++++++++++ scripts/puppeteer/SETUP.md | 261 ++++++++ scripts/puppeteer/debug-browser.js | 105 ++++ scripts/puppeteer/examples/detect-issues.js | 318 ++++++++++ .../examples/test-format-selector.js | 180 ++++++ scripts/puppeteer/inspect-page.js | 328 ++++++++++ scripts/puppeteer/screenshot.js | 104 ++++ scripts/puppeteer/utils/puppeteer-helpers.js | 486 +++++++++++++++ 11 files changed, 2612 insertions(+), 1 deletion(-) create mode 100644 scripts/puppeteer/.gitignore create mode 100644 scripts/puppeteer/QUICK-REFERENCE.md create mode 100644 scripts/puppeteer/README.md create mode 100644 scripts/puppeteer/SETUP.md create mode 100644 scripts/puppeteer/debug-browser.js create mode 100644 scripts/puppeteer/examples/detect-issues.js create mode 100644 scripts/puppeteer/examples/test-format-selector.js create mode 100644 scripts/puppeteer/inspect-page.js create mode 100644 scripts/puppeteer/screenshot.js create mode 100644 scripts/puppeteer/utils/puppeteer-helpers.js diff --git a/package.json b/package.json index db94df926..c2bd29453 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,13 @@ "eslint-plugin-jsx-a11y": "^6.10.2", "globals": "^15.14.0", "hugo-extended": ">=0.101.0", + "pixelmatch": "^6.0.0", + "pngjs": "^7.0.0", "postcss": ">=8.4.31", "postcss-cli": ">=9.1.0", "prettier": "^3.2.5", "prettier-plugin-sql": "^0.18.0", + "puppeteer": "^23.11.1", "remark": "^15.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", @@ -87,7 +90,10 @@ "test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/index.cy.js\" content/example.md", "sync-plugins": "cd helper-scripts/influxdb3-plugins && node port_to_docs.js", "sync-plugins:dry-run": "cd helper-scripts/influxdb3-plugins && node port_to_docs.js --dry-run", - "validate-plugin-config": "cd helper-scripts/influxdb3-plugins && node port_to_docs.js --validate" + "validate-plugin-config": "cd helper-scripts/influxdb3-plugins && node port_to_docs.js --validate", + "debug:browser": "node scripts/puppeteer/debug-browser.js", + "debug:screenshot": "node scripts/puppeteer/screenshot.js", + "debug:inspect": "node scripts/puppeteer/inspect-page.js" }, "type": "module", "browserslist": [ diff --git a/scripts/puppeteer/.gitignore b/scripts/puppeteer/.gitignore new file mode 100644 index 000000000..643508c0d --- /dev/null +++ b/scripts/puppeteer/.gitignore @@ -0,0 +1,17 @@ +# Debug output +debug-output/ +*.png +*.jpg +*.jpeg +*.pdf + +# Test outputs +test-screenshot.png +screenshot-*.png +format-selector-*.png +issues-detected-*.png +inspect-*.png +reports/ + +# Node modules (if someone runs npm install here) +node_modules/ diff --git a/scripts/puppeteer/QUICK-REFERENCE.md b/scripts/puppeteer/QUICK-REFERENCE.md new file mode 100644 index 000000000..198efafe6 --- /dev/null +++ b/scripts/puppeteer/QUICK-REFERENCE.md @@ -0,0 +1,244 @@ +# Puppeteer Quick Reference + +One-page reference for AI agents using Puppeteer with docs-v2. + +## Prerequisites + +```bash +# 1. Hugo server must be running +npx hugo server + +# 2. Packages installed (one-time) +PUPPETEER_SKIP_DOWNLOAD=true yarn install +``` + +## Common Commands + +### Take Screenshot +```bash +yarn debug:screenshot # Basic screenshot +yarn debug:screenshot --full-page # Full scrollable page +yarn debug:screenshot --selector .class # Specific element +yarn debug:screenshot --viewport 375x667 # Mobile size +``` + +### Inspect Page +```bash +yarn debug:inspect # Full analysis +yarn debug:inspect --output report.json # Save report +yarn debug:inspect --screenshot # Include screenshot +``` + +### Open Browser +```bash +yarn debug:browser # Interactive mode +yarn debug:browser --devtools # With DevTools +yarn debug:browser --slow-mo 500 # Slow motion +``` + +## Quick Workflows + +### User Reports Visual Issue +```bash +yarn debug:screenshot /path/to/page/ --full-page +yarn debug:inspect /path/to/page/ +# Review screenshot and inspection report +``` + +### Testing Component Change +```bash +# Before +yarn debug:screenshot /example/ --output before.png + +# Make changes, restart Hugo + +# After +yarn debug:screenshot /example/ --output after.png +``` + +### Debugging JavaScript Error +```bash +yarn debug:inspect /path/ # Check errors section +yarn debug:browser /path/ --devtools # Debug in browser +``` + +### Performance Check +```bash +yarn debug:inspect /path/ --output perf.json +# Check perf.json โ†’ performance.performance.loadComplete +# Should be < 3000ms +``` + +### Automated Issue Detection +```bash +node scripts/puppeteer/examples/detect-issues.js /path/ +``` + +## Programmatic Usage + +```javascript +import { + launchBrowser, + navigateToPage, + takeScreenshot, + elementExists, + getPageMetrics, +} from './utils/puppeteer-helpers.js'; + +const browser = await launchBrowser(); +const page = await navigateToPage(browser, '/influxdb3/core/'); + +// Check element +const hasComponent = await elementExists(page, '[data-component="format-selector"]'); + +// Screenshot +await takeScreenshot(page, 'debug.png'); + +// Performance +const metrics = await getPageMetrics(page); +console.log('Load time:', metrics.performance.loadComplete); + +await browser.close(); +``` + +## Common Flags + +```bash +--chrome PATH # Use system Chrome +--base-url URL # Different base URL +--viewport WxH # Viewport size +--output PATH # Output file path +--full-page # Full page screenshot +--selector CSS # Element selector +--screenshot # Include screenshot +--devtools # Open DevTools +--slow-mo NUM # Slow down actions +``` + +## Troubleshooting + +### Browser not found +```bash +# Find Chrome +which google-chrome + +# Use with flag +yarn debug:browser /path/ --chrome "$(which google-chrome)" +``` + +### Hugo not running +```bash +# Check if running +curl -s http://localhost:1313/ + +# Start if needed +npx hugo server +``` + +### Network restrictions +```bash +# Install without downloading Chrome +PUPPETEER_SKIP_DOWNLOAD=true yarn install +``` + +## Helper Functions + +### Browser & Navigation +- `launchBrowser(options)` - Launch browser +- `navigateToPage(browser, path, options)` - Navigate +- `clickAndNavigate(page, selector)` - Click & wait + +### Screenshots +- `takeScreenshot(page, path, options)` - Capture screenshot +- `compareScreenshots(baseline, current, diff)` - Compare images +- `testResponsive(page, viewports, testFn)` - Multi-viewport + +### Elements +- `elementExists(page, selector)` - Check exists +- `waitForElement(page, selector, timeout)` - Wait +- `getElementText(page, selector)` - Get text + +### Analysis +- `getPageMetrics(page)` - Performance data +- `getPageLinks(page)` - All links +- `getComputedStyles(page, selector)` - CSS values + +### Debugging +- `debugPage(page, name)` - Save HTML + screenshot + logs +- `captureConsoleLogs(page)` - Capture console + +## What to Check + +### Shortcode Remnants +Look for: `{{<`, `{{%`, `{{.` +```bash +yarn debug:inspect /path/ +# Check: report.shortcodeRemnants +``` + +### JavaScript Errors +```bash +yarn debug:inspect /path/ +# Check: report.errors +``` + +### Performance +```bash +yarn debug:inspect /path/ +# Check: report.performance.performance.loadComplete < 3000ms +# Check: report.performance.performance.firstContentfulPaint < 1500ms +``` + +### Accessibility +```bash +yarn debug:inspect /path/ +# Check: report.accessibility +# - hasMainLandmark +# - hasH1 +# - imagesWithoutAlt +# - linksWithoutText +``` + +### Components +```bash +yarn debug:inspect /path/ +# Check: report.components +# Lists all [data-component] elements +``` + +## Examples + +All in `scripts/puppeteer/examples/`: + +- `test-format-selector.js` - Test interactive component +- `detect-issues.js` - Automated issue detection + +Run with: +```bash +node scripts/puppeteer/examples/detect-issues.js /path/ +``` + +## Documentation + +- **Full Guide**: `scripts/puppeteer/README.md` +- **Setup**: `scripts/puppeteer/SETUP.md` +- **This Reference**: `scripts/puppeteer/QUICK-REFERENCE.md` + +## Emergency Debug + +When something's broken and you need full context: + +```bash +yarn debug:inspect /path/ --output emergency.json --screenshot +yarn debug:screenshot /path/ --full-page --output emergency-full.png +yarn debug:browser /path/ --devtools +``` + +This gives you: +1. JSON report with all page data +2. Full page screenshot +3. Interactive browser with DevTools + +--- + +**Remember**: Always start Hugo server first! (`npx hugo server`) diff --git a/scripts/puppeteer/README.md b/scripts/puppeteer/README.md new file mode 100644 index 000000000..c57253146 --- /dev/null +++ b/scripts/puppeteer/README.md @@ -0,0 +1,562 @@ +# Puppeteer Integration for AI Agent Development + +This directory contains Puppeteer utilities designed to help AI agents (like Claude) test and debug the InfluxData documentation site during development. + +## Purpose + +Puppeteer enables AI agents to: + +- **See what's happening** - Take screenshots to visually inspect pages +- **Debug interactively** - Launch a browser to manually test features +- **Gather context** - Inspect page metadata, performance, errors, and structure +- **Test components** - Verify JavaScript components are working correctly +- **Validate content** - Check for shortcode remnants, broken links, and accessibility issues + +## Installation + +### Step 1: Install dependencies + +Due to network restrictions, install Puppeteer without downloading the browser binary: + +```bash +PUPPETEER_SKIP_DOWNLOAD=true yarn install +``` + +### Step 2: Configure Chrome path (if needed) + +If you're using system Chrome instead of Puppeteer's bundled browser, set the path in your scripts: + +**Common Chrome paths:** +- macOS: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` +- Linux: `/usr/bin/google-chrome` +- Windows: `C:\Program Files\Google\Chrome\Application\chrome.exe` + +You can pass the Chrome path using the `--chrome` flag: + +```bash +yarn debug:browser /influxdb3/core/ --chrome "/usr/bin/google-chrome" +``` + +### Step 3: Start Hugo server + +Before using Puppeteer tools, make sure the Hugo development server is running: + +```bash +npx hugo server +``` + +The server should be accessible at `http://localhost:1313`. + +## Quick Start for AI Agents + +### Common Debugging Workflow + +When a user reports an issue or you need to debug something: + +1. **Start Hugo server** (if not already running) + ```bash + npx hugo server + ``` + +2. **Inspect the page** to gather information + ```bash + yarn debug:inspect /influxdb3/core/get-started/ + ``` + +3. **Take a screenshot** to see visual issues + ```bash + yarn debug:screenshot /influxdb3/core/get-started/ --full-page + ``` + +4. **Open browser interactively** if you need to test manually + ```bash + yarn debug:browser /influxdb3/core/get-started/ --devtools + ``` + +## Available Tools + +### 1. Page Inspector (`yarn debug:inspect`) + +Gather comprehensive information about a page: + +```bash +yarn debug:inspect [options] +``` + +**What it reports:** +- Page metadata (title, description, language) +- Performance metrics (load time, FCP, etc.) +- Console errors and warnings +- Links analysis (internal/external counts) +- Detected components (`data-component` attributes) +- Shortcode remnants (Hugo shortcodes that didn't render) +- Basic accessibility checks +- Content structure (headings, code blocks) + +**Examples:** + +```bash +# Inspect a page +yarn debug:inspect /influxdb3/core/ + +# Save report to JSON +yarn debug:inspect /influxdb3/core/ --output report.json + +# Also capture a screenshot +yarn debug:inspect /influxdb3/core/ --screenshot +``` + +**Use cases:** +- User reports a page isn't loading correctly +- Need to check if a page has JavaScript errors +- Want to verify shortcodes are rendering properly +- Need performance metrics for optimization + +### 2. Screenshot Tool (`yarn debug:screenshot`) + +Capture screenshots of pages or specific elements: + +```bash +yarn debug:screenshot [options] +``` + +**Options:** +- `--output PATH` - Save to specific file +- `--full-page` - Capture entire scrollable page +- `--selector CSS` - Capture specific element +- `--viewport WxH` - Set viewport size (e.g., `375x667` for mobile) + +**Examples:** + +```bash +# Basic screenshot +yarn debug:screenshot /influxdb3/core/ + +# Full page screenshot +yarn debug:screenshot /influxdb3/core/ --full-page + +# Screenshot of specific element +yarn debug:screenshot /influxdb3/core/ --selector .article--content + +# Mobile viewport screenshot +yarn debug:screenshot /influxdb3/core/ --viewport 375x667 + +# Custom output path +yarn debug:screenshot /influxdb3/core/ --output debug/home-page.png +``` + +**Use cases:** +- User reports visual issue ("the button is cut off") +- Need to see how page looks at different viewport sizes +- Want to capture a specific component for documentation +- Need before/after images for PR review + +### 3. Interactive Browser (`yarn debug:browser`) + +Launch a browser window for manual testing: + +```bash +yarn debug:browser [options] +``` + +**Options:** +- `--devtools` - Open Chrome DevTools automatically +- `--slow-mo NUM` - Slow down actions by NUM milliseconds +- `--viewport WxH` - Set viewport size +- `--base-url URL` - Use different base URL + +**Examples:** + +```bash +# Open page in browser +yarn debug:browser /influxdb3/core/ + +# Open with DevTools +yarn debug:browser /influxdb3/core/ --devtools + +# Slow down for debugging +yarn debug:browser /influxdb3/core/ --slow-mo 500 + +# Mobile viewport +yarn debug:browser /influxdb3/core/ --viewport 375x667 +``` + +**Use cases:** +- Need to manually click through a workflow +- Want to use Chrome DevTools to debug JavaScript +- Testing responsive design breakpoints +- Verifying interactive component behavior + +## Programmatic Usage + +AI agents can also use the helper functions directly in custom scripts: + +```javascript +import { + launchBrowser, + navigateToPage, + takeScreenshot, + getPageMetrics, + elementExists, + getElementText, + clickAndNavigate, + testComponent, +} from './utils/puppeteer-helpers.js'; + +// Launch browser +const browser = await launchBrowser({ headless: true }); + +// Navigate to page +const page = await navigateToPage(browser, '/influxdb3/core/'); + +// Check if element exists +const hasFormatSelector = await elementExists(page, '[data-component="format-selector"]'); +console.log('Format selector present:', hasFormatSelector); + +// Get text content +const title = await getElementText(page, 'h1'); +console.log('Page title:', title); + +// Take screenshot +await takeScreenshot(page, 'debug.png'); + +// Get performance metrics +const metrics = await getPageMetrics(page); +console.log('Load time:', metrics.performance.loadComplete); + +// Close browser +await browser.close(); +``` + +## Common Scenarios + +### Scenario 1: User reports "shortcodes are showing as raw text" + +```bash +# Inspect the page for shortcode remnants +yarn debug:inspect /path/to/page/ + +# Take a screenshot to see the issue +yarn debug:screenshot /path/to/page/ --full-page --output shortcode-issue.png +``` + +**What to look for in the report:** +- `shortcodeRemnants` section will show any `{{<` or `{{%` patterns +- Screenshot will show visual rendering + +### Scenario 2: User reports "page is loading slowly" + +```bash +# Inspect page for performance metrics +yarn debug:inspect /path/to/page/ --output performance-report.json + +# Check the report for: +# - performance.performance.loadComplete (should be < 3000ms) +# - performance.performance.firstContentfulPaint (should be < 1500ms) +``` + +### Scenario 3: User reports "JavaScript error in console" + +```bash +# Inspect page for console errors +yarn debug:inspect /path/to/page/ + +# Open browser with DevTools to see detailed error +yarn debug:browser /path/to/page/ --devtools +``` + +**What to look for:** +- `errors` section in inspection report +- Red error messages in DevTools console +- Stack traces showing which file/line caused the error + +### Scenario 4: User reports "component not working on mobile" + +```bash +# Take screenshot at mobile viewport +yarn debug:screenshot /path/to/page/ --viewport 375x667 --output mobile-view.png + +# Open browser at mobile viewport for testing +yarn debug:browser /path/to/page/ --viewport 375x667 --devtools +``` + +### Scenario 5: Testing a Hugo shortcode implementation + +```bash +# 1. Inspect test page for components +yarn debug:inspect /example/ --screenshot + +# 2. Take screenshots of different states +yarn debug:screenshot /example/ --selector '[data-component="tabs-wrapper"]' + +# 3. Open browser to test interactivity +yarn debug:browser /example/ --devtools +``` + +### Scenario 6: Validating responsive design + +```javascript +// Create a custom script: test-responsive.js +import { launchBrowser, navigateToPage, takeScreenshot, testResponsive } from './utils/puppeteer-helpers.js'; + +const browser = await launchBrowser(); +const page = await navigateToPage(browser, '/influxdb3/core/'); + +const viewports = [ + { width: 375, height: 667, name: 'iPhone SE' }, + { width: 768, height: 1024, name: 'iPad' }, + { width: 1280, height: 720, name: 'Desktop' }, + { width: 1920, height: 1080, name: 'Desktop HD' }, +]; + +const results = await testResponsive(page, viewports, async (page, viewport) => { + await takeScreenshot(page, `responsive-${viewport.name}.png`); + const hasNav = await elementExists(page, '[data-component="mobile-nav"]'); + return { hasNav }; +}); + +console.log('Responsive test results:', results); +await browser.close(); +``` + +```bash +node scripts/puppeteer/test-responsive.js +``` + +## Helper Functions Reference + +See `utils/puppeteer-helpers.js` for complete documentation. Key functions: + +### Browser & Navigation +- `launchBrowser(options)` - Launch browser instance +- `navigateToPage(browser, urlPath, options)` - Navigate to page +- `clickAndNavigate(page, selector)` - Click and wait for navigation + +### Elements +- `elementExists(page, selector)` - Check if element exists +- `waitForElement(page, selector, timeout)` - Wait for element +- `getElementText(page, selector)` - Get element text content +- `getComputedStyles(page, selector, properties)` - Get CSS styles + +### Screenshots & Visual +- `takeScreenshot(page, path, options)` - Capture screenshot +- `compareScreenshots(baseline, current, diff)` - Compare images +- `testResponsive(page, viewports, testFn)` - Test at different sizes + +### Analysis +- `getPageMetrics(page)` - Get performance metrics +- `getPageLinks(page)` - Get all links on page +- `captureConsoleLogs(page)` - Capture console output +- `debugPage(page, name)` - Save HTML + screenshot for debugging + +### Testing +- `testComponent(page, selector, testFn)` - Test component behavior + +## Troubleshooting + +### Error: "Failed to launch browser" + +**Problem:** Puppeteer can't find Chrome executable + +**Solutions:** + +1. **Use system Chrome:** + ```bash + yarn debug:browser /path/ --chrome "/usr/bin/google-chrome" + ``` + +2. **Install Puppeteer browser:** + ```bash + npx puppeteer browsers install chrome + ``` + +3. **Check common Chrome paths:** + - macOS: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` + - Linux: `/usr/bin/google-chrome` or `/usr/bin/chromium` + - Windows: `C:\Program Files\Google\Chrome\Application\chrome.exe` + +### Error: "Failed to navigate to http://localhost:1313" + +**Problem:** Hugo server is not running + +**Solution:** +```bash +# In a separate terminal +npx hugo server +``` + +### Error: "Element not found: .selector" + +**Problem:** Element doesn't exist on page or page hasn't finished loading + +**Solutions:** + +1. **Wait for element:** + ```javascript + await waitForElement(page, '.selector', 10000); // 10 second timeout + ``` + +2. **Check if element exists first:** + ```javascript + if (await elementExists(page, '.selector')) { + // Element exists, safe to interact + } + ``` + +3. **Take screenshot to debug:** + ```bash + yarn debug:screenshot /path/ --output debug.png + ``` + +### Network restrictions blocking browser download + +**Problem:** Cannot download Puppeteer's bundled Chrome due to network restrictions + +**Solution:** +```bash +# Install without browser binary +PUPPETEER_SKIP_DOWNLOAD=true yarn install + +# Use system Chrome with --chrome flag +yarn debug:browser /path/ --chrome "/usr/bin/google-chrome" +``` + +## Best Practices for AI Agents + +### 1. Always check if Hugo server is running first + +```bash +# Check if server is responding +curl -s -o /dev/null -w "%{http_code}" http://localhost:1313/ +``` + +If it returns `000` or connection refused, start Hugo: +```bash +npx hugo server +``` + +### 2. Use inspection before screenshots + +Inspection is faster and provides more context: +```bash +# First, inspect to understand the issue +yarn debug:inspect /path/ + +# Then take targeted screenshots if needed +yarn debug:screenshot /path/ --selector .problem-component +``` + +### 3. Prefer headless for automated checks + +Headless mode is faster and doesn't require display: +```javascript +const browser = await launchBrowser({ headless: true }); +``` + +Only use non-headless (`headless: false`) when you need to visually debug. + +### 4. Clean up resources + +Always close the browser when done: +```javascript +try { + const browser = await launchBrowser(); + const page = await navigateToPage(browser, '/path/'); + // ... do work +} finally { + await browser.close(); +} +``` + +### 5. Use meaningful screenshot names + +```bash +# Bad +yarn debug:screenshot /path/ --output screenshot.png + +# Good +yarn debug:screenshot /influxdb3/core/ --output debug/influxdb3-core-home-issue-123.png +``` + +### 6. Capture full context for bug reports + +When a user reports an issue, gather comprehensive context: +```bash +# 1. Inspection report +yarn debug:inspect /path/to/issue/ --output reports/issue-123-inspect.json --screenshot + +# 2. Full page screenshot +yarn debug:screenshot /path/to/issue/ --full-page --output reports/issue-123-full.png + +# 3. Element screenshot if specific +yarn debug:screenshot /path/to/issue/ --selector .problem-area --output reports/issue-123-element.png +``` + +## Integration with Development Workflow + +### Use in PR Reviews + +```bash +# Before changes +yarn debug:screenshot /path/ --output before.png + +# Make changes to code + +# After changes (restart Hugo if needed) +yarn debug:screenshot /path/ --output after.png + +# Compare visually +``` + +### Use for Component Development + +When developing a new component: + +```bash +# 1. Open browser to test interactively +yarn debug:browser /example/ --devtools + +# 2. Inspect for errors and components +yarn debug:inspect /example/ + +# 3. Take screenshots for documentation +yarn debug:screenshot /example/ --selector '[data-component="new-component"]' +``` + +### Use for Regression Testing + +```bash +# Create baseline screenshots +yarn debug:screenshot /influxdb3/core/ --output baselines/core-home.png +yarn debug:screenshot /influxdb3/core/get-started/ --output baselines/core-get-started.png + +# After changes, compare +yarn debug:screenshot /influxdb3/core/ --output current/core-home.png + +# Visual comparison (manual for now, can be automated with pixelmatch) +``` + +## Next Steps + +1. **Install dependencies** when you have network access: + ```bash + PUPPETEER_SKIP_DOWNLOAD=true yarn install + ``` + +2. **Configure Chrome path** if needed (see [Troubleshooting](#troubleshooting)) + +3. **Test the setup** with a simple example: + ```bash + npx hugo server # In one terminal + yarn debug:screenshot / # In another terminal + ``` + +4. **Start using for development** - See [Common Scenarios](#common-scenarios) + +## Related Documentation + +- [Puppeteer API Documentation](https://pptr.dev/) +- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) +- [Hugo Documentation](https://gohugo.io/documentation/) +- [Testing Guide](../../DOCS-TESTING.md) +- [Contributing Guide](../../DOCS-CONTRIBUTING.md) diff --git a/scripts/puppeteer/SETUP.md b/scripts/puppeteer/SETUP.md new file mode 100644 index 000000000..5c32fae95 --- /dev/null +++ b/scripts/puppeteer/SETUP.md @@ -0,0 +1,261 @@ +# Puppeteer Setup Guide + +Quick setup guide for AI agents using Puppeteer with docs-v2. + +## Installation + +### Option 1: Use System Chrome (Recommended for network-restricted environments) + +```bash +# Install Puppeteer without downloading Chrome +PUPPETEER_SKIP_DOWNLOAD=true yarn install +``` + +Then use the `--chrome` flag to point to your system Chrome: + +```bash +yarn debug:browser /influxdb3/core/ --chrome "/usr/bin/google-chrome" +``` + +**Find your Chrome path:** + +```bash +# macOS +which google-chrome-stable +# Usually: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome + +# Linux +which google-chrome +# Usually: /usr/bin/google-chrome or /usr/bin/chromium + +# Windows (PowerShell) +Get-Command chrome +# Usually: C:\Program Files\Google\Chrome\Application\chrome.exe +``` + +### Option 2: Install Puppeteer's Bundled Chrome (Requires network access) + +```bash +# Regular installation (downloads Chrome) +yarn install + +# Or install Puppeteer browser separately +npx puppeteer browsers install chrome +``` + +## Verification + +### Step 1: Check dependencies + +```bash +# Check if Puppeteer is in package.json +grep puppeteer package.json +``` + +Should show: +``` +"puppeteer": "^23.11.1", +``` + +### Step 2: Start Hugo server + +```bash +# Start Hugo development server +npx hugo server +``` + +Should show: +``` +Web Server is available at http://localhost:1313/ +``` + +### Step 3: Test Puppeteer + +```bash +# Test screenshot tool +yarn debug:screenshot / --output test-screenshot.png +``` + +If successful, you'll see: +``` +๐Ÿ“ธ Screenshot Utility +===================== + +URL: http://localhost:1313/ +Viewport: 1280x720 +Output: test-screenshot.png + +Navigating to: http://localhost:1313/ +โœ“ Page loaded successfully +โœ“ Screenshot saved: test-screenshot.png + +โœ“ Screenshot captured successfully +``` + +### Step 4: Verify screenshot was created + +```bash +ls -lh test-screenshot.png +``` + +Should show a PNG file (typically 100-500KB). + +## Troubleshooting + +### Issue: "Failed to launch browser" + +**Error message:** +``` +Failed to launch browser: Could not find Chrome +``` + +**Solution:** Use system Chrome with `--chrome` flag: + +```bash +# Find Chrome path +which google-chrome + +# Use with Puppeteer +yarn debug:browser / --chrome "$(which google-chrome)" +``` + +### Issue: "Failed to navigate to http://localhost:1313" + +**Error message:** +``` +Failed to navigate to http://localhost:1313/: net::ERR_CONNECTION_REFUSED +``` + +**Solution:** Start Hugo server: + +```bash +npx hugo server +``` + +### Issue: "PUPPETEER_SKIP_DOWNLOAD not working" + +**Error message:** +``` +ERROR: Failed to set up chrome-headless-shell +``` + +**Solution:** Set environment variable before yarn command: + +```bash +# Correct +PUPPETEER_SKIP_DOWNLOAD=true yarn install + +# Won't work +yarn install PUPPETEER_SKIP_DOWNLOAD=true +``` + +### Issue: "Command not found: yarn" + +**Solution:** Install Yarn or use npm: + +```bash +# Install Yarn +npm install -g yarn + +# Or use npm instead +npm run debug:screenshot -- / --output test.png +``` + +## Quick Test Script + +Save this as `test-puppeteer-setup.js` and run with `node test-puppeteer-setup.js`: + +```javascript +#!/usr/bin/env node + +/** + * Quick test to verify Puppeteer setup + */ + +import { launchBrowser, navigateToPage, takeScreenshot } from './utils/puppeteer-helpers.js'; + +async function test() { + console.log('\n๐Ÿงช Testing Puppeteer Setup\n'); + + let browser; + try { + // 1. Launch browser + console.log('1. Launching browser...'); + browser = await launchBrowser({ headless: true }); + console.log(' โœ“ Browser launched\n'); + + // 2. Navigate to home page + console.log('2. Navigating to home page...'); + const page = await navigateToPage(browser, '/'); + console.log(' โœ“ Page loaded\n'); + + // 3. Take screenshot + console.log('3. Taking screenshot...'); + await takeScreenshot(page, 'test-screenshot.png'); + console.log(' โœ“ Screenshot saved\n'); + + // 4. Get page title + console.log('4. Getting page title...'); + const title = await page.title(); + console.log(` โœ“ Title: "${title}"\n`); + + console.log('โœ… All tests passed!\n'); + console.log('Puppeteer is set up correctly and ready to use.\n'); + } catch (error) { + console.error('\nโŒ Test failed:', error.message); + console.error('\nSee SETUP.md for troubleshooting steps.\n'); + process.exit(1); + } finally { + if (browser) { + await browser.close(); + } + } +} + +test(); +``` + +Run the test: + +```bash +cd scripts/puppeteer +node test-puppeteer-setup.js +``` + +## Environment Variables + +You can set these environment variables to customize Puppeteer behavior: + +```bash +# Skip downloading Puppeteer's bundled Chrome +export PUPPETEER_SKIP_DOWNLOAD=true + +# Use custom Chrome path +export PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome + +# Disable headless mode by default +export PUPPETEER_HEADLESS=false +``` + +Add to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.) to make permanent. + +## Next Steps + +Once setup is verified: + +1. **Read the main README**: [README.md](README.md) +2. **Try the debugging tools**: + ```bash + yarn debug:inspect /influxdb3/core/ + yarn debug:screenshot /influxdb3/core/ --full-page + yarn debug:browser /influxdb3/core/ --devtools + ``` +3. **Create custom scripts** using the helper functions +4. **Integrate into your workflow** for testing and debugging + +## Getting Help + +- **Main documentation**: [README.md](README.md) +- **Helper functions**: [utils/puppeteer-helpers.js](utils/puppeteer-helpers.js) +- **Puppeteer docs**: https://pptr.dev/ +- **Report issues**: Create a GitHub issue in docs-v2 diff --git a/scripts/puppeteer/debug-browser.js b/scripts/puppeteer/debug-browser.js new file mode 100644 index 000000000..2b87adeed --- /dev/null +++ b/scripts/puppeteer/debug-browser.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +/** + * Interactive Browser Debugger for AI Agents + * + * This script launches a browser in non-headless mode so AI agents can + * visually debug issues during development. + * + * Usage: + * yarn debug:browser [url-path] [options] + * + * Examples: + * yarn debug:browser /influxdb3/core/ + * yarn debug:browser /influxdb3/core/ --devtools + * yarn debug:browser /influxdb3/core/ --slow-mo 100 + * + * Options: + * --devtools Open Chrome DevTools + * --slow-mo NUM Slow down by NUM milliseconds + * --viewport WxH Set viewport size (default: 1280x720) + * --base-url URL Set base URL (default: http://localhost:1313) + * --chrome PATH Path to Chrome executable + */ + +import { + launchBrowser, + navigateToPage, + debugPage, +} from './utils/puppeteer-helpers.js'; + +async function main() { + const args = process.argv.slice(2); + + // Parse arguments + const urlPath = args.find((arg) => !arg.startsWith('--')) || '/'; + const devtools = args.includes('--devtools'); + const slowMo = parseInt( + args.find((arg) => arg.startsWith('--slow-mo'))?.split('=')[1] || '0', + 10 + ); + const viewport = + args.find((arg) => arg.startsWith('--viewport'))?.split('=')[1] || + '1280x720'; + const baseUrl = + args.find((arg) => arg.startsWith('--base-url'))?.split('=')[1] || + 'http://localhost:1313'; + const chromePath = args + .find((arg) => arg.startsWith('--chrome')) + ?.split('=')[1]; + + const [width, height] = viewport.split('x').map(Number); + + console.log('\n๐Ÿ” Interactive Browser Debugger'); + console.log('================================\n'); + + let browser; + try { + // Launch browser + console.log('Launching browser...'); + browser = await launchBrowser({ + headless: false, + devtools, + slowMo, + executablePath: chromePath, + }); + + // Navigate to page + const page = await navigateToPage(browser, urlPath, { baseUrl }); + + // Set viewport + await page.setViewport({ width, height }); + console.log(`Viewport set to: ${width}x${height}`); + + // Enable console logging + page.on('console', (msg) => { + console.log(`[Browser Console ${msg.type()}]:`, msg.text()); + }); + + page.on('pageerror', (error) => { + console.error('[Page Error]:', error.message); + }); + + console.log('\nโœ“ Browser ready for debugging'); + console.log('\nThe browser will remain open for manual inspection.'); + console.log('Press Ctrl+C to close the browser and exit.\n'); + + // Keep the browser open until user interrupts + await new Promise((resolve) => { + process.on('SIGINT', () => { + console.log('\nClosing browser...'); + resolve(); + }); + }); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } finally { + if (browser) { + await browser.close(); + console.log('โœ“ Browser closed'); + } + } +} + +main(); diff --git a/scripts/puppeteer/examples/detect-issues.js b/scripts/puppeteer/examples/detect-issues.js new file mode 100644 index 000000000..2733f7a8a --- /dev/null +++ b/scripts/puppeteer/examples/detect-issues.js @@ -0,0 +1,318 @@ +#!/usr/bin/env node + +/** + * Example: Detect Common Issues + * + * This script demonstrates how to use Puppeteer to detect common issues + * in documentation pages: + * - Shortcode remnants (Hugo shortcodes that didn't render) + * - Broken images + * - Missing alt text + * - JavaScript errors + * - Slow page load + * + * Run with: node scripts/puppeteer/examples/detect-issues.js + */ + +import { + launchBrowser, + navigateToPage, + takeScreenshot, + getPageMetrics, +} from '../utils/puppeteer-helpers.js'; + +async function detectIssues(urlPath) { + console.log('\n๐Ÿ” Detecting Common Issues\n'); + console.log(`Page: ${urlPath}\n`); + + const issues = []; + let browser; + + try { + // Launch browser + browser = await launchBrowser({ headless: true }); + const page = await navigateToPage(browser, urlPath); + + // 1. Check for shortcode remnants + console.log('1. Checking for shortcode remnants...'); + const shortcodeRemnants = await page.evaluate(() => { + const html = document.documentElement.outerHTML; + const patterns = [ + { name: 'Hugo shortcode open', regex: /\{\{<[^>]+>\}\}/g }, + { name: 'Hugo shortcode percent', regex: /\{\{%[^%]+%\}\}/g }, + { name: 'Hugo variable', regex: /\{\{\s*\.[^\s}]+\s*\}\}/g }, + ]; + + const findings = []; + patterns.forEach(({ name, regex }) => { + const matches = html.match(regex); + if (matches) { + findings.push({ + type: name, + count: matches.length, + samples: matches.slice(0, 3), + }); + } + }); + + return findings; + }); + + if (shortcodeRemnants.length > 0) { + shortcodeRemnants.forEach((finding) => { + issues.push({ + severity: 'high', + type: 'shortcode-remnant', + message: `Found ${finding.count} instances of ${finding.type}`, + samples: finding.samples, + }); + }); + console.log( + ` โš ๏ธ Found ${shortcodeRemnants.length} types of shortcode remnants` + ); + } else { + console.log(' โœ“ No shortcode remnants detected'); + } + + // 2. Check for broken images + console.log('\n2. Checking for broken images...'); + const brokenImages = await page.evaluate(() => { + const images = Array.from(document.querySelectorAll('img')); + return images + .filter((img) => !img.complete || img.naturalWidth === 0) + .map((img) => ({ + src: img.src, + alt: img.alt, + })); + }); + + if (brokenImages.length > 0) { + issues.push({ + severity: 'high', + type: 'broken-images', + message: `Found ${brokenImages.length} broken images`, + details: brokenImages, + }); + console.log(` โš ๏ธ Found ${brokenImages.length} broken images`); + } else { + console.log(' โœ“ All images loaded successfully'); + } + + // 3. Check for images without alt text + console.log('\n3. Checking for accessibility issues...'); + const missingAltText = await page.evaluate(() => { + const images = Array.from(document.querySelectorAll('img:not([alt])')); + return images.map((img) => img.src); + }); + + if (missingAltText.length > 0) { + issues.push({ + severity: 'medium', + type: 'missing-alt-text', + message: `Found ${missingAltText.length} images without alt text`, + details: missingAltText, + }); + console.log( + ` โš ๏ธ Found ${missingAltText.length} images without alt text` + ); + } else { + console.log(' โœ“ All images have alt text'); + } + + // 4. Check for empty links + const emptyLinks = await page.evaluate(() => { + const links = Array.from( + document.querySelectorAll('a:not([aria-label])') + ); + return links + .filter((link) => !link.textContent.trim()) + .map((link) => link.href); + }); + + if (emptyLinks.length > 0) { + issues.push({ + severity: 'medium', + type: 'empty-links', + message: `Found ${emptyLinks.length} links without text`, + details: emptyLinks, + }); + console.log(` โš ๏ธ Found ${emptyLinks.length} links without text`); + } else { + console.log(' โœ“ All links have text or aria-label'); + } + + // 5. Check for JavaScript errors + console.log('\n4. Checking for JavaScript errors...'); + const jsErrors = []; + page.on('pageerror', (error) => { + jsErrors.push(error.message); + }); + page.on('console', (msg) => { + if (msg.type() === 'error') { + jsErrors.push(msg.text()); + } + }); + + // Wait a bit for errors to accumulate + await page.waitForTimeout(2000); + + if (jsErrors.length > 0) { + issues.push({ + severity: 'high', + type: 'javascript-errors', + message: `Found ${jsErrors.length} JavaScript errors`, + details: jsErrors, + }); + console.log(` โš ๏ธ Found ${jsErrors.length} JavaScript errors`); + } else { + console.log(' โœ“ No JavaScript errors detected'); + } + + // 6. Check page performance + console.log('\n5. Checking page performance...'); + const metrics = await getPageMetrics(page); + const loadTime = metrics.performance?.loadComplete || 0; + const fcp = metrics.performance?.firstContentfulPaint || 0; + + if (loadTime > 3000) { + issues.push({ + severity: 'medium', + type: 'slow-load', + message: `Page load time is ${loadTime.toFixed(0)}ms (> 3000ms)`, + }); + console.log(` โš ๏ธ Slow page load: ${loadTime.toFixed(0)}ms`); + } else { + console.log(` โœ“ Page load time: ${loadTime.toFixed(0)}ms`); + } + + if (fcp > 1500) { + issues.push({ + severity: 'low', + type: 'slow-fcp', + message: `First Contentful Paint is ${fcp.toFixed(0)}ms (> 1500ms)`, + }); + console.log(` โš ๏ธ Slow FCP: ${fcp.toFixed(0)}ms`); + } else { + console.log(` โœ“ First Contentful Paint: ${fcp.toFixed(0)}ms`); + } + + // 7. Check for missing heading hierarchy + console.log('\n6. Checking heading structure...'); + const headingIssues = await page.evaluate(() => { + const headings = Array.from( + document.querySelectorAll('h1, h2, h3, h4, h5, h6') + ); + const issues = []; + + // Check for multiple h1s + const h1s = headings.filter((h) => h.tagName === 'H1'); + if (h1s.length === 0) { + issues.push('No h1 found'); + } else if (h1s.length > 1) { + issues.push(`Multiple h1s found (${h1s.length})`); + } + + // Check for skipped heading levels + let prevLevel = 0; + headings.forEach((heading) => { + const level = parseInt(heading.tagName.substring(1)); + if (prevLevel > 0 && level > prevLevel + 1) { + issues.push(`Skipped from h${prevLevel} to h${level}`); + } + prevLevel = level; + }); + + return issues; + }); + + if (headingIssues.length > 0) { + issues.push({ + severity: 'low', + type: 'heading-structure', + message: 'Heading structure issues detected', + details: headingIssues, + }); + console.log(' โš ๏ธ Heading structure issues:'); + headingIssues.forEach((issue) => console.log(` - ${issue}`)); + } else { + console.log(' โœ“ Heading structure is correct'); + } + + // Take screenshot if issues found + if (issues.length > 0) { + console.log('\n๐Ÿ“ธ Taking screenshot for reference...'); + await takeScreenshot(page, `issues-detected-${Date.now()}.png`, { + fullPage: true, + }); + } + + // Summary + console.log('\n' + '='.repeat(50)); + console.log('SUMMARY'); + console.log('='.repeat(50) + '\n'); + + if (issues.length === 0) { + console.log('โœ… No issues detected!\n'); + } else { + const high = issues.filter((i) => i.severity === 'high').length; + const medium = issues.filter((i) => i.severity === 'medium').length; + const low = issues.filter((i) => i.severity === 'low').length; + + console.log(`Found ${issues.length} issue(s):\n`); + console.log(` High: ${high}`); + console.log(` Medium: ${medium}`); + console.log(` Low: ${low}\n`); + + console.log('Details:\n'); + issues.forEach((issue, index) => { + const icon = + issue.severity === 'high' + ? '๐Ÿ”ด' + : issue.severity === 'medium' + ? '๐ŸŸก' + : '๐Ÿ”ต'; + console.log( + `${index + 1}. ${icon} [${issue.severity.toUpperCase()}] ${issue.message}` + ); + + if (issue.samples && issue.samples.length > 0) { + console.log(' Samples:'); + issue.samples.forEach((sample) => { + console.log(` - "${sample}"`); + }); + } + + if ( + issue.details && + issue.details.length > 0 && + issue.details.length <= 5 + ) { + console.log(' Details:'); + issue.details.forEach((detail) => { + const str = + typeof detail === 'string' ? detail : JSON.stringify(detail); + console.log(` - ${str}`); + }); + } + + console.log(''); + }); + } + } catch (error) { + console.error('\nโŒ Error:', error.message); + throw error; + } finally { + if (browser) { + await browser.close(); + } + } + + return issues; +} + +// CLI +const urlPath = process.argv[2] || '/'; +detectIssues(urlPath).catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/scripts/puppeteer/examples/test-format-selector.js b/scripts/puppeteer/examples/test-format-selector.js new file mode 100644 index 000000000..6c31fc34f --- /dev/null +++ b/scripts/puppeteer/examples/test-format-selector.js @@ -0,0 +1,180 @@ +#!/usr/bin/env node + +/** + * Example: Test Format Selector Component + * + * This example demonstrates how to use Puppeteer to test an interactive + * component on the documentation site. + * + * Run with: node scripts/puppeteer/examples/test-format-selector.js + */ + +import { + launchBrowser, + navigateToPage, + takeScreenshot, + elementExists, + waitForElement, + clickAndNavigate, + debugPage, +} from '../utils/puppeteer-helpers.js'; + +async function testFormatSelector() { + console.log('\n๐Ÿงช Testing Format Selector Component\n'); + + let browser; + try { + // Launch browser + console.log('Launching browser...'); + browser = await launchBrowser({ headless: true }); + + // Navigate to a page with format selector + console.log('Navigating to page...'); + const page = await navigateToPage(browser, '/influxdb3/core/get-started/'); + + // Check if format selector exists + console.log('\n1. Checking if format selector exists...'); + const hasFormatSelector = await elementExists( + page, + '[data-component="format-selector"]' + ); + + if (!hasFormatSelector) { + console.log(' โš ๏ธ Format selector not found on this page'); + console.log( + " This is expected if the page doesn't have multiple formats" + ); + return; + } + console.log(' โœ“ Format selector found'); + + // Take initial screenshot + console.log('\n2. Capturing initial state...'); + await takeScreenshot(page, 'format-selector-initial.png', { + selector: '[data-component="format-selector"]', + }); + console.log(' โœ“ Screenshot saved'); + + // Click the format selector button + console.log('\n3. Testing dropdown interaction...'); + const buttonExists = await elementExists( + page, + '[data-component="format-selector"] button' + ); + + if (buttonExists) { + // Click button to open dropdown + await page.click('[data-component="format-selector"] button'); + console.log(' โœ“ Clicked format selector button'); + + // Wait for dropdown menu to appear + await waitForElement( + page, + '[data-component="format-selector"] [role="menu"]', + 3000 + ); + console.log(' โœ“ Dropdown menu appeared'); + + // Take screenshot of open dropdown + await takeScreenshot(page, 'format-selector-open.png', { + selector: '[data-component="format-selector"]', + }); + console.log(' โœ“ Screenshot of open dropdown saved'); + + // Get all format options + const options = await page.$$eval( + '[data-component="format-selector"] [role="menuitem"]', + (items) => items.map((item) => item.textContent.trim()) + ); + console.log(` โœ“ Found ${options.length} format options:`, options); + + // Test clicking each option + console.log('\n4. Testing format options...'); + const menuItems = await page.$$( + '[data-component="format-selector"] [role="menuitem"]' + ); + + for (let i = 0; i < Math.min(menuItems.length, 3); i++) { + const option = options[i]; + console.log(` Testing option: ${option}`); + + // Click the format selector button again (it closes after selection) + await page.click('[data-component="format-selector"] button'); + await page.waitForTimeout(300); // Wait for animation + + // Click the option + await page.click( + `[data-component="format-selector"] [role="menuitem"]:nth-child(${i + 1})` + ); + await page.waitForTimeout(500); // Wait for content to update + + // Take screenshot of result + await takeScreenshot( + page, + `format-selector-${option.toLowerCase().replace(/\s+/g, '-')}.png` + ); + console.log(` โœ“ Tested ${option} format`); + } + } + + // Check for JavaScript errors + console.log('\n5. Checking for JavaScript errors...'); + const errors = await page.evaluate(() => { + // Check if any errors were logged + return window.__errors || []; + }); + + if (errors.length > 0) { + console.log(' โš ๏ธ Found JavaScript errors:'); + errors.forEach((err) => console.log(` - ${err}`)); + } else { + console.log(' โœ“ No JavaScript errors detected'); + } + + // Get computed styles + console.log('\n6. Checking component styles...'); + const styles = await page.evaluate(() => { + const selector = document.querySelector( + '[data-component="format-selector"]' + ); + if (!selector) return null; + + const computed = window.getComputedStyle(selector); + return { + display: computed.display, + visibility: computed.visibility, + opacity: computed.opacity, + }; + }); + + if (styles) { + console.log(' Component styles:', styles); + console.log(' โœ“ Component is visible'); + } + + console.log('\nโœ… Format selector tests completed successfully!\n'); + } catch (error) { + console.error('\nโŒ Test failed:', error.message); + + // Save debug output + if (browser) { + const pages = await browser.pages(); + if (pages.length > 0) { + await debugPage(pages[0], 'format-selector-error'); + console.log('\n๐Ÿ’พ Debug information saved to debug-output/'); + } + } + + throw error; + } finally { + if (browser) { + await browser.close(); + } + } +} + +// Run the test +testFormatSelector().catch((error) => { + console.error('\nFatal error:', error); + process.exit(1); +}); diff --git a/scripts/puppeteer/inspect-page.js b/scripts/puppeteer/inspect-page.js new file mode 100644 index 000000000..9259d184c --- /dev/null +++ b/scripts/puppeteer/inspect-page.js @@ -0,0 +1,328 @@ +#!/usr/bin/env node + +/** + * Page Inspector for AI Agents + * + * This script inspects a page and provides detailed information for debugging: + * - Page metadata + * - Performance metrics + * - Console errors + * - Links analysis + * - Component detection + * + * Usage: + * yarn debug:inspect [options] + * + * Examples: + * yarn debug:inspect /influxdb3/core/ + * yarn debug:inspect /influxdb3/core/ --output report.json + * yarn debug:inspect /influxdb3/core/ --screenshot + * + * Options: + * --output PATH Save report to JSON file + * --screenshot Also capture a screenshot + * --base-url URL Set base URL (default: http://localhost:1313) + * --chrome PATH Path to Chrome executable + */ + +import { + launchBrowser, + navigateToPage, + takeScreenshot, + getPageMetrics, + getPageLinks, + elementExists, + getElementText, +} from './utils/puppeteer-helpers.js'; +import fs from 'fs/promises'; + +async function inspectPage(page) { + console.log('Inspecting page...\n'); + + const report = {}; + + // 1. Page metadata + console.log('1. Gathering page metadata...'); + report.metadata = await page.evaluate(() => ({ + title: document.title, + url: window.location.href, + description: document.querySelector('meta[name="description"]')?.content, + viewport: document.querySelector('meta[name="viewport"]')?.content, + lang: document.documentElement.lang, + })); + + // 2. Performance metrics + console.log('2. Collecting performance metrics...'); + report.performance = await getPageMetrics(page); + + // 3. Console errors + console.log('3. Checking for console errors...'); + const errors = []; + page.on('pageerror', (error) => { + errors.push({ type: 'pageerror', message: error.message }); + }); + page.on('console', (msg) => { + if (msg.type() === 'error') { + errors.push({ type: 'console', message: msg.text() }); + } + }); + // Wait a bit for errors to accumulate + await page.waitForTimeout(1000); + report.errors = errors; + + // 4. Links analysis + console.log('4. Analyzing links...'); + const links = await getPageLinks(page); + report.links = { + total: links.length, + internal: links.filter((l) => !l.isExternal).length, + external: links.filter((l) => l.isExternal).length, + list: links, + }; + + // 5. Component detection + console.log('5. Detecting components...'); + const components = await page.evaluate(() => { + const componentElements = document.querySelectorAll('[data-component]'); + return Array.from(componentElements).map((el) => ({ + type: el.getAttribute('data-component'), + id: el.id, + classes: Array.from(el.classList), + })); + }); + report.components = components; + + // 6. Hugo shortcode detection + console.log('6. Checking for shortcode remnants...'); + const shortcodeRemnants = await page.evaluate(() => { + const html = document.documentElement.outerHTML; + const patterns = [ + /\{\{<[^>]+>\}\}/g, + /\{\{%[^%]+%\}\}/g, + /\{\{-?[^}]+-?\}\}/g, + ]; + + const findings = []; + patterns.forEach((pattern, index) => { + const matches = html.match(pattern); + if (matches) { + findings.push({ + pattern: pattern.toString(), + count: matches.length, + samples: matches.slice(0, 3), + }); + } + }); + + return findings; + }); + report.shortcodeRemnants = shortcodeRemnants; + + // 7. Accessibility quick check + console.log('7. Running basic accessibility checks...'); + report.accessibility = await page.evaluate(() => { + return { + hasMainLandmark: !!document.querySelector('main'), + hasH1: !!document.querySelector('h1'), + h1Text: document.querySelector('h1')?.textContent, + imagesWithoutAlt: Array.from(document.querySelectorAll('img:not([alt])')) + .length, + linksWithoutText: Array.from( + document.querySelectorAll('a:not([aria-label])') + ).filter((a) => !a.textContent.trim()).length, + }; + }); + + // 8. Content structure + console.log('8. Analyzing content structure...'); + report.contentStructure = await page.evaluate(() => { + const headings = Array.from( + document.querySelectorAll('h1, h2, h3, h4, h5, h6') + ).map((h) => ({ + level: parseInt(h.tagName.substring(1)), + text: h.textContent.trim(), + })); + + const codeBlocks = Array.from(document.querySelectorAll('pre code')).map( + (block) => ({ + language: Array.from(block.classList) + .find((c) => c.startsWith('language-')) + ?.substring(9), + lines: block.textContent.split('\n').length, + }) + ); + + return { + headings, + codeBlocks: { + total: codeBlocks.length, + byLanguage: codeBlocks.reduce((acc, block) => { + const lang = block.language || 'unknown'; + acc[lang] = (acc[lang] || 0) + 1; + return acc; + }, {}), + }, + }; + }); + + return report; +} + +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0 || args[0].startsWith('--')) { + console.error('Error: URL path is required'); + console.log('\nUsage: yarn debug:inspect [options]'); + console.log('\nExample: yarn debug:inspect /influxdb3/core/ --screenshot'); + process.exit(1); + } + + // Parse arguments + const urlPath = args.find((arg) => !arg.startsWith('--')); + const outputPath = args + .find((arg) => arg.startsWith('--output')) + ?.split('=')[1]; + const takeScreenshotFlag = args.includes('--screenshot'); + const baseUrl = + args.find((arg) => arg.startsWith('--base-url'))?.split('=')[1] || + 'http://localhost:1313'; + const chromePath = args + .find((arg) => arg.startsWith('--chrome')) + ?.split('=')[1]; + + console.log('\n๐Ÿ” Page Inspector'); + console.log('=================\n'); + console.log(`Inspecting: ${baseUrl}${urlPath}\n`); + + let browser; + try { + // Launch browser + browser = await launchBrowser({ + headless: true, + executablePath: chromePath, + }); + + // Navigate to page + const page = await navigateToPage(browser, urlPath, { baseUrl }); + + // Inspect page + const report = await inspectPage(page); + + // Take screenshot if requested + if (takeScreenshotFlag) { + const screenshotPath = outputPath + ? outputPath.replace('.json', '.png') + : `inspect-${new Date().toISOString().replace(/[:.]/g, '-')}.png`; + await takeScreenshot(page, screenshotPath); + report.screenshot = screenshotPath; + } + + // Display report + console.log('\n๐Ÿ“Š Inspection Report'); + console.log('===================\n'); + + console.log('Metadata:'); + console.log(` Title: ${report.metadata.title}`); + console.log(` URL: ${report.metadata.url}`); + console.log(` Description: ${report.metadata.description || 'N/A'}\n`); + + console.log('Performance:'); + console.log( + ` DOM Content Loaded: ${report.performance.performance?.domContentLoaded?.toFixed(2) || 'N/A'}ms` + ); + console.log( + ` Load Complete: ${report.performance.performance?.loadComplete?.toFixed(2) || 'N/A'}ms` + ); + console.log( + ` First Paint: ${report.performance.performance?.firstPaint?.toFixed(2) || 'N/A'}ms` + ); + console.log( + ` FCP: ${report.performance.performance?.firstContentfulPaint?.toFixed(2) || 'N/A'}ms\n` + ); + + console.log('Errors:'); + if (report.errors.length > 0) { + report.errors.forEach((err) => { + console.log(` โŒ [${err.type}] ${err.message}`); + }); + } else { + console.log(' โœ“ No errors detected'); + } + console.log(''); + + console.log('Links:'); + console.log(` Total: ${report.links.total}`); + console.log(` Internal: ${report.links.internal}`); + console.log(` External: ${report.links.external}\n`); + + console.log('Components:'); + if (report.components.length > 0) { + report.components.forEach((comp) => { + console.log(` - ${comp.type}${comp.id ? ` (id: ${comp.id})` : ''}`); + }); + } else { + console.log(' None detected'); + } + console.log(''); + + console.log('Shortcode Remnants:'); + if (report.shortcodeRemnants.length > 0) { + console.log(' โš ๏ธ Found shortcode remnants:'); + report.shortcodeRemnants.forEach((finding) => { + console.log(` - ${finding.count} matches for ${finding.pattern}`); + finding.samples.forEach((sample) => { + console.log(` "${sample}"`); + }); + }); + } else { + console.log(' โœ“ No shortcode remnants detected'); + } + console.log(''); + + console.log('Accessibility:'); + console.log( + ` Main landmark: ${report.accessibility.hasMainLandmark ? 'โœ“' : 'โŒ'}` + ); + console.log(` H1 present: ${report.accessibility.hasH1 ? 'โœ“' : 'โŒ'}`); + if (report.accessibility.h1Text) { + console.log(` H1 text: "${report.accessibility.h1Text}"`); + } + console.log( + ` Images without alt: ${report.accessibility.imagesWithoutAlt}` + ); + console.log( + ` Links without text: ${report.accessibility.linksWithoutText}\n` + ); + + console.log('Content Structure:'); + console.log(` Headings: ${report.contentStructure.headings.length}`); + console.log(` Code blocks: ${report.contentStructure.codeBlocks.total}`); + if (Object.keys(report.contentStructure.codeBlocks.byLanguage).length > 0) { + console.log(' Languages:'); + Object.entries(report.contentStructure.codeBlocks.byLanguage).forEach( + ([lang, count]) => { + console.log(` - ${lang}: ${count}`); + } + ); + } + console.log(''); + + // Save report if requested + if (outputPath) { + await fs.writeFile(outputPath, JSON.stringify(report, null, 2)); + console.log(`\nโœ“ Report saved to: ${outputPath}`); + } + + console.log(''); + } catch (error) { + console.error('\nError:', error.message); + process.exit(1); + } finally { + if (browser) { + await browser.close(); + } + } +} + +main(); diff --git a/scripts/puppeteer/screenshot.js b/scripts/puppeteer/screenshot.js new file mode 100644 index 000000000..c1146850d --- /dev/null +++ b/scripts/puppeteer/screenshot.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node + +/** + * Screenshot Utility for AI Agents + * + * This script takes screenshots of documentation pages for debugging and validation. + * + * Usage: + * yarn debug:screenshot [options] + * + * Examples: + * yarn debug:screenshot /influxdb3/core/ + * yarn debug:screenshot /influxdb3/core/ --output debug.png + * yarn debug:screenshot /influxdb3/core/ --full-page + * yarn debug:screenshot /influxdb3/core/ --selector .article--content + * yarn debug:screenshot /influxdb3/core/ --viewport 375x667 + * + * Options: + * --output PATH Output file path (default: screenshot-{timestamp}.png) + * --full-page Capture full page scroll + * --selector SELECTOR Capture specific element + * --viewport WxH Set viewport size (default: 1280x720) + * --base-url URL Set base URL (default: http://localhost:1313) + * --chrome PATH Path to Chrome executable + */ + +import { + launchBrowser, + navigateToPage, + takeScreenshot, +} from './utils/puppeteer-helpers.js'; +import path from 'path'; + +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0 || args[0].startsWith('--')) { + console.error('Error: URL path is required'); + console.log('\nUsage: yarn debug:screenshot [options]'); + console.log( + '\nExample: yarn debug:screenshot /influxdb3/core/ --full-page' + ); + process.exit(1); + } + + // Parse arguments + const urlPath = args.find((arg) => !arg.startsWith('--')); + const output = + args.find((arg) => arg.startsWith('--output'))?.split('=')[1] || + `screenshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`; + const fullPage = args.includes('--full-page'); + const selector = args + .find((arg) => arg.startsWith('--selector')) + ?.split('=')[1]; + const viewport = + args.find((arg) => arg.startsWith('--viewport'))?.split('=')[1] || + '1280x720'; + const baseUrl = + args.find((arg) => arg.startsWith('--base-url'))?.split('=')[1] || + 'http://localhost:1313'; + const chromePath = args + .find((arg) => arg.startsWith('--chrome')) + ?.split('=')[1]; + + const [width, height] = viewport.split('x').map(Number); + + console.log('\n๐Ÿ“ธ Screenshot Utility'); + console.log('=====================\n'); + console.log(`URL: ${baseUrl}${urlPath}`); + console.log(`Viewport: ${width}x${height}`); + console.log(`Output: ${output}`); + if (fullPage) console.log('Mode: Full page'); + if (selector) console.log(`Element: ${selector}`); + console.log(''); + + let browser; + try { + // Launch browser + browser = await launchBrowser({ + headless: true, + executablePath: chromePath, + }); + + // Navigate to page + const page = await navigateToPage(browser, urlPath, { baseUrl }); + + // Set viewport + await page.setViewport({ width, height }); + + // Take screenshot + await takeScreenshot(page, output, { fullPage, selector }); + + console.log('\nโœ“ Screenshot captured successfully\n'); + } catch (error) { + console.error('\nError:', error.message); + process.exit(1); + } finally { + if (browser) { + await browser.close(); + } + } +} + +main(); diff --git a/scripts/puppeteer/utils/puppeteer-helpers.js b/scripts/puppeteer/utils/puppeteer-helpers.js new file mode 100644 index 000000000..513420f08 --- /dev/null +++ b/scripts/puppeteer/utils/puppeteer-helpers.js @@ -0,0 +1,486 @@ +/** + * Puppeteer Helper Utilities for AI Agent Development + * + * This module provides reusable functions for AI agents to debug and test + * the documentation site during development. + * + * Usage: + * import { launchBrowser, navigateToPage, takeScreenshot } from './utils/puppeteer-helpers.js'; + * + * const browser = await launchBrowser(); + * const page = await navigateToPage(browser, '/influxdb3/core/'); + * await takeScreenshot(page, 'debug-screenshot.png'); + * await browser.close(); + */ + +import puppeteer from 'puppeteer'; +import fs from 'fs/promises'; +import path from 'path'; + +/** + * Launch a browser instance + * + * @param {Object} options - Browser launch options + * @param {boolean} options.headless - Run in headless mode (default: true) + * @param {boolean} options.devtools - Open DevTools (default: false) + * @param {number} options.slowMo - Slow down operations by ms (default: 0) + * @returns {Promise} Puppeteer browser instance + */ +export async function launchBrowser(options = {}) { + const { + headless = true, + devtools = false, + slowMo = 0, + executablePath = null, + } = options; + + const launchOptions = { + headless: headless ? 'new' : false, + devtools, + slowMo, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-web-security', // Allow cross-origin requests for local development + ], + }; + + // Use system Chrome if available (useful when PUPPETEER_SKIP_DOWNLOAD was used) + if (executablePath) { + launchOptions.executablePath = executablePath; + } + + try { + return await puppeteer.launch(launchOptions); + } catch (error) { + console.error('Failed to launch browser:', error.message); + console.log('\nTroubleshooting:'); + console.log( + '1. If Puppeteer browser not installed, set executablePath to system Chrome' + ); + console.log('2. Common paths:'); + console.log( + ' - macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + ); + console.log(' - Linux: /usr/bin/google-chrome'); + console.log( + ' - Windows: C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' + ); + console.log( + '3. Or install Puppeteer browser: PUPPETEER_SKIP_DOWNLOAD=false yarn add puppeteer' + ); + throw error; + } +} + +/** + * Navigate to a page on the local Hugo server + * + * @param {Browser} browser - Puppeteer browser instance + * @param {string} urlPath - URL path (e.g., '/influxdb3/core/') + * @param {Object} options - Navigation options + * @param {string} options.baseUrl - Base URL (default: 'http://localhost:1313') + * @param {number} options.timeout - Navigation timeout in ms (default: 30000) + * @param {string} options.waitUntil - When to consider navigation succeeded (default: 'networkidle2') + * @returns {Promise} Puppeteer page instance + */ +export async function navigateToPage(browser, urlPath, options = {}) { + const { + baseUrl = 'http://localhost:1313', + timeout = 30000, + waitUntil = 'networkidle2', + } = options; + + const page = await browser.newPage(); + + // Set viewport size + await page.setViewport({ width: 1280, height: 720 }); + + // Enable console logging from the page + page.on('console', (msg) => { + const type = msg.type(); + if (type === 'error' || type === 'warning') { + console.log(`[Browser ${type}]:`, msg.text()); + } + }); + + // Log page errors + page.on('pageerror', (error) => { + console.error('[Page Error]:', error.message); + }); + + const fullUrl = `${baseUrl}${urlPath}`; + console.log(`Navigating to: ${fullUrl}`); + + try { + await page.goto(fullUrl, { waitUntil, timeout }); + console.log('โœ“ Page loaded successfully'); + return page; + } catch (error) { + console.error(`Failed to navigate to ${fullUrl}:`, error.message); + console.log('\nTroubleshooting:'); + console.log('1. Make sure Hugo server is running: yarn hugo server'); + console.log('2. Check if the URL path is correct'); + console.log('3. Try increasing timeout if page is slow to load'); + throw error; + } +} + +/** + * Take a screenshot of the page or a specific element + * + * @param {Page} page - Puppeteer page instance + * @param {string} outputPath - Output file path + * @param {Object} options - Screenshot options + * @param {string} options.selector - CSS selector to screenshot specific element + * @param {boolean} options.fullPage - Capture full page (default: false) + * @param {Object} options.clip - Clip region {x, y, width, height} + * @returns {Promise} + */ +export async function takeScreenshot(page, outputPath, options = {}) { + const { selector, fullPage = false, clip } = options; + + // Ensure output directory exists + const dir = path.dirname(outputPath); + await fs.mkdir(dir, { recursive: true }); + + const screenshotOptions = { + path: outputPath, + fullPage, + }; + + if (clip) { + screenshotOptions.clip = clip; + } + + try { + if (selector) { + const element = await page.$(selector); + if (!element) { + throw new Error(`Element not found: ${selector}`); + } + await element.screenshot({ path: outputPath }); + console.log(`โœ“ Screenshot saved (element: ${selector}): ${outputPath}`); + } else { + await page.screenshot(screenshotOptions); + console.log(`โœ“ Screenshot saved: ${outputPath}`); + } + } catch (error) { + console.error('Failed to take screenshot:', error.message); + throw error; + } +} + +/** + * Get page metrics and performance data + * + * @param {Page} page - Puppeteer page instance + * @returns {Promise} Performance metrics + */ +export async function getPageMetrics(page) { + const metrics = await page.metrics(); + + const performanceData = await page.evaluate(() => { + const perfData = window.performance.getEntriesByType('navigation')[0]; + return { + domContentLoaded: + perfData?.domContentLoadedEventEnd - + perfData?.domContentLoadedEventStart, + loadComplete: perfData?.loadEventEnd - perfData?.loadEventStart, + firstPaint: performance.getEntriesByType('paint')[0]?.startTime, + firstContentfulPaint: performance.getEntriesByType('paint')[1]?.startTime, + }; + }); + + return { + ...metrics, + performance: performanceData, + }; +} + +/** + * Check for JavaScript errors on the page + * + * @param {Page} page - Puppeteer page instance + * @returns {Promise} Array of error messages + */ +export async function getPageErrors(page) { + const errors = []; + + page.on('pageerror', (error) => { + errors.push(error.message); + }); + + page.on('console', (msg) => { + if (msg.type() === 'error') { + errors.push(msg.text()); + } + }); + + return errors; +} + +/** + * Get all links on the page + * + * @param {Page} page - Puppeteer page instance + * @returns {Promise} Array of link objects {href, text} + */ +export async function getPageLinks(page) { + return await page.evaluate(() => { + const links = Array.from(document.querySelectorAll('a')); + return links.map((link) => ({ + href: link.href, + text: link.textContent.trim(), + isExternal: !link.href.startsWith(window.location.origin), + })); + }); +} + +/** + * Check if an element exists on the page + * + * @param {Page} page - Puppeteer page instance + * @param {string} selector - CSS selector + * @returns {Promise} True if element exists + */ +export async function elementExists(page, selector) { + return (await page.$(selector)) !== null; +} + +/** + * Wait for an element to appear on the page + * + * @param {Page} page - Puppeteer page instance + * @param {string} selector - CSS selector + * @param {number} timeout - Timeout in ms (default: 5000) + * @returns {Promise} Element handle + */ +export async function waitForElement(page, selector, timeout = 5000) { + try { + return await page.waitForSelector(selector, { timeout }); + } catch (error) { + console.error(`Element not found within ${timeout}ms: ${selector}`); + throw error; + } +} + +/** + * Get text content of an element + * + * @param {Page} page - Puppeteer page instance + * @param {string} selector - CSS selector + * @returns {Promise} Text content + */ +export async function getElementText(page, selector) { + const element = await page.$(selector); + if (!element) { + throw new Error(`Element not found: ${selector}`); + } + return await page.evaluate((el) => el.textContent, element); +} + +/** + * Click an element and wait for navigation + * + * @param {Page} page - Puppeteer page instance + * @param {string} selector - CSS selector + * @param {Object} options - Click options + * @returns {Promise} + */ +export async function clickAndNavigate(page, selector, options = {}) { + const { waitUntil = 'networkidle2' } = options; + + await Promise.all([ + page.waitForNavigation({ waitUntil }), + page.click(selector), + ]); +} + +/** + * Test a component's interactive behavior + * + * @param {Page} page - Puppeteer page instance + * @param {string} componentSelector - Component selector + * @param {Function} testFn - Test function to run + * @returns {Promise} Test function result + */ +export async function testComponent(page, componentSelector, testFn) { + const component = await page.$(componentSelector); + if (!component) { + throw new Error(`Component not found: ${componentSelector}`); + } + + console.log(`Testing component: ${componentSelector}`); + return await testFn(page, component); +} + +/** + * Capture console logs from the page + * + * @param {Page} page - Puppeteer page instance + * @returns {Array} Array to store console logs + */ +export function captureConsoleLogs(page) { + const logs = []; + + page.on('console', (msg) => { + logs.push({ + type: msg.type(), + text: msg.text(), + location: msg.location(), + }); + }); + + return logs; +} + +/** + * Get computed styles for an element + * + * @param {Page} page - Puppeteer page instance + * @param {string} selector - CSS selector + * @param {Array} properties - CSS properties to get + * @returns {Promise} Computed styles object + */ +export async function getComputedStyles(page, selector, properties = []) { + return await page.evaluate( + (sel, props) => { + const element = document.querySelector(sel); + if (!element) return null; + + const styles = window.getComputedStyle(element); + if (props.length === 0) { + return Object.fromEntries( + Array.from(styles).map((prop) => [ + prop, + styles.getPropertyValue(prop), + ]) + ); + } + + return Object.fromEntries( + props.map((prop) => [prop, styles.getPropertyValue(prop)]) + ); + }, + selector, + properties + ); +} + +/** + * Check responsive design at different viewports + * + * @param {Page} page - Puppeteer page instance + * @param {Array} viewports - Array of {width, height, name} objects + * @param {Function} testFn - Test function to run at each viewport + * @returns {Promise} Test results for each viewport + */ +export async function testResponsive(page, viewports, testFn) { + const results = []; + + for (const viewport of viewports) { + console.log( + `Testing at ${viewport.name || `${viewport.width}x${viewport.height}`}` + ); + await page.setViewport({ width: viewport.width, height: viewport.height }); + const result = await testFn(page, viewport); + results.push({ viewport, result }); + } + + return results; +} + +/** + * Compare two screenshots for visual regression + * + * @param {string} baselinePath - Path to baseline screenshot + * @param {string} currentPath - Path to current screenshot + * @param {string} diffPath - Path to save diff image + * @param {Object} options - Comparison options + * @returns {Promise} Comparison result {match, diffPixels, diffPercentage} + */ +export async function compareScreenshots( + baselinePath, + currentPath, + diffPath, + options = {} +) { + const { threshold = 0.1 } = options; + + // This function requires pixelmatch - will be implemented when pixelmatch is available + try { + const { default: pixelmatch } = await import('pixelmatch'); + const { PNG } = await import('pngjs'); + + const baseline = PNG.sync.read(await fs.readFile(baselinePath)); + const current = PNG.sync.read(await fs.readFile(currentPath)); + const { width, height } = baseline; + const diff = new PNG({ width, height }); + + const diffPixels = pixelmatch( + baseline.data, + current.data, + diff.data, + width, + height, + { threshold } + ); + + const diffPercentage = (diffPixels / (width * height)) * 100; + + if (diffPath) { + await fs.writeFile(diffPath, PNG.sync.write(diff)); + } + + return { + match: diffPixels === 0, + diffPixels, + diffPercentage, + }; + } catch (error) { + console.warn( + 'Screenshot comparison requires pixelmatch and pngjs packages' + ); + console.warn( + 'Install with: PUPPETEER_SKIP_DOWNLOAD=true yarn add -D pixelmatch pngjs' + ); + throw error; + } +} + +/** + * Debug helper: Save page HTML and screenshot + * + * @param {Page} page - Puppeteer page instance + * @param {string} debugName - Debug session name + * @returns {Promise} + */ +export async function debugPage(page, debugName = 'debug') { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const debugDir = `debug-output/${debugName}-${timestamp}`; + + await fs.mkdir(debugDir, { recursive: true }); + + // Save HTML + const html = await page.content(); + await fs.writeFile(`${debugDir}/page.html`, html); + + // Save screenshot + await takeScreenshot(page, `${debugDir}/screenshot.png`, { fullPage: true }); + + // Save console logs if captured + const consoleLogs = await page.evaluate(() => { + return window.__consoleLogs || []; + }); + await fs.writeFile( + `${debugDir}/console-logs.json`, + JSON.stringify(consoleLogs, null, 2) + ); + + console.log(`โœ“ Debug output saved to: ${debugDir}`); + console.log(` - page.html`); + console.log(` - screenshot.png`); + console.log(` - console-logs.json`); +}