@everydaydevopsio/pilot 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-agent-bridge.yaml +10 -0
- package/.dockerignore +9 -0
- package/CLAUDE.md +61 -0
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/browser.d.ts +100 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +654 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli/init.d.ts +4 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +56 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/skill-template.d.ts +2 -0
- package/dist/cli/skill-template.d.ts.map +1 -0
- package/dist/cli/skill-template.js +114 -0
- package/dist/cli/skill-template.js.map +1 -0
- package/dist/commands/click.d.ts +21 -0
- package/dist/commands/click.d.ts.map +1 -0
- package/dist/commands/click.js +64 -0
- package/dist/commands/click.js.map +1 -0
- package/dist/commands/evaluate.d.ts +14 -0
- package/dist/commands/evaluate.d.ts.map +1 -0
- package/dist/commands/evaluate.js +28 -0
- package/dist/commands/evaluate.js.map +1 -0
- package/dist/commands/navigate.d.ts +19 -0
- package/dist/commands/navigate.d.ts.map +1 -0
- package/dist/commands/navigate.js +78 -0
- package/dist/commands/navigate.js.map +1 -0
- package/dist/commands/page_info.d.ts +8 -0
- package/dist/commands/page_info.d.ts.map +1 -0
- package/dist/commands/page_info.js +13 -0
- package/dist/commands/page_info.js.map +1 -0
- package/dist/commands/screenshot.d.ts +18 -0
- package/dist/commands/screenshot.d.ts.map +1 -0
- package/dist/commands/screenshot.js +37 -0
- package/dist/commands/screenshot.js.map +1 -0
- package/dist/commands/tabs.d.ts +10 -0
- package/dist/commands/tabs.d.ts.map +1 -0
- package/dist/commands/tabs.js +15 -0
- package/dist/commands/tabs.js.map +1 -0
- package/dist/commands/type.d.ts +14 -0
- package/dist/commands/type.d.ts.map +1 -0
- package/dist/commands/type.js +61 -0
- package/dist/commands/type.js.map +1 -0
- package/dist/commands/wait.d.ts +17 -0
- package/dist/commands/wait.d.ts.map +1 -0
- package/dist/commands/wait.js +61 -0
- package/dist/commands/wait.js.map +1 -0
- package/dist/mcp/console-buffer.d.ts +28 -0
- package/dist/mcp/console-buffer.d.ts.map +1 -0
- package/dist/mcp/console-buffer.js +68 -0
- package/dist/mcp/console-buffer.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +74 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +18 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +46 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/browser.d.ts +5 -0
- package/dist/mcp/tools/browser.d.ts.map +1 -0
- package/dist/mcp/tools/browser.js +435 -0
- package/dist/mcp/tools/browser.js.map +1 -0
- package/dist/mcp/tools/errors.d.ts +4 -0
- package/dist/mcp/tools/errors.d.ts.map +1 -0
- package/dist/mcp/tools/errors.js +117 -0
- package/dist/mcp/tools/errors.js.map +1 -0
- package/dist/mcp/tools/names.d.ts +7 -0
- package/dist/mcp/tools/names.d.ts.map +1 -0
- package/dist/mcp/tools/names.js +24 -0
- package/dist/mcp/tools/names.js.map +1 -0
- package/dist/util/config.d.ts +34 -0
- package/dist/util/config.d.ts.map +1 -0
- package/dist/util/config.js +53 -0
- package/dist/util/config.js.map +1 -0
- package/dist/util/logger.d.ts +4 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +16 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/viewport.d.ts +105 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +145 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +74 -0
- package/smoke/mcp-smoke.ts +161 -0
- package/src/browser.ts +798 -0
- package/src/cli/init.ts +62 -0
- package/src/cli/skill-template.ts +113 -0
- package/src/commands/click.ts +93 -0
- package/src/commands/evaluate.ts +45 -0
- package/src/commands/navigate.ts +105 -0
- package/src/commands/page_info.ts +21 -0
- package/src/commands/screenshot.ts +72 -0
- package/src/commands/tabs.ts +35 -0
- package/src/commands/type.ts +77 -0
- package/src/commands/wait.ts +90 -0
- package/src/mcp/console-buffer.ts +94 -0
- package/src/mcp/index.ts +107 -0
- package/src/mcp/server.ts +67 -0
- package/src/mcp/tools/browser.ts +605 -0
- package/src/mcp/tools/errors.ts +158 -0
- package/src/mcp/tools/names.ts +25 -0
- package/src/util/config.ts +71 -0
- package/src/util/logger.ts +18 -0
- package/src/viewport.ts +204 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ConsoleBuffer, ConsoleLevel } from '../console-buffer.js';
|
|
4
|
+
|
|
5
|
+
// Raw shapes for MCP SDK (expects ZodRawShape, not ZodObject)
|
|
6
|
+
|
|
7
|
+
const getConsoleLogsShape = {
|
|
8
|
+
level: z
|
|
9
|
+
.enum(['log', 'warn', 'error', 'info', 'debug'])
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Filter by single log level'),
|
|
12
|
+
levels: z
|
|
13
|
+
.array(z.enum(['log', 'warn', 'error', 'info', 'debug']))
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Filter by multiple log levels'),
|
|
16
|
+
sinceMs: z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.positive()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Only messages from the last N milliseconds'),
|
|
22
|
+
limit: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.positive()
|
|
26
|
+
.default(100)
|
|
27
|
+
.describe('Maximum messages to return')
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getErrorsShape = {
|
|
31
|
+
sinceMs: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Only errors from the last N milliseconds'),
|
|
37
|
+
limit: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.positive()
|
|
41
|
+
.default(50)
|
|
42
|
+
.describe('Maximum errors to return'),
|
|
43
|
+
includeWarnings: z
|
|
44
|
+
.boolean()
|
|
45
|
+
.default(false)
|
|
46
|
+
.describe('Include warnings in addition to errors')
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function registerErrorTools(
|
|
50
|
+
server: McpServer,
|
|
51
|
+
buffer: ConsoleBuffer
|
|
52
|
+
): void {
|
|
53
|
+
server.tool(
|
|
54
|
+
'browser_get_console_logs',
|
|
55
|
+
'Get console log messages from the browser. Use this to see console output during testing.',
|
|
56
|
+
getConsoleLogsShape,
|
|
57
|
+
async ({ level, levels, sinceMs, limit }) => {
|
|
58
|
+
let filterLevels: ConsoleLevel | ConsoleLevel[] | undefined;
|
|
59
|
+
|
|
60
|
+
if (levels && levels.length > 0) {
|
|
61
|
+
filterLevels = levels as ConsoleLevel[];
|
|
62
|
+
} else if (level) {
|
|
63
|
+
filterLevels = level as ConsoleLevel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const messages = buffer.getAll({
|
|
67
|
+
level: filterLevels,
|
|
68
|
+
sinceMs,
|
|
69
|
+
limit
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const total = buffer.size();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text' as const,
|
|
78
|
+
text: JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
messages,
|
|
81
|
+
count: messages.length,
|
|
82
|
+
total,
|
|
83
|
+
hasMore: total > messages.length
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.tool(
|
|
95
|
+
'browser_get_errors',
|
|
96
|
+
'Get console errors (and optionally warnings) from the browser. Use this to check for JavaScript errors while testing an application.',
|
|
97
|
+
getErrorsShape,
|
|
98
|
+
async ({ sinceMs, limit, includeWarnings }) => {
|
|
99
|
+
const levels: ConsoleLevel[] = includeWarnings
|
|
100
|
+
? ['error', 'warn']
|
|
101
|
+
: ['error'];
|
|
102
|
+
|
|
103
|
+
const errors = buffer.getAll({
|
|
104
|
+
level: levels,
|
|
105
|
+
sinceMs,
|
|
106
|
+
limit
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const total = buffer.size();
|
|
110
|
+
|
|
111
|
+
if (errors.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text' as const,
|
|
116
|
+
text: 'No errors found.'
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: 'text' as const,
|
|
126
|
+
text: JSON.stringify(
|
|
127
|
+
{
|
|
128
|
+
errors,
|
|
129
|
+
count: errors.length,
|
|
130
|
+
hasMore: total > errors.length
|
|
131
|
+
},
|
|
132
|
+
null,
|
|
133
|
+
2
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
server.tool(
|
|
142
|
+
'browser_clear_errors',
|
|
143
|
+
'Clear the console message buffer. Use this before testing to get a clean slate.',
|
|
144
|
+
{},
|
|
145
|
+
async () => {
|
|
146
|
+
const cleared = buffer.clear();
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text' as const,
|
|
152
|
+
text: `Cleared ${cleared} console messages.`
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical list of MCP tool names registered by this server.
|
|
3
|
+
* Shared between the smoke test and e2e test suite so both stay in sync.
|
|
4
|
+
*/
|
|
5
|
+
export const EXPECTED_TOOLS = [
|
|
6
|
+
'browser_start',
|
|
7
|
+
'browser_stop',
|
|
8
|
+
'browser_screenshot',
|
|
9
|
+
'browser_navigate',
|
|
10
|
+
'browser_click',
|
|
11
|
+
'browser_type',
|
|
12
|
+
'browser_evaluate',
|
|
13
|
+
'browser_wait',
|
|
14
|
+
'browser_page_info',
|
|
15
|
+
'browser_list_tabs',
|
|
16
|
+
'browser_new_tab',
|
|
17
|
+
'browser_close_tab',
|
|
18
|
+
'browser_switch_tab',
|
|
19
|
+
'browser_viewport_resize',
|
|
20
|
+
'browser_get_console_logs',
|
|
21
|
+
'browser_get_errors',
|
|
22
|
+
'browser_clear_errors'
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type ToolName = (typeof EXPECTED_TOOLS)[number];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const ConfigSchema = z.object({
|
|
4
|
+
cdpPort: z.number().int().min(1).max(65535).default(9222),
|
|
5
|
+
cdpHost: z.string().default('127.0.0.1'),
|
|
6
|
+
cdpRetryMs: z.number().int().min(100).default(2000),
|
|
7
|
+
cdpMaxRetryMs: z.number().int().min(1000).default(30000),
|
|
8
|
+
logLevel: z
|
|
9
|
+
.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal'])
|
|
10
|
+
.default('info'),
|
|
11
|
+
chromePath: z.string().optional(),
|
|
12
|
+
headless: z.boolean().default(true),
|
|
13
|
+
profileName: z.string().default('profile1'),
|
|
14
|
+
viewport: z.string().default('desktop'),
|
|
15
|
+
responsive: z.boolean().optional()
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
19
|
+
|
|
20
|
+
export interface CliArgs {
|
|
21
|
+
cdpPort?: number;
|
|
22
|
+
cdpHost?: string;
|
|
23
|
+
logLevel?: string;
|
|
24
|
+
chromePath?: string;
|
|
25
|
+
headless?: boolean;
|
|
26
|
+
profileName?: string;
|
|
27
|
+
viewport?: string;
|
|
28
|
+
responsive?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadConfig(cliArgs: CliArgs = {}): Config {
|
|
32
|
+
const env = {
|
|
33
|
+
cdpPort: process.env.PILOT_CDP_PORT
|
|
34
|
+
? parseInt(process.env.PILOT_CDP_PORT, 10)
|
|
35
|
+
: undefined,
|
|
36
|
+
cdpHost: process.env.PILOT_CDP_HOST,
|
|
37
|
+
cdpRetryMs: process.env.PILOT_CDP_RETRY_MS
|
|
38
|
+
? parseInt(process.env.PILOT_CDP_RETRY_MS, 10)
|
|
39
|
+
: undefined,
|
|
40
|
+
cdpMaxRetryMs: process.env.PILOT_CDP_MAX_RETRY_MS
|
|
41
|
+
? parseInt(process.env.PILOT_CDP_MAX_RETRY_MS, 10)
|
|
42
|
+
: undefined,
|
|
43
|
+
logLevel: process.env.PILOT_LOG_LEVEL,
|
|
44
|
+
chromePath: process.env.PILOT_CHROME_PATH,
|
|
45
|
+
headless:
|
|
46
|
+
process.env.PILOT_HEADLESS !== undefined
|
|
47
|
+
? process.env.PILOT_HEADLESS !== 'false'
|
|
48
|
+
: undefined,
|
|
49
|
+
profileName: process.env.PILOT_PROFILE_NAME,
|
|
50
|
+
viewport: process.env.PILOT_VIEWPORT,
|
|
51
|
+
responsive:
|
|
52
|
+
process.env.PILOT_RESPONSIVE !== undefined
|
|
53
|
+
? process.env.PILOT_RESPONSIVE !== 'false'
|
|
54
|
+
: undefined
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const merged = {
|
|
58
|
+
cdpPort: cliArgs.cdpPort ?? env.cdpPort,
|
|
59
|
+
cdpHost: cliArgs.cdpHost ?? env.cdpHost,
|
|
60
|
+
cdpRetryMs: env.cdpRetryMs,
|
|
61
|
+
cdpMaxRetryMs: env.cdpMaxRetryMs,
|
|
62
|
+
logLevel: cliArgs.logLevel ?? env.logLevel,
|
|
63
|
+
chromePath: cliArgs.chromePath ?? env.chromePath,
|
|
64
|
+
headless: cliArgs.headless ?? env.headless,
|
|
65
|
+
profileName: cliArgs.profileName ?? env.profileName,
|
|
66
|
+
viewport: cliArgs.viewport ?? env.viewport,
|
|
67
|
+
responsive: cliArgs.responsive ?? env.responsive
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return ConfigSchema.parse(merged);
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
let _logger: pino.Logger | null = null;
|
|
4
|
+
|
|
5
|
+
export function createLogger(level = 'info'): pino.Logger {
|
|
6
|
+
// Always write to stderr: this is an MCP server that communicates over stdio,
|
|
7
|
+
// so stdout must carry only MCP protocol messages. pino-pretty is a
|
|
8
|
+
// devDependency and is not available in production/global installs.
|
|
9
|
+
_logger = pino({ level }, process.stderr);
|
|
10
|
+
return _logger;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getLogger(): pino.Logger {
|
|
14
|
+
if (!_logger) {
|
|
15
|
+
_logger = createLogger();
|
|
16
|
+
}
|
|
17
|
+
return _logger;
|
|
18
|
+
}
|
package/src/viewport.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Client } from 'chrome-remote-interface';
|
|
2
|
+
|
|
3
|
+
interface EmulationDomain {
|
|
4
|
+
setDeviceMetricsOverride(params: {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
deviceScaleFactor: number;
|
|
8
|
+
mobile: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
setTouchEmulationEnabled(params: {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
maxTouchPoints?: number;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
setUserAgentOverride(params: { userAgent: string }): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BrowserDomain {
|
|
18
|
+
getWindowForTarget(params?: {
|
|
19
|
+
targetId?: string;
|
|
20
|
+
}): Promise<{ windowId: number }>;
|
|
21
|
+
setWindowBounds(params: {
|
|
22
|
+
windowId: number;
|
|
23
|
+
bounds: { width?: number; height?: number };
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ViewportConfig {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
deviceScaleFactor: number;
|
|
31
|
+
mobile: boolean;
|
|
32
|
+
userAgent?: string;
|
|
33
|
+
responsive?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MOBILE_USER_AGENT =
|
|
37
|
+
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36';
|
|
38
|
+
|
|
39
|
+
export const VIEWPORT_PRESETS = {
|
|
40
|
+
desktop: {
|
|
41
|
+
width: 1920,
|
|
42
|
+
height: 1080,
|
|
43
|
+
deviceScaleFactor: 1,
|
|
44
|
+
mobile: false,
|
|
45
|
+
responsive: true
|
|
46
|
+
},
|
|
47
|
+
'desktop-small': {
|
|
48
|
+
width: 1366,
|
|
49
|
+
height: 768,
|
|
50
|
+
deviceScaleFactor: 1,
|
|
51
|
+
mobile: false,
|
|
52
|
+
responsive: true
|
|
53
|
+
},
|
|
54
|
+
tablet: {
|
|
55
|
+
width: 768,
|
|
56
|
+
height: 1024,
|
|
57
|
+
deviceScaleFactor: 2,
|
|
58
|
+
mobile: true,
|
|
59
|
+
userAgent: MOBILE_USER_AGENT
|
|
60
|
+
},
|
|
61
|
+
'tablet-landscape': {
|
|
62
|
+
width: 1024,
|
|
63
|
+
height: 768,
|
|
64
|
+
deviceScaleFactor: 2,
|
|
65
|
+
mobile: true,
|
|
66
|
+
userAgent: MOBILE_USER_AGENT
|
|
67
|
+
},
|
|
68
|
+
mobile: {
|
|
69
|
+
width: 390,
|
|
70
|
+
height: 844,
|
|
71
|
+
deviceScaleFactor: 3,
|
|
72
|
+
mobile: true,
|
|
73
|
+
userAgent: MOBILE_USER_AGENT
|
|
74
|
+
},
|
|
75
|
+
'mobile-landscape': {
|
|
76
|
+
width: 844,
|
|
77
|
+
height: 390,
|
|
78
|
+
deviceScaleFactor: 3,
|
|
79
|
+
mobile: true,
|
|
80
|
+
userAgent: MOBILE_USER_AGENT
|
|
81
|
+
},
|
|
82
|
+
'mobile-small': {
|
|
83
|
+
width: 360,
|
|
84
|
+
height: 800,
|
|
85
|
+
deviceScaleFactor: 3,
|
|
86
|
+
mobile: true,
|
|
87
|
+
userAgent: MOBILE_USER_AGENT
|
|
88
|
+
},
|
|
89
|
+
'desktop-scaled': {
|
|
90
|
+
width: 1536,
|
|
91
|
+
height: 864,
|
|
92
|
+
deviceScaleFactor: 1.25,
|
|
93
|
+
mobile: false
|
|
94
|
+
},
|
|
95
|
+
'desktop-qhd': {
|
|
96
|
+
width: 2560,
|
|
97
|
+
height: 1440,
|
|
98
|
+
deviceScaleFactor: 1,
|
|
99
|
+
mobile: false,
|
|
100
|
+
responsive: true
|
|
101
|
+
},
|
|
102
|
+
'mobile-pro': {
|
|
103
|
+
width: 393,
|
|
104
|
+
height: 852,
|
|
105
|
+
deviceScaleFactor: 3,
|
|
106
|
+
mobile: true,
|
|
107
|
+
userAgent: MOBILE_USER_AGENT
|
|
108
|
+
},
|
|
109
|
+
'mobile-large': {
|
|
110
|
+
width: 430,
|
|
111
|
+
height: 932,
|
|
112
|
+
deviceScaleFactor: 3,
|
|
113
|
+
mobile: true,
|
|
114
|
+
userAgent: MOBILE_USER_AGENT
|
|
115
|
+
},
|
|
116
|
+
'mobile-android': {
|
|
117
|
+
width: 412,
|
|
118
|
+
height: 915,
|
|
119
|
+
deviceScaleFactor: 2.625,
|
|
120
|
+
mobile: true,
|
|
121
|
+
userAgent: MOBILE_USER_AGENT
|
|
122
|
+
}
|
|
123
|
+
} satisfies Record<string, ViewportConfig>;
|
|
124
|
+
|
|
125
|
+
export type ViewportPresetName = keyof typeof VIEWPORT_PRESETS;
|
|
126
|
+
|
|
127
|
+
export interface ResolveViewportOptions {
|
|
128
|
+
preset?: string;
|
|
129
|
+
width?: number;
|
|
130
|
+
height?: number;
|
|
131
|
+
deviceScaleFactor?: number;
|
|
132
|
+
responsive?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveViewport(
|
|
136
|
+
opts: ResolveViewportOptions = {}
|
|
137
|
+
): ViewportConfig {
|
|
138
|
+
const presetName = opts.preset ?? 'desktop';
|
|
139
|
+
const presets = VIEWPORT_PRESETS as Record<string, ViewportConfig>;
|
|
140
|
+
const base = presets[presetName];
|
|
141
|
+
if (!base) {
|
|
142
|
+
const valid = Object.keys(VIEWPORT_PRESETS).join(', ');
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Unknown viewport preset "${presetName}". Valid presets: ${valid}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
...(opts.width !== undefined && { width: opts.width }),
|
|
151
|
+
...(opts.height !== undefined && { height: opts.height }),
|
|
152
|
+
...(opts.deviceScaleFactor !== undefined && {
|
|
153
|
+
deviceScaleFactor: opts.deviceScaleFactor
|
|
154
|
+
}),
|
|
155
|
+
...(opts.responsive !== undefined && { responsive: opts.responsive })
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function applyViewport(
|
|
160
|
+
client: Client,
|
|
161
|
+
config: ViewportConfig
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const emulation = client.Emulation as unknown as EmulationDomain;
|
|
164
|
+
const browser = client.Browser as unknown as BrowserDomain;
|
|
165
|
+
|
|
166
|
+
// Resize the actual browser window so it matches the viewport dimensions.
|
|
167
|
+
// This is needed because --window-size is unreliable (Chrome may restore
|
|
168
|
+
// a previous size from the profile) and setDeviceMetricsOverride only
|
|
169
|
+
// overrides the rendering viewport, not the window itself.
|
|
170
|
+
try {
|
|
171
|
+
const { windowId } = await browser.getWindowForTarget();
|
|
172
|
+
await browser.setWindowBounds({
|
|
173
|
+
windowId,
|
|
174
|
+
bounds: { width: config.width, height: config.height }
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
// Best-effort: headless Chrome or older versions may not support this.
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// In responsive mode, skip setDeviceMetricsOverride so the page uses the
|
|
181
|
+
// real window dimensions and reflows naturally when the window is resized
|
|
182
|
+
// — just like a normal browser. This is the default for desktop presets.
|
|
183
|
+
if (!config.responsive) {
|
|
184
|
+
await emulation.setDeviceMetricsOverride({
|
|
185
|
+
width: config.width,
|
|
186
|
+
height: config.height,
|
|
187
|
+
deviceScaleFactor: config.deviceScaleFactor,
|
|
188
|
+
mobile: config.mobile
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (config.mobile) {
|
|
193
|
+
await emulation.setTouchEmulationEnabled({
|
|
194
|
+
enabled: true,
|
|
195
|
+
maxTouchPoints: 5
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (config.userAgent) {
|
|
200
|
+
await emulation.setUserAgentOverride({
|
|
201
|
+
userAgent: config.userAgent
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"baseUrl": ".",
|
|
18
|
+
"paths": {
|
|
19
|
+
"@/*": ["src/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"],
|
|
23
|
+
"exclude": ["node_modules", "dist", "test", "e2e"]
|
|
24
|
+
}
|