Update test framework: fix run_tests.py to support all test files, add auto-import-check for test files

This commit is contained in:
qiaoxinjiu
2026-05-09 15:11:30 +08:00
parent eb053a347f
commit eaba8328da
21739 changed files with 2236758 additions and 719 deletions

View File

@@ -0,0 +1,21 @@
import { CodegenOptions, CodegenResult, CodegenSession } from './types.js';
export declare class PlaywrightGenerator {
private static readonly DEFAULT_OPTIONS;
private options;
constructor(options?: CodegenOptions);
private validateOptions;
generateTest(session: CodegenSession): Promise<CodegenResult>;
private createTestCase;
private convertActionToStep;
private generateNavigateStep;
private generateFillStep;
private generateClickStep;
private generateScreenshotStep;
private generateExpectResponseStep;
private generateAssertResponseStep;
private generateHoverStep;
private generateSelectStep;
private generateCustomUserAgentStep;
private generateTestCode;
private getOutputFilePath;
}

View File

@@ -0,0 +1,158 @@
import * as path from 'path';
export class PlaywrightGenerator {
constructor(options = {}) {
this.validateOptions(options);
this.options = { ...PlaywrightGenerator.DEFAULT_OPTIONS, ...options };
}
validateOptions(options) {
if (options.outputPath && typeof options.outputPath !== 'string') {
throw new Error('outputPath must be a string');
}
if (options.testNamePrefix && typeof options.testNamePrefix !== 'string') {
throw new Error('testNamePrefix must be a string');
}
if (options.includeComments !== undefined && typeof options.includeComments !== 'boolean') {
throw new Error('includeComments must be a boolean');
}
}
async generateTest(session) {
if (!session || !Array.isArray(session.actions)) {
throw new Error('Invalid session data');
}
const testCase = this.createTestCase(session);
const testCode = this.generateTestCode(testCase);
const filePath = this.getOutputFilePath(session);
return {
testCode,
filePath,
sessionId: session.id,
};
}
createTestCase(session) {
const testCase = {
name: `${this.options.testNamePrefix}_${new Date(session.startTime).toISOString().split('T')[0]}`,
steps: [],
imports: new Set(['test', 'expect']),
};
for (const action of session.actions) {
const step = this.convertActionToStep(action);
if (step) {
testCase.steps.push(step);
}
}
return testCase;
}
convertActionToStep(action) {
const { toolName, parameters } = action;
switch (toolName) {
case 'playwright_navigate':
return this.generateNavigateStep(parameters);
case 'playwright_fill':
return this.generateFillStep(parameters);
case 'playwright_click':
return this.generateClickStep(parameters);
case 'playwright_screenshot':
return this.generateScreenshotStep(parameters);
case 'playwright_expect_response':
return this.generateExpectResponseStep(parameters);
case 'playwright_assert_response':
return this.generateAssertResponseStep(parameters);
case 'playwright_hover':
return this.generateHoverStep(parameters);
case 'playwright_select':
return this.generateSelectStep(parameters);
case 'playwright_custom_user_agent':
return this.generateCustomUserAgentStep(parameters);
default:
console.warn(`Unsupported tool: ${toolName}`);
return null;
}
}
generateNavigateStep(parameters) {
const { url, waitUntil } = parameters;
const options = waitUntil ? `, { waitUntil: '${waitUntil}' }` : '';
return `
// Navigate to URL
await page.goto('${url}'${options});`;
}
generateFillStep(parameters) {
const { selector, value } = parameters;
return `
// Fill input field
await page.fill('${selector}', '${value}');`;
}
generateClickStep(parameters) {
const { selector } = parameters;
return `
// Click element
await page.click('${selector}');`;
}
generateScreenshotStep(parameters) {
const { name, fullPage = false, path } = parameters;
const options = [];
if (fullPage)
options.push('fullPage: true');
if (path)
options.push(`path: '${path}'`);
const optionsStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
return `
// Take screenshot
await page.screenshot({ path: '${name}.png'${optionsStr} });`;
}
generateExpectResponseStep(parameters) {
const { url, id } = parameters;
return `
// Wait for response
const ${id}Response = page.waitForResponse('${url}');`;
}
generateAssertResponseStep(parameters) {
const { id, value } = parameters;
const assertion = value
? `\n const responseText = await ${id}Response.text();\n expect(responseText).toContain('${value}');`
: `\n expect(${id}Response.ok()).toBeTruthy();`;
return `
// Assert response${assertion}`;
}
generateHoverStep(parameters) {
const { selector } = parameters;
return `
// Hover over element
await page.hover('${selector}');`;
}
generateSelectStep(parameters) {
const { selector, value } = parameters;
return `
// Select option
await page.selectOption('${selector}', '${value}');`;
}
generateCustomUserAgentStep(parameters) {
const { userAgent } = parameters;
return `
// Set custom user agent
await context.setUserAgent('${userAgent}');`;
}
generateTestCode(testCase) {
const imports = Array.from(testCase.imports)
.map(imp => `import { ${imp} } from '@playwright/test';`)
.join('\n');
return `
${imports}
test('${testCase.name}', async ({ page, context }) => {
${testCase.steps.join('\n')}
});`;
}
getOutputFilePath(session) {
if (!session.id) {
throw new Error('Session ID is required');
}
const sanitizedPrefix = this.options.testNamePrefix.toLowerCase().replace(/[^a-z0-9_]/g, '_');
const fileName = `${sanitizedPrefix}_${session.id}.spec.ts`;
return path.resolve(this.options.outputPath, fileName);
}
}
PlaywrightGenerator.DEFAULT_OPTIONS = {
outputPath: 'tests',
testNamePrefix: 'MCP',
includeComments: true,
};

View File

@@ -0,0 +1,11 @@
import { Tool } from '../../types.js';
import type { Browser, Page } from 'playwright';
declare global {
var browser: Browser | undefined;
var page: Page | undefined;
}
export declare const startCodegenSession: Tool;
export declare const endCodegenSession: Tool;
export declare const getCodegenSession: Tool;
export declare const clearCodegenSession: Tool;
export declare const codegenTools: Tool[];

View File

@@ -0,0 +1,187 @@
import { ActionRecorder } from './recorder.js';
import { PlaywrightGenerator } from './generator.js';
import * as fs from 'fs/promises';
import * as path from 'path';
// Helper function to get workspace root path
const getWorkspaceRoot = () => {
return process.cwd();
};
const DEFAULT_OPTIONS = {
outputPath: path.join(getWorkspaceRoot(), 'e2e'),
testNamePrefix: 'Test',
includeComments: true
};
export const startCodegenSession = {
name: 'start_codegen_session',
description: 'Start a new code generation session to record MCP tool actions',
parameters: {
type: 'object',
properties: {
options: {
type: 'object',
description: 'Code generation options',
properties: {
outputPath: { type: 'string' },
testNamePrefix: { type: 'string' },
includeComments: { type: 'boolean' }
}
}
}
},
handler: async ({ options = {} }) => {
try {
// Merge provided options with defaults
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
// Ensure output path is absolute and normalized
const workspaceRoot = getWorkspaceRoot();
const outputPath = path.isAbsolute(mergedOptions.outputPath)
? mergedOptions.outputPath
: path.join(workspaceRoot, mergedOptions.outputPath);
mergedOptions.outputPath = outputPath;
// Ensure output directory exists
try {
await fs.mkdir(outputPath, { recursive: true });
}
catch (mkdirError) {
throw new Error(`Failed to create output directory: ${mkdirError.message}`);
}
const sessionId = ActionRecorder.getInstance().startSession();
// Store options with the session
const recorder = ActionRecorder.getInstance();
const session = recorder.getSession(sessionId);
if (session) {
session.options = mergedOptions;
}
return {
sessionId,
options: mergedOptions,
message: `Started codegen session. Tests will be generated in: ${outputPath}`
};
}
catch (error) {
throw new Error(`Failed to start codegen session: ${error.message}`);
}
}
};
export const endCodegenSession = {
name: 'end_codegen_session',
description: 'End the current code generation session and generate Playwright test',
parameters: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'ID of the session to end'
}
},
required: ['sessionId']
},
handler: async ({ sessionId }) => {
try {
const recorder = ActionRecorder.getInstance();
const session = recorder.endSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
if (!session.options) {
throw new Error(`Session ${sessionId} has no options configured`);
}
const generator = new PlaywrightGenerator(session.options);
const result = await generator.generateTest(session);
// Double check output directory exists
const outputDir = path.dirname(result.filePath);
await fs.mkdir(outputDir, { recursive: true });
// Write test file
try {
await fs.writeFile(result.filePath, result.testCode, 'utf-8');
}
catch (writeError) {
throw new Error(`Failed to write test file: ${writeError.message}`);
}
// Close Playwright browser and cleanup
try {
if (global.browser?.isConnected()) {
await global.browser.close();
}
}
catch (browserError) {
console.warn('Failed to close browser:', browserError.message);
}
finally {
global.browser = undefined;
global.page = undefined;
}
const absolutePath = path.resolve(result.filePath);
return {
filePath: absolutePath,
outputDirectory: outputDir,
testCode: result.testCode,
message: `Generated test file at: ${absolutePath}\nOutput directory: ${outputDir}`
};
}
catch (error) {
// Ensure browser cleanup even on error
try {
if (global.browser?.isConnected()) {
await global.browser.close();
}
}
catch {
// Ignore cleanup errors
}
finally {
global.browser = undefined;
global.page = undefined;
}
throw new Error(`Failed to end codegen session: ${error.message}`);
}
}
};
export const getCodegenSession = {
name: 'get_codegen_session',
description: 'Get information about a code generation session',
parameters: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'ID of the session to retrieve'
}
},
required: ['sessionId']
},
handler: async ({ sessionId }) => {
const session = ActionRecorder.getInstance().getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
return session;
}
};
export const clearCodegenSession = {
name: 'clear_codegen_session',
description: 'Clear a code generation session',
parameters: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'ID of the session to clear'
}
},
required: ['sessionId']
},
handler: async ({ sessionId }) => {
const success = ActionRecorder.getInstance().clearSession(sessionId);
if (!success) {
throw new Error(`Session ${sessionId} not found`);
}
return { success };
}
};
export const codegenTools = [
startCodegenSession,
endCodegenSession,
getCodegenSession,
clearCodegenSession
];

View File

@@ -0,0 +1,14 @@
import { CodegenSession } from './types';
export declare class ActionRecorder {
private static instance;
private sessions;
private activeSession;
private constructor();
static getInstance(): ActionRecorder;
startSession(): string;
endSession(sessionId: string): CodegenSession | null;
recordAction(toolName: string, parameters: Record<string, unknown>, result?: unknown): void;
getSession(sessionId: string): CodegenSession | null;
getActiveSession(): CodegenSession | null;
clearSession(sessionId: string): boolean;
}

View File

@@ -0,0 +1,62 @@
import { v4 as uuidv4 } from 'uuid';
export class ActionRecorder {
constructor() {
this.sessions = new Map();
this.activeSession = null;
}
static getInstance() {
if (!ActionRecorder.instance) {
ActionRecorder.instance = new ActionRecorder();
}
return ActionRecorder.instance;
}
startSession() {
const sessionId = uuidv4();
this.sessions.set(sessionId, {
id: sessionId,
actions: [],
startTime: Date.now(),
});
this.activeSession = sessionId;
return sessionId;
}
endSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.endTime = Date.now();
if (this.activeSession === sessionId) {
this.activeSession = null;
}
return session;
}
return null;
}
recordAction(toolName, parameters, result) {
if (!this.activeSession) {
return;
}
const session = this.sessions.get(this.activeSession);
if (!session) {
return;
}
const action = {
toolName,
parameters,
timestamp: Date.now(),
result,
};
session.actions.push(action);
}
getSession(sessionId) {
return this.sessions.get(sessionId) || null;
}
getActiveSession() {
return this.activeSession ? this.sessions.get(this.activeSession) : null;
}
clearSession(sessionId) {
if (this.activeSession === sessionId) {
this.activeSession = null;
}
return this.sessions.delete(sessionId);
}
}

View File

@@ -0,0 +1,28 @@
export interface CodegenAction {
toolName: string;
parameters: Record<string, unknown>;
timestamp: number;
result?: unknown;
}
export interface CodegenSession {
id: string;
actions: CodegenAction[];
startTime: number;
endTime?: number;
options?: CodegenOptions;
}
export interface PlaywrightTestCase {
name: string;
steps: string[];
imports: Set<string>;
}
export interface CodegenOptions {
outputPath?: string;
testNamePrefix?: string;
includeComments?: boolean;
}
export interface CodegenResult {
testCode: string;
filePath: string;
sessionId: string;
}

View File

@@ -0,0 +1 @@
export {};