From 6b18332a7b13836b879960547ea25bd6573a84f2 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 21 Oct 2025 22:24:15 +0100 Subject: [PATCH 1/4] Fix for optional headers --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index fcdf15e..da9e565 100644 --- a/src/index.js +++ b/src/index.js @@ -36,7 +36,7 @@ const defaultContext = { const screenshotSchema = z.object({ url: z.string().url(), theme: z.enum(["light", "dark"]).default("light"), - headers: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.any()).optional(), sleep: z.number().min(0).max(60000).default(3000), }); router.post( @@ -59,7 +59,7 @@ router.post( return await route.continue({ headers: { ...request.headers(), - ...body.headers, + ...(body.headers || {}), }, }); } From 0f0d6256150c9a6fb362eaf5cbd38f7fbd992dc4 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 21 Oct 2025 22:40:07 +0100 Subject: [PATCH 2/4] Enhance README and API: Add extensive configuration options for screenshots and reports, including viewport, geolocation, permissions, and custom thresholds. Update usage examples and improve validation for timezone identifiers. --- README.md | 472 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/index.js | 219 ++++++++++++++++++++++-- 2 files changed, 668 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 98630fc..0e173af 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,493 @@ [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![appwrite.io](https://img.shields.io/badge/appwrite-.io-f02e65?style=flat-square)](https://appwrite.io) -Docker Browser is simple to use and extend REST API meant to simplify screenshot preview, reports, and analysis. +Docker Browser is a powerful REST API for taking screenshots, generating Lighthouse reports, and performing web analysis with extensive configuration options. -## Usage +## Features -Add Docker Browser to your `docker-compose.yml`. +- πŸ“Έ **Screenshots** with customizable viewport, format, and quality +- πŸ“Š **Lighthouse Reports** for performance, accessibility, SEO, and more +- 🌐 **Browser Context** configuration (user agent, locale, timezone, geolocation) +- 🎨 **Theme Support** (light/dark mode) +- ⚑ **Performance** optimized with Playwright and Chromium +- πŸ”§ **Highly Configurable** with comprehensive API options -``` +## Quick Start + +Add Docker Browser to your `docker-compose.yml`: + +```yaml services: appwrite-browser: image: appwrite/browser:0.1.0 + ports: + - "3000:3000" ``` -Start Docker Browser alongside rest of your services. +Start the service: -``` +```bash docker compose up -d ``` -Communicate with Docker Browser endpoints. +Take a simple screenshot: + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{"url":"https://example.com"}' +``` + +## API Endpoints + +### Health Check + +**GET** `/v1/health` + +Check if the browser service is running. + +```bash +curl http://localhost:3000/v1/health +``` + +**Response:** +```json +{ + "status": "ok" +} +``` + +### Screenshots + +**POST** `/v1/screenshots` + +Take screenshots of web pages with extensive configuration options. + +#### Basic Usage + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{"url":"https://example.com"}' +``` + +#### Advanced Configuration + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": { + "width": 1920, + "height": 1080 + }, + "format": "jpeg", + "quality": 95, + "fullPage": true, + "theme": "dark", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "locale": "en-US", + "timezoneId": "America/New_York", + "geolocation": { + "latitude": 40.7128, + "longitude": -74.0060, + "accuracy": 100 + }, + "headers": { + "Authorization": "Bearer your-token", + "X-Custom-Header": "value" + }, + "waitUntil": "networkidle", + "timeout": 60000, + "sleep": 5000, + "deviceScaleFactor": 2, + "hasTouch": true, + "isMobile": false + }' +``` + +#### Screenshot Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `url` | string | **required** | The URL to screenshot | +| `viewport` | object | `{width: 1280, height: 720}` | Viewport dimensions | +| `viewport.width` | number | 1280 | Viewport width (1-3840) | +| `viewport.height` | number | 720 | Viewport height (1-2160) | +| `format` | string | "png" | Image format: "png", "jpeg", "webp" | +| `quality` | number | 90 | Image quality 0-100 (for jpeg/webp) | +| `fullPage` | boolean | false | Capture full page scroll | +| `clip` | object | - | Crop area: `{x, y, width, height}` | +| `theme` | string | "light" | Browser theme: "light", "dark" | +| `userAgent` | string | - | Custom user agent string | +| `locale` | string | - | Browser locale (e.g., "en-US") | +| `timezoneId` | string | - | IANA timezone identifier (see [Timezone](#timezone) section) | +| `geolocation` | object | - | GPS coordinates | +| `geolocation.latitude` | number | - | Latitude (-90 to 90) | +| `geolocation.longitude` | number | - | Longitude (-180 to 180) | +| `geolocation.accuracy` | number | - | Accuracy in meters | +| `permissions` | array | - | Browser permissions (see [Permissions](#permissions) section) | +| `headers` | object | - | Custom HTTP headers | +| `waitUntil` | string | "domcontentloaded" | Wait condition: "load", "domcontentloaded", "networkidle", "commit" | +| `timeout` | number | 30000 | Navigation timeout (0-120000ms) | +| `sleep` | number | 3000 | Wait time after load (0-60000ms) | +| `deviceScaleFactor` | number | 1 | Device pixel ratio (0.1-3) | +| `hasTouch` | boolean | false | Touch support | +| `isMobile` | boolean | false | Mobile device emulation | + +### Lighthouse Reports + +**POST** `/v1/reports` + +Generate Lighthouse performance, accessibility, SEO, and PWA reports. + +#### Basic Usage + +```bash +curl -X POST http://localhost:3000/v1/reports \ + -H "Content-Type: application/json" \ + -d '{"url":"https://example.com"}' +``` + +#### Advanced Configuration ```bash -curl -X POST -H 'content-type: application/json' -d '{"url":"http://google.com/ping"}' http://appwrite-browser:3000/v1/screenshots +curl -X POST http://localhost:3000/v1/reports \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": "desktop", + "json": true, + "html": true, + "csv": false, + "theme": "dark", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "locale": "en-US", + "timezoneId": "America/New_York", + "headers": { + "Authorization": "Bearer your-token" + }, + "thresholds": { + "performance": 90, + "accessibility": 95, + "best-practices": 85, + "seo": 80, + "pwa": 70 + }, + "waitUntil": "networkidle", + "timeout": 60000 + }' +``` + +#### Report Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `url` | string | **required** | The URL to analyze | +| `viewport` | string | "mobile" | Device type: "mobile", "desktop" | +| `json` | boolean | true | Include JSON report | +| `html` | boolean | false | Include HTML report | +| `csv` | boolean | false | Include CSV report | +| `theme` | string | "light" | Browser theme: "light", "dark" | +| `userAgent` | string | - | Custom user agent string | +| `locale` | string | - | Browser locale | +| `timezoneId` | string | - | IANA timezone identifier (see [Timezone](#timezone) section) | +| `permissions` | array | - | Browser permissions (see [Permissions](#permissions) section) | +| `headers` | object | - | Custom HTTP headers | +| `thresholds` | object | - | Performance thresholds (0-100) | +| `thresholds.performance` | number | 0 | Performance score threshold | +| `thresholds.accessibility` | number | 0 | Accessibility score threshold | +| `thresholds.best-practices` | number | 0 | Best practices score threshold | +| `thresholds.seo` | number | 0 | SEO score threshold | +| `thresholds.pwa` | number | 0 | PWA score threshold | +| `waitUntil` | string | "domcontentloaded" | Wait condition | +| `timeout` | number | 30000 | Navigation timeout (0-120000ms) | + +## Timezone + +The `timezoneId` parameter allows you to set the browser's timezone for accurate time-based testing and content capture. This is particularly useful for testing applications that display time-sensitive content or have timezone-dependent features. + +### Format + +The `timezoneId` must be a valid **IANA timezone identifier** in the format `Region/City`. This is the same format used by JavaScript's `Intl.DateTimeFormat` and is the standard for timezone identification. + +### Valid Timezone Examples + +#### Americas +- `America/New_York` - Eastern Time (US) +- `America/Chicago` - Central Time (US) +- `America/Denver` - Mountain Time (US) +- `America/Los_Angeles` - Pacific Time (US) +- `America/Toronto` - Eastern Time (Canada) +- `America/Vancouver` - Pacific Time (Canada) +- `America/Sao_Paulo` - BrasΓ­lia Time (Brazil) +- `America/Mexico_City` - Central Time (Mexico) +- `America/Argentina/Buenos_Aires` - Argentina Time + +#### Europe +- `Europe/London` - Greenwich Mean Time / British Summer Time +- `Europe/Paris` - Central European Time +- `Europe/Berlin` - Central European Time +- `Europe/Rome` - Central European Time +- `Europe/Madrid` - Central European Time +- `Europe/Amsterdam` - Central European Time +- `Europe/Moscow` - Moscow Time +- `Europe/Istanbul` - Turkey Time + +#### Asia +- `Asia/Tokyo` - Japan Standard Time +- `Asia/Shanghai` - China Standard Time +- `Asia/Hong_Kong` - Hong Kong Time +- `Asia/Singapore` - Singapore Time +- `Asia/Seoul` - Korea Standard Time +- `Asia/Dubai` - Gulf Standard Time +- `Asia/Kolkata` - India Standard Time +- `Asia/Bangkok` - Indochina Time + +#### Australia & Pacific +- `Australia/Sydney` - Australian Eastern Time +- `Australia/Melbourne` - Australian Eastern Time +- `Australia/Perth` - Australian Western Time +- `Pacific/Auckland` - New Zealand Time +- `Pacific/Honolulu` - Hawaii-Aleutian Time +- `Pacific/Fiji` - Fiji Time + +#### Africa +- `Africa/Cairo` - Eastern European Time +- `Africa/Johannesburg` - South Africa Standard Time +- `Africa/Lagos` - West Africa Time +- `Africa/Nairobi` - East Africa Time + +#### Special Cases +- `UTC` - Coordinated Universal Time +- `GMT` - Greenwich Mean Time + +### Timezone Examples + +#### Basic Timezone Setting +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "timezoneId": "America/New_York" + }' +``` + +#### International Testing +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://ecommerce-site.com", + "timezoneId": "Asia/Tokyo", + "locale": "ja-JP" + }' +``` + +#### UTC for Consistent Testing +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://api-dashboard.com", + "timezoneId": "UTC" + }' +``` + +### Common Use Cases + +- **E-commerce**: Test pricing displays in different timezones +- **Scheduling Apps**: Verify appointment times across regions +- **News Sites**: Check time-sensitive content display +- **Financial Apps**: Test market hours and trading times +- **Social Media**: Verify post timestamps and feeds +- **Analytics**: Test time-based reporting accuracy + +### Validation + +The API validates timezone identifiers using the IANA timezone database format. Invalid timezone identifiers will result in a validation error with a helpful message. + +**Valid format**: `Region/City` (e.g., `America/New_York`) +**Invalid formats**: +- `EST`, `PST` (abbreviations not supported) +- `UTC+5` (offset format not supported) +- `New York` (missing region prefix) + +## Permissions + +The `permissions` parameter allows you to grant specific browser permissions to the page during screenshot capture or report generation. This is useful for testing features that require user permission or for capturing content that depends on specific browser capabilities. + +### Available Permissions + +| Permission | Description | Use Case | +|------------|-------------|----------| +| `geolocation` | Access to device location | Location-based features, maps | +| `camera` | Access to device camera | Video calls, photo capture | +| `microphone` | Access to device microphone | Audio recording, voice calls | +| `notifications` | Display notifications | Push notifications, alerts | +| `clipboard-read` | Read from clipboard | Copy/paste functionality | +| `clipboard-write` | Write to clipboard | Copy/paste functionality | +| `payment-handler` | Handle payment requests | E-commerce, payment flows | +| `midi` | Access to MIDI devices | Music applications | +| `usb` | Access to USB devices | Hardware integration | +| `serial` | Access to serial ports | Hardware communication | +| `bluetooth` | Access to Bluetooth devices | IoT, wireless devices | +| `persistent-storage` | Persistent storage access | Offline data storage | +| `accelerometer` | Access to accelerometer | Motion detection, games | +| `gyroscope` | Access to gyroscope | Orientation, VR/AR | +| `magnetometer` | Access to magnetometer | Compass, navigation | +| `ambient-light-sensor` | Access to light sensor | Auto-brightness, themes | +| `background-sync` | Background synchronization | Offline sync | +| `background-fetch` | Background data fetching | Offline content | +| `idle-detection` | Detect user idle state | Power management | +| `periodic-background-sync` | Periodic background sync | Scheduled updates | +| `push` | Push messaging | Real-time notifications | +| `speaker-selection` | Audio output selection | Audio routing | +| `storage-access` | Cross-site storage access | Third-party cookies | +| `top-level-storage-access` | Top-level storage access | First-party storage | +| `window-management` | Window management | Multi-window apps | +| `local-fonts` | Access to local fonts | Custom typography | +| `display-capture` | Screen capture | Screen sharing | +| `nfc` | Near Field Communication | Contactless payments | +| `screen-wake-lock` | Prevent screen sleep | Always-on displays | +| `web-share` | Web Share API | Native sharing | +| `xr-spatial-tracking` | XR spatial tracking | VR/AR applications | + +### Permission Examples + +#### Basic Permissions +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "permissions": ["geolocation", "notifications"] + }' +``` + +#### Camera and Microphone Access +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://video-call-app.com", + "permissions": ["camera", "microphone", "notifications"] + }' +``` + +#### Hardware Integration +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://iot-dashboard.com", + "permissions": ["usb", "bluetooth", "serial"] + }' +``` + +#### VR/AR Applications +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://vr-app.com", + "permissions": ["xr-spatial-tracking", "accelerometer", "gyroscope", "magnetometer"] + }' +``` + +#### Payment and E-commerce +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://ecommerce-site.com", + "permissions": ["payment-handler", "clipboard-write", "nfc"] + }' +``` + +## Examples + +### Mobile Screenshot + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": {"width": 375, "height": 667}, + "isMobile": true, + "hasTouch": true, + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)" + }' +``` + +### High-Quality Screenshot + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "format": "jpeg", + "quality": 100, + "deviceScaleFactor": 2, + "fullPage": true + }' +``` + +### Geolocation Testing + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "geolocation": { + "latitude": 37.7749, + "longitude": -122.4194, + "accuracy": 100 + }, + "timezoneId": "America/Los_Angeles" + }' +``` + +### Performance Report with Custom Thresholds + +```bash +curl -X POST http://localhost:3000/v1/reports \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": "desktop", + "thresholds": { + "performance": 90, + "accessibility": 95, + "best-practices": 85, + "seo": 80 + }, + "html": true + }' ``` ## Development Make sure you have [pnpm](https://pnpm.io/) installed. -To install dependencies, run the following command. +Install dependencies: ```bash pnpm i ``` -Next, start the server by running `npm start`, and visit use endpoint `http://localhost:3000` as REST API endpoint. +Start the development server: + +```bash +npm start +``` + +The API will be available at `http://localhost:3000`. ## Contributing diff --git a/src/index.js b/src/index.js index da9e565..cf656c9 100644 --- a/src/index.js +++ b/src/index.js @@ -38,15 +38,101 @@ const screenshotSchema = z.object({ theme: z.enum(["light", "dark"]).default("light"), headers: z.record(z.string(), z.any()).optional(), sleep: z.number().min(0).max(60000).default(3000), + // Viewport options + viewport: z.object({ + width: z.number().min(1).max(3840).default(1280), + height: z.number().min(1).max(2160).default(720), + }).optional(), + // Screenshot options + format: z.enum(["png", "jpeg", "webp"]).default("png"), + quality: z.number().min(0).max(100).default(90), + fullPage: z.boolean().default(false), + clip: z.object({ + x: z.number().min(0), + y: z.number().min(0), + width: z.number().min(1), + height: z.number().min(1), + }).optional(), + // Browser context options + userAgent: z.string().optional(), + locale: z.string().optional(), + timezoneId: z.string().regex( + /^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific|UTC)\/[A-Za-z_]+$/, + "Must be a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')" + ).optional(), + geolocation: z.object({ + latitude: z.number().min(-90).max(90), + longitude: z.number().min(-180).max(180), + accuracy: z.number().min(0).optional(), + }).optional(), + permissions: z.array(z.enum([ + "geolocation", + "camera", + "microphone", + "notifications", + "clipboard-read", + "clipboard-write", + "payment-handler", + "midi", + "usb", + "serial", + "bluetooth", + "persistent-storage", + "accelerometer", + "gyroscope", + "magnetometer", + "ambient-light-sensor", + "background-sync", + "background-fetch", + "idle-detection", + "periodic-background-sync", + "push", + "speaker-selection", + "storage-access", + "top-level-storage-access", + "window-management", + "local-fonts", + "display-capture", + "nfc", + "screen-wake-lock", + "web-share", + "xr-spatial-tracking" + ])).optional(), + // Navigation options + waitUntil: z.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("domcontentloaded"), + timeout: z.number().min(0).max(120000).default(30000), + // Additional options + deviceScaleFactor: z.number().min(0.1).max(3).default(1), + hasTouch: z.boolean().default(false), + isMobile: z.boolean().default(false), }); router.post( "/v1/screenshots", defineEventHandler(async (event) => { const body = await readValidatedBody(event, screenshotSchema.parse); - const context = await browser.newContext({ + + // Build context options + const contextOptions = { ...defaultContext, colorScheme: body.theme, - }); + viewport: body.viewport || defaultContext.viewport, + deviceScaleFactor: body.deviceScaleFactor, + hasTouch: body.hasTouch, + isMobile: body.isMobile, + }; + + // Add optional context options + if (body.userAgent) contextOptions.userAgent = body.userAgent; + if (body.locale) contextOptions.locale = body.locale; + if (body.timezoneId) contextOptions.timezoneId = body.timezoneId; + if (body.geolocation) contextOptions.geolocation = body.geolocation; + + const context = await browser.newContext(contextOptions); + + // Grant permissions if specified + if (body.permissions && body.permissions.length > 0) { + await context.grantPermissions(body.permissions, { origin: body.url }); + } // await context.tracing.start({ screenshots: true, snapshots: true }); @@ -68,14 +154,26 @@ router.post( }); await page.goto(body.url, { - waitUntil: "domcontentloaded", + waitUntil: body.waitUntil, + timeout: body.timeout, }); if (body.sleep > 0) { await page.waitForTimeout(body.sleep); // Safe addition for any extra JS } - const screen = await page.screenshot(); + // Build screenshot options + const screenshotOptions = { + type: body.format, + quality: body.quality, + fullPage: body.fullPage, + }; + + if (body.clip) { + screenshotOptions.clip = body.clip; + } + + const screen = await page.screenshot(screenshotOptions); // await context.tracing.stop({ path: '/tmp/trace' + Date.now() + '.zip' }); @@ -90,6 +188,59 @@ const lighthouseSchema = z.object({ json: z.boolean().default(true), html: z.boolean().default(false), csv: z.boolean().default(false), + // Additional lighthouse options + theme: z.enum(["light", "dark"]).default("light"), + headers: z.record(z.string(), z.any()).optional(), + userAgent: z.string().optional(), + locale: z.string().optional(), + timezoneId: z.string().regex( + /^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific|UTC)\/[A-Za-z_]+$/, + "Must be a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')" + ).optional(), + permissions: z.array(z.enum([ + "geolocation", + "camera", + "microphone", + "notifications", + "clipboard-read", + "clipboard-write", + "payment-handler", + "midi", + "usb", + "serial", + "bluetooth", + "persistent-storage", + "accelerometer", + "gyroscope", + "magnetometer", + "ambient-light-sensor", + "background-sync", + "background-fetch", + "idle-detection", + "periodic-background-sync", + "push", + "speaker-selection", + "storage-access", + "top-level-storage-access", + "window-management", + "local-fonts", + "display-capture", + "nfc", + "screen-wake-lock", + "web-share", + "xr-spatial-tracking" + ])).optional(), + // Performance thresholds + thresholds: z.object({ + performance: z.number().min(0).max(100).default(0), + accessibility: z.number().min(0).max(100).default(0), + "best-practices": z.number().min(0).max(100).default(0), + seo: z.number().min(0).max(100).default(0), + pwa: z.number().min(0).max(100).default(0), + }).optional(), + // Navigation options + waitUntil: z.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("domcontentloaded"), + timeout: z.number().min(0).max(120000).default(30000), }); const configs = { mobile: lighthouseMobileConfig, @@ -99,9 +250,57 @@ router.post( "/v1/reports", defineEventHandler(async (event) => { const body = await readValidatedBody(event, lighthouseSchema.parse); - const context = await browser.newContext(defaultContext); + + // Build context options + const contextOptions = { + ...defaultContext, + colorScheme: body.theme, + }; + + // Add optional context options + if (body.userAgent) contextOptions.userAgent = body.userAgent; + if (body.locale) contextOptions.locale = body.locale; + if (body.timezoneId) contextOptions.timezoneId = body.timezoneId; + + const context = await browser.newContext(contextOptions); + + // Grant permissions if specified + if (body.permissions && body.permissions.length > 0) { + await context.grantPermissions(body.permissions, { origin: body.url }); + } + const page = await context.newPage(); - await page.goto(body.url); + + // Override headers if provided + if (body.headers) { + await page.route("**/*", async (route, request) => { + const url = request.url(); + if (url.startsWith("http://appwrite/")) { + return await route.continue({ + headers: { + ...request.headers(), + ...body.headers, + }, + }); + } + return await route.continue({ headers: request.headers() }); + }); + } + + await page.goto(body.url, { + waitUntil: body.waitUntil, + timeout: body.timeout, + }); + + // Use custom thresholds if provided, otherwise use defaults + const thresholds = body.thresholds || { + "best-practices": 0, + accessibility: 0, + performance: 0, + pwa: 0, + seo: 0, + }; + const results = await playAudit({ reports: { formats: { @@ -113,13 +312,7 @@ router.post( config: configs[body.viewport], page: page, port: 9222, - thresholds: { - "best-practices": 0, - accessibility: 0, - performance: 0, - pwa: 0, - seo: 0, - }, + thresholds, }); await context.close(); return JSON.parse(results.report); From 3030854e187096f9fcafef4db51aa0813cb2f71a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 21 Oct 2025 23:17:44 +0100 Subject: [PATCH 3/4] Add browser configuration test endpoint and update README - Introduced a new GET endpoint `/v1/test` to display current browser configuration values in an HTML format. - Enhanced the README with detailed usage instructions, response format, and various test command examples for different scenarios, including mobile and high-quality desktop tests. --- README.md | 218 +++++++++++++++++++++++++++++++ src/index.js | 362 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 580 insertions(+) diff --git a/README.md b/README.md index 0e173af..0a8ac0c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,42 @@ curl http://localhost:3000/v1/health } ``` +### Test Configuration + +**GET** `/v1/test` + +Display current browser configuration values. This endpoint returns an HTML page showing what the browser currently has set, using inline JavaScript to detect and display all browser capabilities and settings. Perfect for taking screenshots to verify browser state. + +#### Usage + +```bash +curl http://localhost:3000/v1/test +``` + +#### Response Format + +The test endpoint returns an HTML page with a comprehensive visual display of current browser configuration values. The page includes: + +- **Viewport & Display**: Screen dimensions, device pixel ratio +- **Theme & Appearance**: Color scheme detection +- **Localization**: Language, timezone settings +- **Device & Hardware**: Touch support, mobile detection +- **User Agent**: Current user agent string +- **Geolocation**: GPS coordinates and accuracy (if available) +- **Permissions**: Browser permission states +- **Page Information**: URL, title, ready state + +The HTML page automatically adapts to the browser's theme preference (light/dark) and uses inline JavaScript to display real-time browser values. + +#### Use Cases + +- **Browser State Inspection**: See what the browser currently has configured +- **Visual Verification**: Take screenshots to verify browser capabilities +- **Debugging**: Visual inspection of browser configuration issues +- **Documentation**: Generate visual examples of browser behavior +- **Screenshot Testing**: Perfect for testing the screenshot API itself +- **Development**: Quick way to check browser state during development + ### Screenshots **POST** `/v1/screenshots` @@ -299,6 +335,188 @@ curl -X POST http://localhost:3000/v1/screenshots \ }' ``` +#### Comprehensive Test Command + +Here's a complete test command that showcases all available parameters: + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": { + "width": 1920, + "height": 1080 + }, + "format": "jpeg", + "quality": 95, + "fullPage": true, + "theme": "dark", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "locale": "en-US", + "timezoneId": "America/New_York", + "geolocation": { + "latitude": 40.7128, + "longitude": -74.0060, + "accuracy": 100 + }, + "permissions": [ + "geolocation", + "notifications", + "camera", + "microphone", + "clipboard-read", + "clipboard-write" + ], + "headers": { + "Authorization": "Bearer test-token-12345", + "X-Custom-Header": "test-value", + "X-Request-ID": "screenshot-test-001" + }, + "waitUntil": "networkidle", + "timeout": 60000, + "sleep": 5000, + "deviceScaleFactor": 2, + "hasTouch": true, + "isMobile": false + }' \ + --output "comprehensive-test-screenshot.jpg" +``` + +#### Mobile Device Test Command + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": { + "width": 375, + "height": 667 + }, + "format": "png", + "quality": 90, + "fullPage": true, + "theme": "light", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", + "locale": "en-US", + "timezoneId": "America/Los_Angeles", + "geolocation": { + "latitude": 37.7749, + "longitude": -122.4194, + "accuracy": 50 + }, + "permissions": [ + "geolocation", + "camera", + "microphone", + "notifications" + ], + "headers": { + "X-Mobile-App": "true", + "X-Device-Type": "mobile" + }, + "waitUntil": "domcontentloaded", + "timeout": 30000, + "sleep": 3000, + "deviceScaleFactor": 3, + "hasTouch": true, + "isMobile": true + }' \ + --output "mobile-test-screenshot.png" +``` + +#### High-Quality Desktop Test Command + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": { + "width": 2560, + "height": 1440 + }, + "format": "jpeg", + "quality": 100, + "fullPage": true, + "theme": "dark", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "locale": "en-US", + "timezoneId": "Europe/London", + "geolocation": { + "latitude": 51.5074, + "longitude": -0.1278, + "accuracy": 20 + }, + "permissions": [ + "geolocation", + "notifications", + "camera", + "microphone", + "clipboard-read", + "clipboard-write", + "payment-handler", + "usb", + "bluetooth" + ], + "headers": { + "Authorization": "Bearer premium-token", + "X-Premium-User": "true", + "X-Request-Source": "screenshot-api" + }, + "waitUntil": "networkidle", + "timeout": 90000, + "sleep": 8000, + "deviceScaleFactor": 2, + "hasTouch": false, + "isMobile": false + }' \ + --output "high-quality-desktop-screenshot.jpg" +``` + +#### Clipped Screenshot Test Command + +```bash +curl -X POST http://localhost:3000/v1/screenshots \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "viewport": { + "width": 1920, + "height": 1080 + }, + "format": "png", + "quality": 90, + "fullPage": false, + "clip": { + "x": 100, + "y": 100, + "width": 800, + "height": 600 + }, + "theme": "light", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", + "locale": "en-GB", + "timezoneId": "Europe/Berlin", + "permissions": [ + "geolocation", + "notifications" + ], + "headers": { + "X-Test-Mode": "clipped-screenshot", + "X-Region": "europe" + }, + "waitUntil": "load", + "timeout": 45000, + "sleep": 2000, + "deviceScaleFactor": 1.5, + "hasTouch": false, + "isMobile": false + }' \ + --output "clipped-screenshot.png" +``` + ### Common Use Cases - **E-commerce**: Test pricing displays in different timezones diff --git a/src/index.js b/src/index.js index cf656c9..1eab1f9 100644 --- a/src/index.js +++ b/src/index.js @@ -328,6 +328,368 @@ router.get( }), ); +router.get( + "/v1/test", + defineEventHandler(async (event) => { + // Create a simple context with default settings + const context = await browser.newContext(defaultContext); + const page = await context.newPage(); + + // Navigate to a simple page to get browser info + await page.goto("about:blank"); + + // Generate HTML page with browser configuration info + const html = await page.evaluate(() => { + const timestamp = new Date().toISOString(); + + return ` + + + + + + Browser Configuration Test + + + + +
+

🌐 Browser Configuration Test

+
Generated: ${timestamp}
+ +
+
+

πŸ“± Viewport & Display

+
+ Viewport Width: + Loading... +
+
+ Viewport Height: + Loading... +
+
+ Screen Width: + Loading... +
+
+ Screen Height: + Loading... +
+
+ Device Pixel Ratio: + Loading... +
+
+ +
+

🎨 Theme & Appearance

+
+ Color Scheme: + Loading... +
+
+ +
+

🌍 Localization

+
+ Language: + Loading... +
+
+ Timezone: + Loading... +
+
+ +
+

πŸ”§ Device & Hardware

+
+ Touch Support: + Loading... +
+
+ Mobile Device: + Loading... +
+
+ +
+

🌐 User Agent

+
+ User Agent: + Loading... +
+
+ +
+

πŸ“ Geolocation

+
+ Location: + Loading... +
+
+ +
+

πŸ” Permissions

+
+ Permission States: + Loading... +
+
+ +
+

πŸ“„ Page Information

+
+ URL: + Loading... +
+
+ Title: + Loading... +
+
+ Ready State: + Loading... +
+
+
+
+ + + +`; + }); + + await context.close(); + + // Set content type to HTML + event.node.res.setHeader('Content-Type', 'text/html'); + return html; + }), +); + createServer(toNodeListener(app)).listen(port); console.log(`Server running on port http://0.0.0.0:${port}`); From a529f768f4a1178a6837ea318e25a1106132ed55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 22 Oct 2025 11:49:09 +0200 Subject: [PATCH 4/4] Formatting fix --- src/index.js | 230 ++++++++++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 102 deletions(-) diff --git a/src/index.js b/src/index.js index 1eab1f9..eb43180 100644 --- a/src/index.js +++ b/src/index.js @@ -39,67 +39,82 @@ const screenshotSchema = z.object({ headers: z.record(z.string(), z.any()).optional(), sleep: z.number().min(0).max(60000).default(3000), // Viewport options - viewport: z.object({ - width: z.number().min(1).max(3840).default(1280), - height: z.number().min(1).max(2160).default(720), - }).optional(), + viewport: z + .object({ + width: z.number().min(1).max(3840).default(1280), + height: z.number().min(1).max(2160).default(720), + }) + .optional(), // Screenshot options format: z.enum(["png", "jpeg", "webp"]).default("png"), quality: z.number().min(0).max(100).default(90), fullPage: z.boolean().default(false), - clip: z.object({ - x: z.number().min(0), - y: z.number().min(0), - width: z.number().min(1), - height: z.number().min(1), - }).optional(), + clip: z + .object({ + x: z.number().min(0), + y: z.number().min(0), + width: z.number().min(1), + height: z.number().min(1), + }) + .optional(), // Browser context options userAgent: z.string().optional(), locale: z.string().optional(), - timezoneId: z.string().regex( - /^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific|UTC)\/[A-Za-z_]+$/, - "Must be a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')" - ).optional(), - geolocation: z.object({ - latitude: z.number().min(-90).max(90), - longitude: z.number().min(-180).max(180), - accuracy: z.number().min(0).optional(), - }).optional(), - permissions: z.array(z.enum([ - "geolocation", - "camera", - "microphone", - "notifications", - "clipboard-read", - "clipboard-write", - "payment-handler", - "midi", - "usb", - "serial", - "bluetooth", - "persistent-storage", - "accelerometer", - "gyroscope", - "magnetometer", - "ambient-light-sensor", - "background-sync", - "background-fetch", - "idle-detection", - "periodic-background-sync", - "push", - "speaker-selection", - "storage-access", - "top-level-storage-access", - "window-management", - "local-fonts", - "display-capture", - "nfc", - "screen-wake-lock", - "web-share", - "xr-spatial-tracking" - ])).optional(), + timezoneId: z + .string() + .regex( + /^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific|UTC)\/[A-Za-z_]+$/, + "Must be a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')", + ) + .optional(), + geolocation: z + .object({ + latitude: z.number().min(-90).max(90), + longitude: z.number().min(-180).max(180), + accuracy: z.number().min(0).optional(), + }) + .optional(), + permissions: z + .array( + z.enum([ + "geolocation", + "camera", + "microphone", + "notifications", + "clipboard-read", + "clipboard-write", + "payment-handler", + "midi", + "usb", + "serial", + "bluetooth", + "persistent-storage", + "accelerometer", + "gyroscope", + "magnetometer", + "ambient-light-sensor", + "background-sync", + "background-fetch", + "idle-detection", + "periodic-background-sync", + "push", + "speaker-selection", + "storage-access", + "top-level-storage-access", + "window-management", + "local-fonts", + "display-capture", + "nfc", + "screen-wake-lock", + "web-share", + "xr-spatial-tracking", + ]), + ) + .optional(), // Navigation options - waitUntil: z.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("domcontentloaded"), + waitUntil: z + .enum(["load", "domcontentloaded", "networkidle", "commit"]) + .default("domcontentloaded"), timeout: z.number().min(0).max(120000).default(30000), // Additional options deviceScaleFactor: z.number().min(0.1).max(3).default(1), @@ -110,7 +125,7 @@ router.post( "/v1/screenshots", defineEventHandler(async (event) => { const body = await readValidatedBody(event, screenshotSchema.parse); - + // Build context options const contextOptions = { ...defaultContext, @@ -193,53 +208,64 @@ const lighthouseSchema = z.object({ headers: z.record(z.string(), z.any()).optional(), userAgent: z.string().optional(), locale: z.string().optional(), - timezoneId: z.string().regex( - /^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific|UTC)\/[A-Za-z_]+$/, - "Must be a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')" - ).optional(), - permissions: z.array(z.enum([ - "geolocation", - "camera", - "microphone", - "notifications", - "clipboard-read", - "clipboard-write", - "payment-handler", - "midi", - "usb", - "serial", - "bluetooth", - "persistent-storage", - "accelerometer", - "gyroscope", - "magnetometer", - "ambient-light-sensor", - "background-sync", - "background-fetch", - "idle-detection", - "periodic-background-sync", - "push", - "speaker-selection", - "storage-access", - "top-level-storage-access", - "window-management", - "local-fonts", - "display-capture", - "nfc", - "screen-wake-lock", - "web-share", - "xr-spatial-tracking" - ])).optional(), + timezoneId: z + .string() + .regex( + /^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific|UTC)\/[A-Za-z_]+$/, + "Must be a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')", + ) + .optional(), + permissions: z + .array( + z.enum([ + "geolocation", + "camera", + "microphone", + "notifications", + "clipboard-read", + "clipboard-write", + "payment-handler", + "midi", + "usb", + "serial", + "bluetooth", + "persistent-storage", + "accelerometer", + "gyroscope", + "magnetometer", + "ambient-light-sensor", + "background-sync", + "background-fetch", + "idle-detection", + "periodic-background-sync", + "push", + "speaker-selection", + "storage-access", + "top-level-storage-access", + "window-management", + "local-fonts", + "display-capture", + "nfc", + "screen-wake-lock", + "web-share", + "xr-spatial-tracking", + ]), + ) + .optional(), // Performance thresholds - thresholds: z.object({ - performance: z.number().min(0).max(100).default(0), - accessibility: z.number().min(0).max(100).default(0), - "best-practices": z.number().min(0).max(100).default(0), - seo: z.number().min(0).max(100).default(0), - pwa: z.number().min(0).max(100).default(0), - }).optional(), + thresholds: z + .object({ + performance: z.number().min(0).max(100).default(0), + accessibility: z.number().min(0).max(100).default(0), + "best-practices": z.number().min(0).max(100).default(0), + seo: z.number().min(0).max(100).default(0), + pwa: z.number().min(0).max(100).default(0), + }) + .optional(), // Navigation options - waitUntil: z.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("domcontentloaded"), + waitUntil: z + .enum(["load", "domcontentloaded", "networkidle", "commit"]) + .default("domcontentloaded"), timeout: z.number().min(0).max(120000).default(30000), }); const configs = { @@ -250,7 +276,7 @@ router.post( "/v1/reports", defineEventHandler(async (event) => { const body = await readValidatedBody(event, lighthouseSchema.parse); - + // Build context options const contextOptions = { ...defaultContext, @@ -683,9 +709,9 @@ router.get( }); await context.close(); - + // Set content type to HTML - event.node.res.setHeader('Content-Type', 'text/html'); + event.node.res.setHeader("Content-Type", "text/html"); return html; }), );