mcp-web-inspector 0.1.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/LICENSE +21 -0
- package/README.md +1017 -0
- package/dist/evals/evals.d.ts +5 -0
- package/dist/evals/evals.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/requestHandler.d.ts +3 -0
- package/dist/requestHandler.js +53 -0
- package/dist/toolHandler.d.ts +91 -0
- package/dist/toolHandler.js +725 -0
- package/dist/tools/api/base.d.ts +33 -0
- package/dist/tools/api/base.js +49 -0
- package/dist/tools/api/index.d.ts +2 -0
- package/dist/tools/api/index.js +3 -0
- package/dist/tools/api/requests.d.ts +47 -0
- package/dist/tools/api/requests.js +168 -0
- package/dist/tools/browser/base.d.ts +51 -0
- package/dist/tools/browser/base.js +111 -0
- package/dist/tools/browser/cleanSession.d.ts +10 -0
- package/dist/tools/browser/cleanSession.js +42 -0
- package/dist/tools/browser/comparePositions.d.ts +11 -0
- package/dist/tools/browser/comparePositions.js +149 -0
- package/dist/tools/browser/computedStyles.d.ts +11 -0
- package/dist/tools/browser/computedStyles.js +128 -0
- package/dist/tools/browser/console.d.ts +37 -0
- package/dist/tools/browser/console.js +106 -0
- package/dist/tools/browser/elementExists.d.ts +9 -0
- package/dist/tools/browser/elementExists.js +57 -0
- package/dist/tools/browser/elementInspection.d.ts +21 -0
- package/dist/tools/browser/elementInspection.js +151 -0
- package/dist/tools/browser/elementPosition.d.ts +11 -0
- package/dist/tools/browser/elementPosition.js +107 -0
- package/dist/tools/browser/elementVisibility.d.ts +12 -0
- package/dist/tools/browser/elementVisibility.js +224 -0
- package/dist/tools/browser/findByText.d.ts +13 -0
- package/dist/tools/browser/findByText.js +207 -0
- package/dist/tools/browser/getRequestDetails.d.ts +9 -0
- package/dist/tools/browser/getRequestDetails.js +137 -0
- package/dist/tools/browser/getTestIds.d.ts +12 -0
- package/dist/tools/browser/getTestIds.js +148 -0
- package/dist/tools/browser/index.d.ts +7 -0
- package/dist/tools/browser/index.js +7 -0
- package/dist/tools/browser/inspectDom.d.ts +12 -0
- package/dist/tools/browser/inspectDom.js +447 -0
- package/dist/tools/browser/interaction.d.ts +104 -0
- package/dist/tools/browser/interaction.js +259 -0
- package/dist/tools/browser/listNetworkRequests.d.ts +10 -0
- package/dist/tools/browser/listNetworkRequests.js +74 -0
- package/dist/tools/browser/measureElement.d.ts +9 -0
- package/dist/tools/browser/measureElement.js +139 -0
- package/dist/tools/browser/navigation.d.ts +38 -0
- package/dist/tools/browser/navigation.js +109 -0
- package/dist/tools/browser/output.d.ts +11 -0
- package/dist/tools/browser/output.js +29 -0
- package/dist/tools/browser/querySelectorAll.d.ts +12 -0
- package/dist/tools/browser/querySelectorAll.js +201 -0
- package/dist/tools/browser/response.d.ts +29 -0
- package/dist/tools/browser/response.js +67 -0
- package/dist/tools/browser/screenshot.d.ts +16 -0
- package/dist/tools/browser/screenshot.js +70 -0
- package/dist/tools/browser/useragent.d.ts +15 -0
- package/dist/tools/browser/useragent.js +32 -0
- package/dist/tools/browser/visiblePage.d.ts +20 -0
- package/dist/tools/browser/visiblePage.js +170 -0
- package/dist/tools/browser/waitForElement.d.ts +10 -0
- package/dist/tools/browser/waitForElement.js +38 -0
- package/dist/tools/browser/waitForNetworkIdle.d.ts +8 -0
- package/dist/tools/browser/waitForNetworkIdle.js +32 -0
- package/dist/tools/codegen/generator.d.ts +21 -0
- package/dist/tools/codegen/generator.js +158 -0
- package/dist/tools/codegen/index.d.ts +11 -0
- package/dist/tools/codegen/index.js +187 -0
- package/dist/tools/codegen/recorder.d.ts +14 -0
- package/dist/tools/codegen/recorder.js +62 -0
- package/dist/tools/codegen/types.d.ts +28 -0
- package/dist/tools/codegen/types.js +1 -0
- package/dist/tools/common/types.d.ts +17 -0
- package/dist/tools/common/types.js +20 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools.d.ts +557 -0
- package/dist/tools.js +554 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
export class WaitForElementTool extends BrowserToolBase {
|
|
3
|
+
async execute(args, context) {
|
|
4
|
+
return this.safeExecute(context, async (page) => {
|
|
5
|
+
const { selector, state = 'visible', timeout = 10000 } = args;
|
|
6
|
+
const normalizedSelector = this.normalizeSelector(selector);
|
|
7
|
+
const locator = page.locator(normalizedSelector);
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
try {
|
|
10
|
+
await locator.waitFor({ state, timeout });
|
|
11
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
12
|
+
// Check current state
|
|
13
|
+
const isVisible = await locator.isVisible().catch(() => false);
|
|
14
|
+
const count = await locator.count();
|
|
15
|
+
const exists = count > 0;
|
|
16
|
+
const statusLines = [
|
|
17
|
+
`✓ Element ${state} after ${duration}s`,
|
|
18
|
+
`Now: ${isVisible ? '✓ visible' : '✗ hidden'}, ${exists ? '✓ exists' : '✗ not found'}`
|
|
19
|
+
];
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: statusLines.join('\n') }],
|
|
22
|
+
isError: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
27
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text',
|
|
31
|
+
text: `✗ Timeout after ${duration}s waiting for element to be ${state}\nError: ${errorMessage}`
|
|
32
|
+
}],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
export interface WaitForNetworkIdleArgs {
|
|
4
|
+
timeout?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class WaitForNetworkIdleTool extends BrowserToolBase {
|
|
7
|
+
execute(args: WaitForNetworkIdleArgs, context: ToolContext): Promise<ToolResponse>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
export class WaitForNetworkIdleTool extends BrowserToolBase {
|
|
3
|
+
async execute(args, context) {
|
|
4
|
+
return this.safeExecute(context, async (page) => {
|
|
5
|
+
const { timeout = 10000 } = args;
|
|
6
|
+
const startTime = Date.now();
|
|
7
|
+
try {
|
|
8
|
+
// Wait for network to be idle (no network connections for at least 500ms)
|
|
9
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
10
|
+
const duration = Date.now() - startTime;
|
|
11
|
+
return {
|
|
12
|
+
content: [{
|
|
13
|
+
type: 'text',
|
|
14
|
+
text: `✓ Network idle after ${duration}ms, 0 pending requests`
|
|
15
|
+
}],
|
|
16
|
+
isError: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const duration = Date.now() - startTime;
|
|
21
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: `✗ Timeout after ${duration}ms waiting for network idle\nError: ${errorMessage}`
|
|
26
|
+
}],
|
|
27
|
+
isError: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CodegenOptions, CodegenResult, CodegenSession } from './types.js';
|
|
2
|
+
export declare class PlaywrightGenerator {
|
|
3
|
+
private static readonly DEFAULT_OPTIONS;
|
|
4
|
+
private options;
|
|
5
|
+
constructor(options?: CodegenOptions);
|
|
6
|
+
private validateOptions;
|
|
7
|
+
generateTest(session: CodegenSession): Promise<CodegenResult>;
|
|
8
|
+
private createTestCase;
|
|
9
|
+
private convertActionToStep;
|
|
10
|
+
private generateNavigateStep;
|
|
11
|
+
private generateFillStep;
|
|
12
|
+
private generateClickStep;
|
|
13
|
+
private generateScreenshotStep;
|
|
14
|
+
private generateExpectResponseStep;
|
|
15
|
+
private generateAssertResponseStep;
|
|
16
|
+
private generateHoverStep;
|
|
17
|
+
private generateSelectStep;
|
|
18
|
+
private generateCustomUserAgentStep;
|
|
19
|
+
private generateTestCode;
|
|
20
|
+
private getOutputFilePath;
|
|
21
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
export class PlaywrightGenerator {
|
|
3
|
+
constructor(options = {}) {
|
|
4
|
+
this.validateOptions(options);
|
|
5
|
+
this.options = { ...PlaywrightGenerator.DEFAULT_OPTIONS, ...options };
|
|
6
|
+
}
|
|
7
|
+
validateOptions(options) {
|
|
8
|
+
if (options.outputPath && typeof options.outputPath !== 'string') {
|
|
9
|
+
throw new Error('outputPath must be a string');
|
|
10
|
+
}
|
|
11
|
+
if (options.testNamePrefix && typeof options.testNamePrefix !== 'string') {
|
|
12
|
+
throw new Error('testNamePrefix must be a string');
|
|
13
|
+
}
|
|
14
|
+
if (options.includeComments !== undefined && typeof options.includeComments !== 'boolean') {
|
|
15
|
+
throw new Error('includeComments must be a boolean');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async generateTest(session) {
|
|
19
|
+
if (!session || !Array.isArray(session.actions)) {
|
|
20
|
+
throw new Error('Invalid session data');
|
|
21
|
+
}
|
|
22
|
+
const testCase = this.createTestCase(session);
|
|
23
|
+
const testCode = this.generateTestCode(testCase);
|
|
24
|
+
const filePath = this.getOutputFilePath(session);
|
|
25
|
+
return {
|
|
26
|
+
testCode,
|
|
27
|
+
filePath,
|
|
28
|
+
sessionId: session.id,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
createTestCase(session) {
|
|
32
|
+
const testCase = {
|
|
33
|
+
name: `${this.options.testNamePrefix}_${new Date(session.startTime).toISOString().split('T')[0]}`,
|
|
34
|
+
steps: [],
|
|
35
|
+
imports: new Set(['test', 'expect']),
|
|
36
|
+
};
|
|
37
|
+
for (const action of session.actions) {
|
|
38
|
+
const step = this.convertActionToStep(action);
|
|
39
|
+
if (step) {
|
|
40
|
+
testCase.steps.push(step);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return testCase;
|
|
44
|
+
}
|
|
45
|
+
convertActionToStep(action) {
|
|
46
|
+
const { toolName, parameters } = action;
|
|
47
|
+
switch (toolName) {
|
|
48
|
+
case 'playwright_navigate':
|
|
49
|
+
return this.generateNavigateStep(parameters);
|
|
50
|
+
case 'playwright_fill':
|
|
51
|
+
return this.generateFillStep(parameters);
|
|
52
|
+
case 'playwright_click':
|
|
53
|
+
return this.generateClickStep(parameters);
|
|
54
|
+
case 'playwright_screenshot':
|
|
55
|
+
return this.generateScreenshotStep(parameters);
|
|
56
|
+
case 'playwright_expect_response':
|
|
57
|
+
return this.generateExpectResponseStep(parameters);
|
|
58
|
+
case 'playwright_assert_response':
|
|
59
|
+
return this.generateAssertResponseStep(parameters);
|
|
60
|
+
case 'playwright_hover':
|
|
61
|
+
return this.generateHoverStep(parameters);
|
|
62
|
+
case 'playwright_select':
|
|
63
|
+
return this.generateSelectStep(parameters);
|
|
64
|
+
case 'playwright_custom_user_agent':
|
|
65
|
+
return this.generateCustomUserAgentStep(parameters);
|
|
66
|
+
default:
|
|
67
|
+
console.warn(`Unsupported tool: ${toolName}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
generateNavigateStep(parameters) {
|
|
72
|
+
const { url, waitUntil } = parameters;
|
|
73
|
+
const options = waitUntil ? `, { waitUntil: '${waitUntil}' }` : '';
|
|
74
|
+
return `
|
|
75
|
+
// Navigate to URL
|
|
76
|
+
await page.goto('${url}'${options});`;
|
|
77
|
+
}
|
|
78
|
+
generateFillStep(parameters) {
|
|
79
|
+
const { selector, value } = parameters;
|
|
80
|
+
return `
|
|
81
|
+
// Fill input field
|
|
82
|
+
await page.fill('${selector}', '${value}');`;
|
|
83
|
+
}
|
|
84
|
+
generateClickStep(parameters) {
|
|
85
|
+
const { selector } = parameters;
|
|
86
|
+
return `
|
|
87
|
+
// Click element
|
|
88
|
+
await page.click('${selector}');`;
|
|
89
|
+
}
|
|
90
|
+
generateScreenshotStep(parameters) {
|
|
91
|
+
const { name, fullPage = false, path } = parameters;
|
|
92
|
+
const options = [];
|
|
93
|
+
if (fullPage)
|
|
94
|
+
options.push('fullPage: true');
|
|
95
|
+
if (path)
|
|
96
|
+
options.push(`path: '${path}'`);
|
|
97
|
+
const optionsStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
|
|
98
|
+
return `
|
|
99
|
+
// Take screenshot
|
|
100
|
+
await page.screenshot({ path: '${name}.png'${optionsStr} });`;
|
|
101
|
+
}
|
|
102
|
+
generateExpectResponseStep(parameters) {
|
|
103
|
+
const { url, id } = parameters;
|
|
104
|
+
return `
|
|
105
|
+
// Wait for response
|
|
106
|
+
const ${id}Response = page.waitForResponse('${url}');`;
|
|
107
|
+
}
|
|
108
|
+
generateAssertResponseStep(parameters) {
|
|
109
|
+
const { id, value } = parameters;
|
|
110
|
+
const assertion = value
|
|
111
|
+
? `\n const responseText = await ${id}Response.text();\n expect(responseText).toContain('${value}');`
|
|
112
|
+
: `\n expect(${id}Response.ok()).toBeTruthy();`;
|
|
113
|
+
return `
|
|
114
|
+
// Assert response${assertion}`;
|
|
115
|
+
}
|
|
116
|
+
generateHoverStep(parameters) {
|
|
117
|
+
const { selector } = parameters;
|
|
118
|
+
return `
|
|
119
|
+
// Hover over element
|
|
120
|
+
await page.hover('${selector}');`;
|
|
121
|
+
}
|
|
122
|
+
generateSelectStep(parameters) {
|
|
123
|
+
const { selector, value } = parameters;
|
|
124
|
+
return `
|
|
125
|
+
// Select option
|
|
126
|
+
await page.selectOption('${selector}', '${value}');`;
|
|
127
|
+
}
|
|
128
|
+
generateCustomUserAgentStep(parameters) {
|
|
129
|
+
const { userAgent } = parameters;
|
|
130
|
+
return `
|
|
131
|
+
// Set custom user agent
|
|
132
|
+
await context.setUserAgent('${userAgent}');`;
|
|
133
|
+
}
|
|
134
|
+
generateTestCode(testCase) {
|
|
135
|
+
const imports = Array.from(testCase.imports)
|
|
136
|
+
.map(imp => `import { ${imp} } from '@playwright/test';`)
|
|
137
|
+
.join('\n');
|
|
138
|
+
return `
|
|
139
|
+
${imports}
|
|
140
|
+
|
|
141
|
+
test('${testCase.name}', async ({ page, context }) => {
|
|
142
|
+
${testCase.steps.join('\n')}
|
|
143
|
+
});`;
|
|
144
|
+
}
|
|
145
|
+
getOutputFilePath(session) {
|
|
146
|
+
if (!session.id) {
|
|
147
|
+
throw new Error('Session ID is required');
|
|
148
|
+
}
|
|
149
|
+
const sanitizedPrefix = this.options.testNamePrefix.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
150
|
+
const fileName = `${sanitizedPrefix}_${session.id}.spec.ts`;
|
|
151
|
+
return path.resolve(this.options.outputPath, fileName);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
PlaywrightGenerator.DEFAULT_OPTIONS = {
|
|
155
|
+
outputPath: 'tests',
|
|
156
|
+
testNamePrefix: 'MCP',
|
|
157
|
+
includeComments: true,
|
|
158
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Tool } from '../../types.js';
|
|
2
|
+
import type { Browser, Page } from 'playwright';
|
|
3
|
+
declare global {
|
|
4
|
+
var browser: Browser | undefined;
|
|
5
|
+
var page: Page | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare const startCodegenSession: Tool;
|
|
8
|
+
export declare const endCodegenSession: Tool;
|
|
9
|
+
export declare const getCodegenSession: Tool;
|
|
10
|
+
export declare const clearCodegenSession: Tool;
|
|
11
|
+
export declare const codegenTools: Tool[];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ActionRecorder } from './recorder.js';
|
|
2
|
+
import { PlaywrightGenerator } from './generator.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
// Helper function to get workspace root path
|
|
6
|
+
const getWorkspaceRoot = () => {
|
|
7
|
+
return process.cwd();
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
outputPath: path.join(getWorkspaceRoot(), 'e2e'),
|
|
11
|
+
testNamePrefix: 'Test',
|
|
12
|
+
includeComments: true
|
|
13
|
+
};
|
|
14
|
+
export const startCodegenSession = {
|
|
15
|
+
name: 'start_codegen_session',
|
|
16
|
+
description: 'Start a new code generation session to record MCP tool actions',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
options: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
description: 'Code generation options',
|
|
23
|
+
properties: {
|
|
24
|
+
outputPath: { type: 'string' },
|
|
25
|
+
testNamePrefix: { type: 'string' },
|
|
26
|
+
includeComments: { type: 'boolean' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
handler: async ({ options = {} }) => {
|
|
32
|
+
try {
|
|
33
|
+
// Merge provided options with defaults
|
|
34
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
35
|
+
// Ensure output path is absolute and normalized
|
|
36
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
37
|
+
const outputPath = path.isAbsolute(mergedOptions.outputPath)
|
|
38
|
+
? mergedOptions.outputPath
|
|
39
|
+
: path.join(workspaceRoot, mergedOptions.outputPath);
|
|
40
|
+
mergedOptions.outputPath = outputPath;
|
|
41
|
+
// Ensure output directory exists
|
|
42
|
+
try {
|
|
43
|
+
await fs.mkdir(outputPath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
catch (mkdirError) {
|
|
46
|
+
throw new Error(`Failed to create output directory: ${mkdirError.message}`);
|
|
47
|
+
}
|
|
48
|
+
const sessionId = ActionRecorder.getInstance().startSession();
|
|
49
|
+
// Store options with the session
|
|
50
|
+
const recorder = ActionRecorder.getInstance();
|
|
51
|
+
const session = recorder.getSession(sessionId);
|
|
52
|
+
if (session) {
|
|
53
|
+
session.options = mergedOptions;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
sessionId,
|
|
57
|
+
options: mergedOptions,
|
|
58
|
+
message: `Started codegen session. Tests will be generated in: ${outputPath}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new Error(`Failed to start codegen session: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
export const endCodegenSession = {
|
|
67
|
+
name: 'end_codegen_session',
|
|
68
|
+
description: 'End the current code generation session and generate Playwright test',
|
|
69
|
+
parameters: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
sessionId: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'ID of the session to end'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ['sessionId']
|
|
78
|
+
},
|
|
79
|
+
handler: async ({ sessionId }) => {
|
|
80
|
+
try {
|
|
81
|
+
const recorder = ActionRecorder.getInstance();
|
|
82
|
+
const session = recorder.endSession(sessionId);
|
|
83
|
+
if (!session) {
|
|
84
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
85
|
+
}
|
|
86
|
+
if (!session.options) {
|
|
87
|
+
throw new Error(`Session ${sessionId} has no options configured`);
|
|
88
|
+
}
|
|
89
|
+
const generator = new PlaywrightGenerator(session.options);
|
|
90
|
+
const result = await generator.generateTest(session);
|
|
91
|
+
// Double check output directory exists
|
|
92
|
+
const outputDir = path.dirname(result.filePath);
|
|
93
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
94
|
+
// Write test file
|
|
95
|
+
try {
|
|
96
|
+
await fs.writeFile(result.filePath, result.testCode, 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
catch (writeError) {
|
|
99
|
+
throw new Error(`Failed to write test file: ${writeError.message}`);
|
|
100
|
+
}
|
|
101
|
+
// Close Playwright browser and cleanup
|
|
102
|
+
try {
|
|
103
|
+
if (global.browser?.isConnected()) {
|
|
104
|
+
await global.browser.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (browserError) {
|
|
108
|
+
console.warn('Failed to close browser:', browserError.message);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
global.browser = undefined;
|
|
112
|
+
global.page = undefined;
|
|
113
|
+
}
|
|
114
|
+
const absolutePath = path.resolve(result.filePath);
|
|
115
|
+
return {
|
|
116
|
+
filePath: absolutePath,
|
|
117
|
+
outputDirectory: outputDir,
|
|
118
|
+
testCode: result.testCode,
|
|
119
|
+
message: `Generated test file at: ${absolutePath}\nOutput directory: ${outputDir}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// Ensure browser cleanup even on error
|
|
124
|
+
try {
|
|
125
|
+
if (global.browser?.isConnected()) {
|
|
126
|
+
await global.browser.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore cleanup errors
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
global.browser = undefined;
|
|
134
|
+
global.page = undefined;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Failed to end codegen session: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
export const getCodegenSession = {
|
|
141
|
+
name: 'get_codegen_session',
|
|
142
|
+
description: 'Get information about a code generation session',
|
|
143
|
+
parameters: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
sessionId: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'ID of the session to retrieve'
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
required: ['sessionId']
|
|
152
|
+
},
|
|
153
|
+
handler: async ({ sessionId }) => {
|
|
154
|
+
const session = ActionRecorder.getInstance().getSession(sessionId);
|
|
155
|
+
if (!session) {
|
|
156
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
157
|
+
}
|
|
158
|
+
return session;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
export const clearCodegenSession = {
|
|
162
|
+
name: 'clear_codegen_session',
|
|
163
|
+
description: 'Clear a code generation session',
|
|
164
|
+
parameters: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
sessionId: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'ID of the session to clear'
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
required: ['sessionId']
|
|
173
|
+
},
|
|
174
|
+
handler: async ({ sessionId }) => {
|
|
175
|
+
const success = ActionRecorder.getInstance().clearSession(sessionId);
|
|
176
|
+
if (!success) {
|
|
177
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
178
|
+
}
|
|
179
|
+
return { success };
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
export const codegenTools = [
|
|
183
|
+
startCodegenSession,
|
|
184
|
+
endCodegenSession,
|
|
185
|
+
getCodegenSession,
|
|
186
|
+
clearCodegenSession
|
|
187
|
+
];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CodegenSession } from './types';
|
|
2
|
+
export declare class ActionRecorder {
|
|
3
|
+
private static instance;
|
|
4
|
+
private sessions;
|
|
5
|
+
private activeSession;
|
|
6
|
+
private constructor();
|
|
7
|
+
static getInstance(): ActionRecorder;
|
|
8
|
+
startSession(): string;
|
|
9
|
+
endSession(sessionId: string): CodegenSession | null;
|
|
10
|
+
recordAction(toolName: string, parameters: Record<string, unknown>, result?: unknown): void;
|
|
11
|
+
getSession(sessionId: string): CodegenSession | null;
|
|
12
|
+
getActiveSession(): CodegenSession | null;
|
|
13
|
+
clearSession(sessionId: string): boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
export class ActionRecorder {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.sessions = new Map();
|
|
5
|
+
this.activeSession = null;
|
|
6
|
+
}
|
|
7
|
+
static getInstance() {
|
|
8
|
+
if (!ActionRecorder.instance) {
|
|
9
|
+
ActionRecorder.instance = new ActionRecorder();
|
|
10
|
+
}
|
|
11
|
+
return ActionRecorder.instance;
|
|
12
|
+
}
|
|
13
|
+
startSession() {
|
|
14
|
+
const sessionId = uuidv4();
|
|
15
|
+
this.sessions.set(sessionId, {
|
|
16
|
+
id: sessionId,
|
|
17
|
+
actions: [],
|
|
18
|
+
startTime: Date.now(),
|
|
19
|
+
});
|
|
20
|
+
this.activeSession = sessionId;
|
|
21
|
+
return sessionId;
|
|
22
|
+
}
|
|
23
|
+
endSession(sessionId) {
|
|
24
|
+
const session = this.sessions.get(sessionId);
|
|
25
|
+
if (session) {
|
|
26
|
+
session.endTime = Date.now();
|
|
27
|
+
if (this.activeSession === sessionId) {
|
|
28
|
+
this.activeSession = null;
|
|
29
|
+
}
|
|
30
|
+
return session;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
recordAction(toolName, parameters, result) {
|
|
35
|
+
if (!this.activeSession) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const session = this.sessions.get(this.activeSession);
|
|
39
|
+
if (!session) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const action = {
|
|
43
|
+
toolName,
|
|
44
|
+
parameters,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
result,
|
|
47
|
+
};
|
|
48
|
+
session.actions.push(action);
|
|
49
|
+
}
|
|
50
|
+
getSession(sessionId) {
|
|
51
|
+
return this.sessions.get(sessionId) || null;
|
|
52
|
+
}
|
|
53
|
+
getActiveSession() {
|
|
54
|
+
return this.activeSession ? this.sessions.get(this.activeSession) : null;
|
|
55
|
+
}
|
|
56
|
+
clearSession(sessionId) {
|
|
57
|
+
if (this.activeSession === sessionId) {
|
|
58
|
+
this.activeSession = null;
|
|
59
|
+
}
|
|
60
|
+
return this.sessions.delete(sessionId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CodegenAction {
|
|
2
|
+
toolName: string;
|
|
3
|
+
parameters: Record<string, unknown>;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
result?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface CodegenSession {
|
|
8
|
+
id: string;
|
|
9
|
+
actions: CodegenAction[];
|
|
10
|
+
startTime: number;
|
|
11
|
+
endTime?: number;
|
|
12
|
+
options?: CodegenOptions;
|
|
13
|
+
}
|
|
14
|
+
export interface PlaywrightTestCase {
|
|
15
|
+
name: string;
|
|
16
|
+
steps: string[];
|
|
17
|
+
imports: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
export interface CodegenOptions {
|
|
20
|
+
outputPath?: string;
|
|
21
|
+
testNamePrefix?: string;
|
|
22
|
+
includeComments?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface CodegenResult {
|
|
25
|
+
testCode: string;
|
|
26
|
+
filePath: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CallToolResult, TextContent, ImageContent } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import type { Page, Browser, APIRequestContext } from 'playwright';
|
|
3
|
+
export interface ToolContext {
|
|
4
|
+
page?: Page;
|
|
5
|
+
browser?: Browser;
|
|
6
|
+
apiContext?: APIRequestContext;
|
|
7
|
+
server?: any;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolResponse extends CallToolResult {
|
|
10
|
+
content: (TextContent | ImageContent)[];
|
|
11
|
+
isError: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ToolHandler {
|
|
14
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createErrorResponse(message: string): ToolResponse;
|
|
17
|
+
export declare function createSuccessResponse(message: string | string[]): ToolResponse;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Helper functions for creating responses
|
|
2
|
+
export function createErrorResponse(message) {
|
|
3
|
+
return {
|
|
4
|
+
content: [{
|
|
5
|
+
type: "text",
|
|
6
|
+
text: message
|
|
7
|
+
}],
|
|
8
|
+
isError: true
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function createSuccessResponse(message) {
|
|
12
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
13
|
+
return {
|
|
14
|
+
content: messages.map(msg => ({
|
|
15
|
+
type: "text",
|
|
16
|
+
text: msg
|
|
17
|
+
})),
|
|
18
|
+
isError: false
|
|
19
|
+
};
|
|
20
|
+
}
|