feat: add comprehensive device emulation with geolocation, locale, timezone, permissions, and colorScheme

- Added browser_list_devices tool to show 143+ available device profiles organized by category (iPhone, iPad, Pixel, Galaxy, Desktop, Other)
- Enhanced browser_configure tool with device emulation using Playwright's device descriptors database
- Added support for geolocation coordinates with accuracy settings
- Implemented locale and timezone configuration for internationalization testing
- Added colorScheme preference (light/dark/no-preference) for accessibility testing
- Included permissions management for various browser APIs (geolocation, notifications, camera, microphone)
- Device emulation properly overrides individual viewport/userAgent settings when specified
- All context options are properly applied and browser context is recreated with new settings

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ryan Malloy 2025-08-11 06:06:43 -06:00
parent 4d13e72213
commit b2462593bc
2 changed files with 151 additions and 5 deletions

View File

@ -16,6 +16,7 @@
import debug from 'debug'; import debug from 'debug';
import * as playwright from 'playwright'; import * as playwright from 'playwright';
import { devices } from 'playwright';
import { logUnhandledError } from './log.js'; import { logUnhandledError } from './log.js';
import { Tab } from './tab.js'; import { Tab } from './tab.js';
@ -341,6 +342,12 @@ export class Context {
headless?: boolean; headless?: boolean;
viewport?: { width: number; height: number }; viewport?: { width: number; height: number };
userAgent?: string; userAgent?: string;
device?: string;
geolocation?: { latitude: number; longitude: number; accuracy?: number };
locale?: string;
timezone?: string;
colorScheme?: 'light' | 'dark' | 'no-preference';
permissions?: string[];
}): Promise<void> { }): Promise<void> {
const currentConfig = { ...this.config }; const currentConfig = { ...this.config };
@ -348,12 +355,53 @@ export class Context {
if (changes.headless !== undefined) { if (changes.headless !== undefined) {
currentConfig.browser.launchOptions.headless = changes.headless; currentConfig.browser.launchOptions.headless = changes.headless;
} }
// Handle device emulation - this overrides individual viewport/userAgent settings
if (changes.device) {
if (!devices[changes.device]) {
throw new Error(`Unknown device: ${changes.device}`);
}
const deviceConfig = devices[changes.device];
// Apply all device properties to context options
currentConfig.browser.contextOptions = {
...currentConfig.browser.contextOptions,
...deviceConfig,
};
} else {
// Apply individual settings only if no device is specified
if (changes.viewport) { if (changes.viewport) {
currentConfig.browser.contextOptions.viewport = changes.viewport; currentConfig.browser.contextOptions.viewport = changes.viewport;
} }
if (changes.userAgent) { if (changes.userAgent) {
currentConfig.browser.contextOptions.userAgent = changes.userAgent; currentConfig.browser.contextOptions.userAgent = changes.userAgent;
} }
}
// Apply additional context options
if (changes.geolocation) {
currentConfig.browser.contextOptions.geolocation = {
latitude: changes.geolocation.latitude,
longitude: changes.geolocation.longitude,
accuracy: changes.geolocation.accuracy || 100
};
}
if (changes.locale) {
currentConfig.browser.contextOptions.locale = changes.locale;
}
if (changes.timezone) {
currentConfig.browser.contextOptions.timezoneId = changes.timezone;
}
if (changes.colorScheme) {
currentConfig.browser.contextOptions.colorScheme = changes.colorScheme;
}
if (changes.permissions) {
currentConfig.browser.contextOptions.permissions = changes.permissions;
}
// Store the modified config // Store the modified config
(this as any).config = currentConfig; (this as any).config = currentConfig;

View File

@ -15,6 +15,7 @@
*/ */
import { z } from 'zod'; import { z } from 'zod';
import { devices } from 'playwright';
import { defineTool } from './tool.js'; import { defineTool } from './tool.js';
import type { Context } from '../context.js'; import type { Context } from '../context.js';
import type { Response } from '../response.js'; import type { Response } from '../response.js';
@ -26,15 +27,79 @@ const configureSchema = z.object({
height: z.number(), height: z.number(),
}).optional().describe('Browser viewport size'), }).optional().describe('Browser viewport size'),
userAgent: z.string().optional().describe('User agent string for the browser'), userAgent: z.string().optional().describe('User agent string for the browser'),
device: z.string().optional().describe('Device to emulate (e.g., "iPhone 13", "iPad", "Pixel 5"). Use browser_list_devices to see available devices.'),
geolocation: z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
accuracy: z.number().min(0).optional().describe('Accuracy in meters (default: 100)')
}).optional().describe('Set geolocation coordinates'),
locale: z.string().optional().describe('Browser locale (e.g., "en-US", "fr-FR", "ja-JP")'),
timezone: z.string().optional().describe('Timezone ID (e.g., "America/New_York", "Europe/London", "Asia/Tokyo")'),
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional().describe('Preferred color scheme'),
permissions: z.array(z.string()).optional().describe('Permissions to grant (e.g., ["geolocation", "notifications", "camera", "microphone"])')
}); });
const listDevicesSchema = z.object({});
export default [ export default [
defineTool({
capability: 'core',
schema: {
name: 'browser_list_devices',
title: 'List available devices for emulation',
description: 'Get a list of all available device emulation profiles including mobile phones, tablets, and desktop browsers. Each device includes viewport, user agent, and capabilities information.',
inputSchema: listDevicesSchema,
type: 'readOnly',
},
handle: async (context: Context, params: z.output<typeof listDevicesSchema>, response: Response) => {
try {
const deviceList = Object.keys(devices).sort();
const deviceCount = deviceList.length;
// Organize devices by category for better presentation
const categories = {
'iPhone': deviceList.filter(d => d.includes('iPhone')),
'iPad': deviceList.filter(d => d.includes('iPad')),
'Pixel': deviceList.filter(d => d.includes('Pixel')),
'Galaxy': deviceList.filter(d => d.includes('Galaxy')),
'Desktop': deviceList.filter(d => d.includes('Desktop')),
'Other': deviceList.filter(d =>
!d.includes('iPhone') &&
!d.includes('iPad') &&
!d.includes('Pixel') &&
!d.includes('Galaxy') &&
!d.includes('Desktop')
)
};
let result = `Available devices for emulation (${deviceCount} total):\n\n`;
for (const [category, deviceNames] of Object.entries(categories)) {
if (deviceNames.length > 0) {
result += `**${category}:**\n`;
deviceNames.forEach(device => {
const deviceInfo = devices[device];
result += `${device} - ${deviceInfo.viewport.width}x${deviceInfo.viewport.height}${deviceInfo.isMobile ? ' (mobile)' : ''}${deviceInfo.hasTouch ? ' (touch)' : ''}\n`;
});
result += '\n';
}
}
result += 'Use browser_configure with the "device" parameter to emulate any of these devices.';
response.addResult(result);
} catch (error) {
throw new Error(`Failed to list devices: ${error}`);
}
},
}),
defineTool({ defineTool({
capability: 'core', capability: 'core',
schema: { schema: {
name: 'browser_configure', name: 'browser_configure',
title: 'Configure browser settings', title: 'Configure browser settings',
description: 'Change browser configuration settings like headless/headed mode, viewport size, or user agent for subsequent operations. This will close the current browser and restart it with new settings.', description: 'Change browser configuration settings like headless/headed mode, viewport size, user agent, device emulation, geolocation, locale, timezone, color scheme, or permissions for subsequent operations. This will close the current browser and restart it with new settings.',
inputSchema: configureSchema, inputSchema: configureSchema,
type: 'destructive', type: 'destructive',
}, },
@ -65,6 +130,33 @@ export default [
} }
} }
if (params.device) {
if (!devices[params.device]) {
throw new Error(`Unknown device: ${params.device}. Use browser_list_devices to see available devices.`);
}
changes.push(`device: emulating ${params.device}`);
}
if (params.geolocation) {
changes.push(`geolocation: ${params.geolocation.latitude}, ${params.geolocation.longitude}${params.geolocation.accuracy || 100}m)`);
}
if (params.locale) {
changes.push(`locale: ${params.locale}`);
}
if (params.timezone) {
changes.push(`timezone: ${params.timezone}`);
}
if (params.colorScheme) {
changes.push(`colorScheme: ${params.colorScheme}`);
}
if (params.permissions && params.permissions.length > 0) {
changes.push(`permissions: ${params.permissions.join(', ')}`);
}
if (changes.length === 0) { if (changes.length === 0) {
response.addResult('No configuration changes detected. Current settings remain the same.'); response.addResult('No configuration changes detected. Current settings remain the same.');
return; return;
@ -75,6 +167,12 @@ export default [
headless: params.headless, headless: params.headless,
viewport: params.viewport, viewport: params.viewport,
userAgent: params.userAgent, userAgent: params.userAgent,
device: params.device,
geolocation: params.geolocation,
locale: params.locale,
timezone: params.timezone,
colorScheme: params.colorScheme,
permissions: params.permissions,
}); });
response.addResult(`Browser configuration updated successfully:\n${changes.map(c => `${c}`).join('\n')}\n\nThe browser has been restarted with the new settings.`); response.addResult(`Browser configuration updated successfully:\n${changes.map(c => `${c}`).join('\n')}\n\nThe browser has been restarted with the new settings.`);