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 <noreply@anthropic.com>
pull/6718/head
Jason Stirnaman 2026-01-14 17:26:27 -06:00 committed by GitHub
parent 925a26e580
commit 784956a31c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 2612 additions and 1 deletions

View File

@ -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": [

17
scripts/puppeteer/.gitignore vendored Normal file
View File

@ -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/

View File

@ -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 <path> # Basic screenshot
yarn debug:screenshot <path> --full-page # Full scrollable page
yarn debug:screenshot <path> --selector .class # Specific element
yarn debug:screenshot <path> --viewport 375x667 # Mobile size
```
### Inspect Page
```bash
yarn debug:inspect <path> # Full analysis
yarn debug:inspect <path> --output report.json # Save report
yarn debug:inspect <path> --screenshot # Include screenshot
```
### Open Browser
```bash
yarn debug:browser <path> # Interactive mode
yarn debug:browser <path> --devtools # With DevTools
yarn debug:browser <path> --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`)

562
scripts/puppeteer/README.md Normal file
View File

@ -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 <url-path> [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 <url-path> [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 <url-path> [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)

261
scripts/puppeteer/SETUP.md Normal file
View File

@ -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

View File

@ -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();

View File

@ -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 <url-path>
*/
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);
});

View File

@ -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);
});

View File

@ -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 <url-path> [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 <url-path> [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();

View File

@ -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 <url-path> [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 <url-path> [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();

View File

@ -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<Browser>} 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<Page>} 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<void>}
*/
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<Object>} 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>} 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>} 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<boolean>} 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<ElementHandle>} 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<string>} 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<void>}
*/
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<any>} 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<string>} properties - CSS properties to get
* @returns {Promise<Object>} 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<Object>} viewports - Array of {width, height, name} objects
* @param {Function} testFn - Test function to run at each viewport
* @returns {Promise<Array>} 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<Object>} 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<void>}
*/
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`);
}