#
tokens: 41287/50000 12/92 files (page 2/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 5. Use http://codebase.md/pvinis/mcp-playwright-stealth?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   └── workflows
│       ├── docusaurus-gh-pages.yml
│       ├── node.js.yml
│       └── test.yml
├── .gitignore
├── Dockerfile
├── docs
│   ├── docs
│   │   ├── ai-courses
│   │   │   ├── _category_.json
│   │   │   ├── AIAgents.mdx
│   │   │   ├── GenAICourse.mdx
│   │   │   ├── img
│   │   │   │   └── GenAI.png
│   │   │   └── MachineLearning.mdx
│   │   ├── img
│   │   │   └── mcp-server.png
│   │   ├── intro.mdx
│   │   ├── local-setup
│   │   │   ├── _category_.json
│   │   │   ├── Debugging.mdx
│   │   │   ├── img
│   │   │   │   └── mcp-server.png
│   │   │   └── Installation.mdx
│   │   ├── playwright-api
│   │   │   ├── _category_.json
│   │   │   ├── Examples.md
│   │   │   ├── img
│   │   │   │   ├── api-response.png
│   │   │   │   └── playwright-api.png
│   │   │   └── Supported-Tools.mdx
│   │   ├── playwright-web
│   │   │   ├── _category_.json
│   │   │   ├── Console-Logging.mdx
│   │   │   ├── Examples.md
│   │   │   ├── img
│   │   │   │   ├── console-log.gif
│   │   │   │   ├── mcp-execution.png
│   │   │   │   └── mcp-result.png
│   │   │   ├── Recording-Actions.mdx
│   │   │   ├── Support-of-Cline-Cursor.mdx
│   │   │   └── Supported-Tools.mdx
│   │   ├── release.mdx
│   │   └── testing-videos
│   │       ├── _category_.json
│   │       ├── AIAgents.mdx
│   │       └── Bdd.mdx
│   ├── docusaurus.config.ts
│   ├── package-lock.json
│   ├── package.json
│   ├── sidebars.ts
│   ├── src
│   │   ├── components
│   │   │   └── HomepageFeatures
│   │   │       ├── index.tsx
│   │   │       ├── styles.module.css
│   │   │       └── YouTubeVideoEmbed.tsx
│   │   ├── css
│   │   │   └── custom.css
│   │   └── pages
│   │       ├── index.module.css
│   │       ├── index.tsx
│   │       └── markdown-page.md
│   ├── static
│   │   ├── .nojekyll
│   │   └── img
│   │       ├── docusaurus-social-card.jpg
│   │       ├── docusaurus.png
│   │       ├── EA-Icon.jpg
│   │       ├── EA-Icon.svg
│   │       ├── easy-to-use.svg
│   │       ├── favicon.ico
│   │       ├── logo.svg
│   │       ├── node.svg
│   │       ├── playwright.svg
│   │       ├── undraw_docusaurus_mountain.svg
│   │       ├── undraw_docusaurus_react.svg
│   │       └── undraw_docusaurus_tree.svg
│   └── tsconfig.json
├── image
│   └── playwright_claude.png
├── jest.config.cjs
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── run-tests.cjs
├── run-tests.js
├── smithery.yaml
├── src
│   ├── __tests__
│   │   ├── codegen.test.ts
│   │   ├── toolHandler.test.ts
│   │   ├── tools
│   │   │   ├── api
│   │   │   │   └── requests.test.ts
│   │   │   └── browser
│   │   │       ├── advancedInteraction.test.ts
│   │   │       ├── console.test.ts
│   │   │       ├── goNavigation.test.ts
│   │   │       ├── interaction.test.ts
│   │   │       ├── navigation.test.ts
│   │   │       ├── output.test.ts
│   │   │       ├── screenshot.test.ts
│   │   │       └── visiblePage.test.ts
│   │   └── tools.test.ts
│   ├── index.ts
│   ├── requestHandler.ts
│   ├── toolHandler.ts
│   ├── tools
│   │   ├── api
│   │   │   ├── base.ts
│   │   │   ├── index.ts
│   │   │   └── requests.ts
│   │   ├── browser
│   │   │   ├── base.ts
│   │   │   ├── console.ts
│   │   │   ├── index.ts
│   │   │   ├── interaction.ts
│   │   │   ├── navigation.ts
│   │   │   ├── output.ts
│   │   │   ├── response.ts
│   │   │   ├── screenshot.ts
│   │   │   ├── useragent.ts
│   │   │   └── visiblePage.ts
│   │   ├── codegen
│   │   │   ├── generator.ts
│   │   │   ├── index.ts
│   │   │   ├── recorder.ts
│   │   │   └── types.ts
│   │   ├── common
│   │   │   └── types.ts
│   │   └── index.ts
│   ├── tools.ts
│   └── types.ts
├── test-import.js
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/src/__tests__/tools/browser/visiblePage.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { VisibleTextTool, VisibleHtmlTool } from '../../../tools/browser/visiblePage.js';
  2 | import { ToolContext } from '../../../tools/common/types.js';
  3 | import { Page, Browser } from 'playwright';
  4 | import { jest } from '@jest/globals';
  5 | 
  6 | // Mock the Page object
  7 | const mockEvaluate = jest.fn();
  8 | const mockContent = jest.fn();
  9 | const mockIsClosed = jest.fn().mockReturnValue(false);
 10 | 
 11 | const mockPage = {
 12 |   evaluate: mockEvaluate,
 13 |   content: mockContent,
 14 |   isClosed: mockIsClosed
 15 | } as unknown as Page;
 16 | 
 17 | // Mock the browser
 18 | const mockIsConnected = jest.fn().mockReturnValue(true);
 19 | const mockBrowser = {
 20 |   isConnected: mockIsConnected
 21 | } as unknown as Browser;
 22 | 
 23 | // Mock the server
 24 | const mockServer = {
 25 |   sendMessage: jest.fn()
 26 | };
 27 | 
 28 | // Mock context
 29 | const mockContext = {
 30 |   page: mockPage,
 31 |   browser: mockBrowser,
 32 |   server: mockServer
 33 | } as ToolContext;
 34 | 
 35 | describe('VisibleTextTool', () => {
 36 |   let visibleTextTool: VisibleTextTool;
 37 | 
 38 |   beforeEach(() => {
 39 |     jest.clearAllMocks();
 40 |     visibleTextTool = new VisibleTextTool(mockServer);
 41 |     // Reset mocks
 42 |     mockIsConnected.mockReturnValue(true);
 43 |     mockIsClosed.mockReturnValue(false);
 44 |     mockEvaluate.mockImplementation(() => Promise.resolve('Sample visible text content'));
 45 |   });
 46 | 
 47 |   test('should retrieve visible text content', async () => {
 48 |     const args = {};
 49 | 
 50 |     const result = await visibleTextTool.execute(args, mockContext);
 51 | 
 52 |     expect(mockEvaluate).toHaveBeenCalled();
 53 |     expect(result.isError).toBe(false);
 54 |     expect(result.content[0].text).toContain('Visible text content');
 55 |     expect(result.content[0].text).toContain('Sample visible text content');
 56 |   });
 57 | 
 58 |   test('should handle missing page', async () => {
 59 |     const args = {};
 60 | 
 61 |     // Context with browser but without page
 62 |     const contextWithoutPage = {
 63 |       browser: mockBrowser,
 64 |       server: mockServer
 65 |     } as unknown as ToolContext;
 66 | 
 67 |     const result = await visibleTextTool.execute(args, contextWithoutPage);
 68 | 
 69 |     expect(mockEvaluate).not.toHaveBeenCalled();
 70 |     expect(result.isError).toBe(true);
 71 |     expect(result.content[0].text).toContain('Page is not available');
 72 |   });
 73 |   
 74 |   test('should handle disconnected browser', async () => {
 75 |     const args = {};
 76 |     
 77 |     // Mock disconnected browser
 78 |     mockIsConnected.mockReturnValueOnce(false);
 79 |     
 80 |     const result = await visibleTextTool.execute(args, mockContext);
 81 |     
 82 |     expect(mockEvaluate).not.toHaveBeenCalled();
 83 |     expect(result.isError).toBe(true);
 84 |     expect(result.content[0].text).toContain('Browser is not connected');
 85 |   });
 86 |   
 87 |   test('should handle closed page', async () => {
 88 |     const args = {};
 89 |     
 90 |     // Mock closed page
 91 |     mockIsClosed.mockReturnValueOnce(true);
 92 |     
 93 |     const result = await visibleTextTool.execute(args, mockContext);
 94 |     
 95 |     expect(mockEvaluate).not.toHaveBeenCalled();
 96 |     expect(result.isError).toBe(true);
 97 |     expect(result.content[0].text).toContain('Page is not available or has been closed');
 98 |   });
 99 | 
100 |   test('should handle evaluation errors', async () => {
101 |     const args = {};
102 | 
103 |     // Mock evaluation error
104 |     mockEvaluate.mockImplementationOnce(() => Promise.reject(new Error('Evaluation failed')));
105 | 
106 |     const result = await visibleTextTool.execute(args, mockContext);
107 | 
108 |     expect(mockEvaluate).toHaveBeenCalled();
109 |     expect(result.isError).toBe(true);
110 |     expect(result.content[0].text).toContain('Failed to get visible text content');
111 |     expect(result.content[0].text).toContain('Evaluation failed');
112 |   });
113 | });
114 | 
115 | describe('VisibleHtmlTool', () => {
116 |   let visibleHtmlTool: VisibleHtmlTool;
117 | 
118 |   beforeEach(() => {
119 |     jest.clearAllMocks();
120 |     visibleHtmlTool = new VisibleHtmlTool(mockServer);
121 |     // Reset mocks
122 |     mockIsConnected.mockReturnValue(true);
123 |     mockIsClosed.mockReturnValue(false);
124 |     mockContent.mockImplementation(() => Promise.resolve('<html><body>Sample HTML content</body></html>'));
125 |   });
126 | 
127 |   test('should retrieve HTML content', async () => {
128 |     const args = {};
129 | 
130 |     const result = await visibleHtmlTool.execute(args, mockContext);
131 | 
132 |     expect(mockContent).toHaveBeenCalled();
133 |     expect(result.isError).toBe(false);
134 |     expect(result.content[0].text).toContain('HTML content');
135 |     expect(result.content[0].text).toContain('<html><body>Sample HTML content</body></html>');
136 |   });
137 | 
138 |   test('should handle missing page', async () => {
139 |     const args = {};
140 | 
141 |     // Context with browser but without page
142 |     const contextWithoutPage = {
143 |       browser: mockBrowser,
144 |       server: mockServer
145 |     } as unknown as ToolContext;
146 | 
147 |     const result = await visibleHtmlTool.execute(args, contextWithoutPage);
148 | 
149 |     expect(mockContent).not.toHaveBeenCalled();
150 |     expect(result.isError).toBe(true);
151 |     expect(result.content[0].text).toContain('Page is not available');
152 |   });
153 |   
154 |   test('should handle disconnected browser', async () => {
155 |     const args = {};
156 |     
157 |     // Mock disconnected browser
158 |     mockIsConnected.mockReturnValueOnce(false);
159 |     
160 |     const result = await visibleHtmlTool.execute(args, mockContext);
161 |     
162 |     expect(mockContent).not.toHaveBeenCalled();
163 |     expect(result.isError).toBe(true);
164 |     expect(result.content[0].text).toContain('Browser is not connected');
165 |   });
166 |   
167 |   test('should handle closed page', async () => {
168 |     const args = {};
169 |     
170 |     // Mock closed page
171 |     mockIsClosed.mockReturnValueOnce(true);
172 |     
173 |     const result = await visibleHtmlTool.execute(args, mockContext);
174 |     
175 |     expect(mockContent).not.toHaveBeenCalled();
176 |     expect(result.isError).toBe(true);
177 |     expect(result.content[0].text).toContain('Page is not available or has been closed');
178 |   });
179 | 
180 |   test('should handle content retrieval errors', async () => {
181 |     const args = {};
182 | 
183 |     // Mock content error
184 |     mockContent.mockImplementationOnce(() => Promise.reject(new Error('Content retrieval failed')));
185 | 
186 |     const result = await visibleHtmlTool.execute(args, mockContext);
187 | 
188 |     expect(mockContent).toHaveBeenCalled();
189 |     expect(result.isError).toBe(true);
190 |     expect(result.content[0].text).toContain('Failed to get visible HTML content');
191 |     expect(result.content[0].text).toContain('Content retrieval failed');
192 |   });
193 | });
```

--------------------------------------------------------------------------------
/src/tools/codegen/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Tool } from "../../types.js";
  2 | import { ActionRecorder } from "./recorder.js";
  3 | import { PlaywrightGenerator } from "./generator.js";
  4 | import { CodegenOptions } from "./types.js";
  5 | import * as fs from "fs/promises";
  6 | import * as path from "path";
  7 | import type { Browser, Page } from "rebrowser-playwright";
  8 | 
  9 | declare global {
 10 |   var browser: Browser | undefined;
 11 |   var page: Page | undefined;
 12 | }
 13 | 
 14 | // Helper function to get workspace root path
 15 | const getWorkspaceRoot = () => {
 16 |   return process.cwd();
 17 | };
 18 | 
 19 | const DEFAULT_OPTIONS: Required<CodegenOptions> = {
 20 |   outputPath: path.join(getWorkspaceRoot(), "e2e"),
 21 |   testNamePrefix: "Test",
 22 |   includeComments: true,
 23 | };
 24 | 
 25 | export const startCodegenSession: Tool = {
 26 |   name: "start_codegen_session",
 27 |   description: "Start a new code generation session to record MCP tool actions",
 28 |   parameters: {
 29 |     type: "object",
 30 |     properties: {
 31 |       options: {
 32 |         type: "object",
 33 |         description: "Code generation options",
 34 |         properties: {
 35 |           outputPath: { type: "string" },
 36 |           testNamePrefix: { type: "string" },
 37 |           includeComments: { type: "boolean" },
 38 |         },
 39 |       },
 40 |     },
 41 |   },
 42 |   handler: async ({ options = {} }: { options?: CodegenOptions }) => {
 43 |     try {
 44 |       // Merge provided options with defaults
 45 |       const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
 46 | 
 47 |       // Ensure output path is absolute and normalized
 48 |       const workspaceRoot = getWorkspaceRoot();
 49 |       const outputPath = path.isAbsolute(mergedOptions.outputPath)
 50 |         ? mergedOptions.outputPath
 51 |         : path.join(workspaceRoot, mergedOptions.outputPath);
 52 | 
 53 |       mergedOptions.outputPath = outputPath;
 54 | 
 55 |       // Ensure output directory exists
 56 |       try {
 57 |         await fs.mkdir(outputPath, { recursive: true });
 58 |       } catch (mkdirError: any) {
 59 |         throw new Error(
 60 |           `Failed to create output directory: ${mkdirError.message}`
 61 |         );
 62 |       }
 63 | 
 64 |       const sessionId = ActionRecorder.getInstance().startSession();
 65 | 
 66 |       // Store options with the session
 67 |       const recorder = ActionRecorder.getInstance();
 68 |       const session = recorder.getSession(sessionId);
 69 |       if (session) {
 70 |         session.options = mergedOptions;
 71 |       }
 72 | 
 73 |       return {
 74 |         sessionId,
 75 |         options: mergedOptions,
 76 |         message: `Started codegen session. Tests will be generated in: ${outputPath}`,
 77 |       };
 78 |     } catch (error: any) {
 79 |       throw new Error(`Failed to start codegen session: ${error.message}`);
 80 |     }
 81 |   },
 82 | };
 83 | 
 84 | export const endCodegenSession: Tool = {
 85 |   name: "end_codegen_session",
 86 |   description:
 87 |     "End the current code generation session and generate Playwright test",
 88 |   parameters: {
 89 |     type: "object",
 90 |     properties: {
 91 |       sessionId: {
 92 |         type: "string",
 93 |         description: "ID of the session to end",
 94 |       },
 95 |     },
 96 |     required: ["sessionId"],
 97 |   },
 98 |   handler: async ({ sessionId }: { sessionId: string }) => {
 99 |     try {
100 |       const recorder = ActionRecorder.getInstance();
101 |       const session = recorder.endSession(sessionId);
102 | 
103 |       if (!session) {
104 |         throw new Error(`Session ${sessionId} not found`);
105 |       }
106 | 
107 |       if (!session.options) {
108 |         throw new Error(`Session ${sessionId} has no options configured`);
109 |       }
110 | 
111 |       const generator = new PlaywrightGenerator(session.options);
112 |       const result = await generator.generateTest(session);
113 | 
114 |       // Double check output directory exists
115 |       const outputDir = path.dirname(result.filePath);
116 |       await fs.mkdir(outputDir, { recursive: true });
117 | 
118 |       // Write test file
119 |       try {
120 |         await fs.writeFile(result.filePath, result.testCode, "utf-8");
121 |       } catch (writeError: any) {
122 |         throw new Error(`Failed to write test file: ${writeError.message}`);
123 |       }
124 | 
125 |       // Close Playwright browser and cleanup
126 |       try {
127 |         if (global.browser?.isConnected()) {
128 |           await global.browser.close();
129 |         }
130 |       } catch (browserError: any) {
131 |         console.warn("Failed to close browser:", browserError.message);
132 |       } finally {
133 |         global.browser = undefined;
134 |         global.page = undefined;
135 |       }
136 | 
137 |       const absolutePath = path.resolve(result.filePath);
138 | 
139 |       return {
140 |         filePath: absolutePath,
141 |         outputDirectory: outputDir,
142 |         testCode: result.testCode,
143 |         message: `Generated test file at: ${absolutePath}\nOutput directory: ${outputDir}`,
144 |       };
145 |     } catch (error: any) {
146 |       // Ensure browser cleanup even on error
147 |       try {
148 |         if (global.browser?.isConnected()) {
149 |           await global.browser.close();
150 |         }
151 |       } catch {
152 |         // Ignore cleanup errors
153 |       } finally {
154 |         global.browser = undefined;
155 |         global.page = undefined;
156 |       }
157 | 
158 |       throw new Error(`Failed to end codegen session: ${error.message}`);
159 |     }
160 |   },
161 | };
162 | 
163 | export const getCodegenSession: Tool = {
164 |   name: "get_codegen_session",
165 |   description: "Get information about a code generation session",
166 |   parameters: {
167 |     type: "object",
168 |     properties: {
169 |       sessionId: {
170 |         type: "string",
171 |         description: "ID of the session to retrieve",
172 |       },
173 |     },
174 |     required: ["sessionId"],
175 |   },
176 |   handler: async ({ sessionId }: { sessionId: string }) => {
177 |     const session = ActionRecorder.getInstance().getSession(sessionId);
178 |     if (!session) {
179 |       throw new Error(`Session ${sessionId} not found`);
180 |     }
181 |     return session;
182 |   },
183 | };
184 | 
185 | export const clearCodegenSession: Tool = {
186 |   name: "clear_codegen_session",
187 |   description: "Clear a code generation session",
188 |   parameters: {
189 |     type: "object",
190 |     properties: {
191 |       sessionId: {
192 |         type: "string",
193 |         description: "ID of the session to clear",
194 |       },
195 |     },
196 |     required: ["sessionId"],
197 |   },
198 |   handler: async ({ sessionId }: { sessionId: string }) => {
199 |     const success = ActionRecorder.getInstance().clearSession(sessionId);
200 |     if (!success) {
201 |       throw new Error(`Session ${sessionId} not found`);
202 |     }
203 |     return { success };
204 |   },
205 | };
206 | 
207 | export const codegenTools = [
208 |   startCodegenSession,
209 |   endCodegenSession,
210 |   getCodegenSession,
211 |   clearCodegenSession,
212 | ];
213 | 
```

--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------

```
1 | <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M99 52h84v34H99z"/><path d="M23 163c-7.398 0-13.843-4.027-17.303-10A19.886 19.886 0 0 0 3 163c0 11.046 8.954 20 20 20h20v-20H23z" fill="#3ECC5F"/><path d="M112.98 57.376L183 53V43c0-11.046-8.954-20-20-20H73l-2.5-4.33c-1.112-1.925-3.889-1.925-5 0L63 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L53 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L43 23c-.022 0-.042.003-.065.003l-4.142-4.141c-1.57-1.571-4.252-.853-4.828 1.294l-1.369 5.104-5.192-1.392c-2.148-.575-4.111 1.389-3.535 3.536l1.39 5.193-5.102 1.367c-2.148.576-2.867 3.259-1.296 4.83l4.142 4.142c0 .021-.003.042-.003.064l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 53l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 63l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 73l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 83l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 93l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 103l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 113l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 123l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 133l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 143l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 153l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 163c0 11.046 8.954 20 20 20h120c11.046 0 20-8.954 20-20V83l-70.02-4.376A10.645 10.645 0 0 1 103 68c0-5.621 4.37-10.273 9.98-10.624" fill="#3ECC5F"/><path fill="#3ECC5F" d="M143 183h30v-40h-30z"/><path d="M193 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 190.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M153 123h30v-20h-30z"/><path d="M193 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 183 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M63 55.5a2.5 2.5 0 0 1-2.5-2.5c0-4.136-3.364-7.5-7.5-7.5s-7.5 3.364-7.5 7.5a2.5 2.5 0 1 1-5 0c0-6.893 5.607-12.5 12.5-12.5S65.5 46.107 65.5 53a2.5 2.5 0 0 1-2.5 2.5" fill="#000"/><path d="M103 183h60c11.046 0 20-8.954 20-20V93h-60c-11.046 0-20 8.954-20 20v70z" fill="#FFFF50"/><path d="M168.02 124h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0-49.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 19.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2M183 61.611c-.012 0-.022-.006-.034-.005-3.09.105-4.552 3.196-5.842 5.923-1.346 2.85-2.387 4.703-4.093 4.647-1.889-.068-2.969-2.202-4.113-4.46-1.314-2.594-2.814-5.536-5.963-5.426-3.046.104-4.513 2.794-5.807 5.167-1.377 2.528-2.314 4.065-4.121 3.994-1.927-.07-2.951-1.805-4.136-3.813-1.321-2.236-2.848-4.75-5.936-4.664-2.994.103-4.465 2.385-5.763 4.4-1.373 2.13-2.335 3.428-4.165 3.351-1.973-.07-2.992-1.51-4.171-3.177-1.324-1.873-2.816-3.993-5.895-3.89-2.928.1-4.399 1.97-5.696 3.618-1.232 1.564-2.194 2.802-4.229 2.724a1 1 0 0 0-.072 2c3.017.101 4.545-1.8 5.872-3.487 1.177-1.496 2.193-2.787 4.193-2.855 1.926-.082 2.829 1.115 4.195 3.045 1.297 1.834 2.769 3.914 5.731 4.021 3.103.104 4.596-2.215 5.918-4.267 1.182-1.834 2.202-3.417 4.15-3.484 1.793-.067 2.769 1.35 4.145 3.681 1.297 2.197 2.766 4.686 5.787 4.796 3.125.108 4.634-2.62 5.949-5.035 1.139-2.088 2.214-4.06 4.119-4.126 1.793-.042 2.728 1.595 4.111 4.33 1.292 2.553 2.757 5.445 5.825 5.556l.169.003c3.064 0 4.518-3.075 5.805-5.794 1.139-2.41 2.217-4.68 4.067-4.773v-2z" fill="#000"/><path fill="#3ECC5F" d="M83 183h40v-40H83z"/><path d="M143 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 140.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M83 123h40v-20H83z"/><path d="M133 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 123 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M143 41.75c-.16 0-.33-.02-.49-.05a2.52 2.52 0 0 1-.47-.14c-.15-.06-.29-.14-.431-.23-.13-.09-.259-.2-.38-.31-.109-.12-.219-.24-.309-.38s-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.16.02-.33.05-.49.03-.16.08-.31.139-.47.061-.15.141-.29.231-.43.09-.13.2-.26.309-.38.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.65-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.11.12.22.25.31.38.09.14.17.28.23.43.06.16.11.31.14.47.029.16.05.33.05.49 0 .66-.271 1.31-.73 1.77-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19m20-1.25c-.66 0-1.3-.27-1.771-.73a3.802 3.802 0 0 1-.309-.38c-.09-.14-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.66.27-1.3.729-1.77.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.66-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.459.47.73 1.11.73 1.77 0 .16-.021.33-.05.49-.03.16-.08.32-.14.47-.07.15-.14.29-.23.43-.09.13-.2.26-.31.38-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19" fill="#000"/></g></svg>
```

--------------------------------------------------------------------------------
/src/__tests__/tools/browser/advancedInteraction.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { DragTool, PressKeyTool } from '../../../tools/browser/interaction.js';
  2 | import { ToolContext } from '../../../tools/common/types.js';
  3 | import { Page, Browser, ElementHandle } from 'playwright';
  4 | import { jest } from '@jest/globals';
  5 | 
  6 | // Mock page functions
  7 | const mockWaitForSelector = jest.fn();
  8 | const mockMouseMove = jest.fn().mockImplementation(() => Promise.resolve());
  9 | const mockMouseDown = jest.fn().mockImplementation(() => Promise.resolve());
 10 | const mockMouseUp = jest.fn().mockImplementation(() => Promise.resolve());
 11 | const mockKeyboardPress = jest.fn().mockImplementation(() => Promise.resolve());
 12 | const mockFocus = jest.fn().mockImplementation(() => Promise.resolve());
 13 | const mockIsClosed = jest.fn().mockReturnValue(false);
 14 | 
 15 | // Mock element handle
 16 | const mockBoundingBox = jest.fn().mockReturnValue({ x: 10, y: 10, width: 100, height: 50 });
 17 | const mockElementHandle = {
 18 |   boundingBox: mockBoundingBox
 19 | } as unknown as ElementHandle;
 20 | 
 21 | // Wait for selector returns element handle
 22 | mockWaitForSelector.mockImplementation(() => Promise.resolve(mockElementHandle));
 23 | 
 24 | // Mock mouse
 25 | const mockMouse = {
 26 |   move: mockMouseMove,
 27 |   down: mockMouseDown,
 28 |   up: mockMouseUp
 29 | };
 30 | 
 31 | // Mock keyboard
 32 | const mockKeyboard = {
 33 |   press: mockKeyboardPress
 34 | };
 35 | 
 36 | // Mock the Page object with proper typing
 37 | const mockPage = {
 38 |   waitForSelector: mockWaitForSelector,
 39 |   mouse: mockMouse,
 40 |   keyboard: mockKeyboard,
 41 |   focus: mockFocus,
 42 |   isClosed: mockIsClosed
 43 | } as unknown as Page;
 44 | 
 45 | // Mock the browser
 46 | const mockIsConnected = jest.fn().mockReturnValue(true);
 47 | const mockBrowser = {
 48 |   isConnected: mockIsConnected
 49 | } as unknown as Browser;
 50 | 
 51 | // Mock the server
 52 | const mockServer = {
 53 |   sendMessage: jest.fn()
 54 | };
 55 | 
 56 | // Mock context
 57 | const mockContext = {
 58 |   page: mockPage,
 59 |   browser: mockBrowser,
 60 |   server: mockServer
 61 | } as ToolContext;
 62 | 
 63 | describe('Advanced Browser Interaction Tools', () => {
 64 |   let dragTool: DragTool;
 65 |   let pressKeyTool: PressKeyTool;
 66 | 
 67 |   beforeEach(() => {
 68 |     jest.clearAllMocks();
 69 |     dragTool = new DragTool(mockServer);
 70 |     pressKeyTool = new PressKeyTool(mockServer);
 71 |     // Reset browser and page mocks
 72 |     mockIsConnected.mockReturnValue(true);
 73 |     mockIsClosed.mockReturnValue(false);
 74 |   });
 75 | 
 76 |   describe('DragTool', () => {
 77 |     test('should drag an element to a target location', async () => {
 78 |       const args = {
 79 |         sourceSelector: '#source-element',
 80 |         targetSelector: '#target-element'
 81 |       };
 82 | 
 83 |       const result = await dragTool.execute(args, mockContext);
 84 | 
 85 |       expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element');
 86 |       expect(mockWaitForSelector).toHaveBeenCalledWith('#target-element');
 87 |       expect(mockBoundingBox).toHaveBeenCalledTimes(2);
 88 |       expect(mockMouseMove).toHaveBeenCalledWith(60, 35); // Source center (10+100/2, 10+50/2)
 89 |       expect(mockMouseDown).toHaveBeenCalled();
 90 |       expect(mockMouseMove).toHaveBeenCalledWith(60, 35); // Target center (same mock values)
 91 |       expect(mockMouseUp).toHaveBeenCalled();
 92 |       expect(result.isError).toBe(false);
 93 |       expect(result.content[0].text).toContain('Dragged element from');
 94 |     });
 95 | 
 96 |     test('should handle errors when element positions cannot be determined', async () => {
 97 |       const args = {
 98 |         sourceSelector: '#source-element',
 99 |         targetSelector: '#target-element'
100 |       };
101 | 
102 |       // Mock failure to get bounding box
103 |       mockBoundingBox.mockReturnValueOnce(null);
104 | 
105 |       const result = await dragTool.execute(args, mockContext);
106 | 
107 |       expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element');
108 |       expect(mockBoundingBox).toHaveBeenCalled();
109 |       expect(mockMouseMove).not.toHaveBeenCalled();
110 |       expect(result.isError).toBe(true);
111 |       expect(result.content[0].text).toContain('Could not get element positions');
112 |     });
113 | 
114 |     test('should handle drag errors', async () => {
115 |       const args = {
116 |         sourceSelector: '#source-element',
117 |         targetSelector: '#target-element'
118 |       };
119 | 
120 |       // Mock a mouse operation error
121 |       mockMouseDown.mockImplementationOnce(() => Promise.reject(new Error('Mouse operation failed')));
122 | 
123 |       const result = await dragTool.execute(args, mockContext);
124 | 
125 |       expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element');
126 |       expect(mockWaitForSelector).toHaveBeenCalledWith('#target-element');
127 |       expect(mockBoundingBox).toHaveBeenCalled();
128 |       expect(mockMouseMove).toHaveBeenCalled();
129 |       expect(mockMouseDown).toHaveBeenCalled();
130 |       expect(result.isError).toBe(true);
131 |       expect(result.content[0].text).toContain('Operation failed');
132 |     });
133 | 
134 |     test('should handle missing page', async () => {
135 |       const args = {
136 |         sourceSelector: '#source-element',
137 |         targetSelector: '#target-element'
138 |       };
139 | 
140 |       const result = await dragTool.execute(args, { server: mockServer } as ToolContext);
141 | 
142 |       expect(mockWaitForSelector).not.toHaveBeenCalled();
143 |       expect(result.isError).toBe(true);
144 |       expect(result.content[0].text).toContain('Browser page not initialized');
145 |     });
146 |   });
147 | 
148 |   describe('PressKeyTool', () => {
149 |     test('should press a keyboard key', async () => {
150 |       const args = {
151 |         key: 'Enter'
152 |       };
153 | 
154 |       const result = await pressKeyTool.execute(args, mockContext);
155 | 
156 |       expect(mockKeyboardPress).toHaveBeenCalledWith('Enter');
157 |       expect(result.isError).toBe(false);
158 |       expect(result.content[0].text).toContain('Pressed key: Enter');
159 |     });
160 | 
161 |     test('should focus an element before pressing a key if selector provided', async () => {
162 |       const args = {
163 |         key: 'Enter',
164 |         selector: '#input-field'
165 |       };
166 | 
167 |       const result = await pressKeyTool.execute(args, mockContext);
168 | 
169 |       expect(mockWaitForSelector).toHaveBeenCalledWith('#input-field');
170 |       expect(mockFocus).toHaveBeenCalledWith('#input-field');
171 |       expect(mockKeyboardPress).toHaveBeenCalledWith('Enter');
172 |       expect(result.isError).toBe(false);
173 |       expect(result.content[0].text).toContain('Pressed key: Enter');
174 |     });
175 | 
176 |     test('should handle key press errors', async () => {
177 |       const args = {
178 |         key: 'Enter'
179 |       };
180 | 
181 |       // Mock a keyboard operation error
182 |       mockKeyboardPress.mockImplementationOnce(() => Promise.reject(new Error('Keyboard operation failed')));
183 | 
184 |       const result = await pressKeyTool.execute(args, mockContext);
185 | 
186 |       expect(mockKeyboardPress).toHaveBeenCalledWith('Enter');
187 |       expect(result.isError).toBe(true);
188 |       expect(result.content[0].text).toContain('Operation failed');
189 |     });
190 | 
191 |     test('should handle missing page', async () => {
192 |       const args = {
193 |         key: 'Enter'
194 |       };
195 | 
196 |       const result = await pressKeyTool.execute(args, { server: mockServer } as ToolContext);
197 | 
198 |       expect(mockKeyboardPress).not.toHaveBeenCalled();
199 |       expect(result.isError).toBe(true);
200 |       expect(result.content[0].text).toContain('Browser page not initialized');
201 |     });
202 |   });
203 | }); 
```

--------------------------------------------------------------------------------
/src/__tests__/tools/api/requests.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { GetRequestTool, PostRequestTool, PutRequestTool, PatchRequestTool, DeleteRequestTool } from '../../../tools/api/requests.js';
  2 | import { ToolContext } from '../../../tools/common/types.js';
  3 | import { APIRequestContext } from 'playwright';
  4 | import { jest } from '@jest/globals';
  5 | 
  6 | // Mock response
  7 | const mockStatus200 = jest.fn().mockReturnValue(200);
  8 | const mockStatus201 = jest.fn().mockReturnValue(201);
  9 | const mockStatus204 = jest.fn().mockReturnValue(204);
 10 | const mockText = jest.fn().mockImplementation(() => Promise.resolve('{"success": true}'));
 11 | const mockStatusText = jest.fn().mockReturnValue('OK');
 12 | 
 13 | const mockResponse = {
 14 |   status: mockStatus200,
 15 |   statusText: mockStatusText,
 16 |   text: mockText
 17 | };
 18 | 
 19 | // Mock API context
 20 | const mockGet = jest.fn().mockImplementation(() => Promise.resolve(mockResponse));
 21 | const mockPost = jest.fn().mockImplementation(() => Promise.resolve({...mockResponse, status: mockStatus201}));
 22 | const mockPut = jest.fn().mockImplementation(() => Promise.resolve(mockResponse));
 23 | const mockPatch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse));
 24 | const mockDelete = jest.fn().mockImplementation(() => Promise.resolve({...mockResponse, status: mockStatus204}));
 25 | const mockDispose = jest.fn().mockImplementation(() => Promise.resolve());
 26 | 
 27 | const mockApiContext = {
 28 |   get: mockGet,
 29 |   post: mockPost,
 30 |   put: mockPut,
 31 |   patch: mockPatch,
 32 |   delete: mockDelete,
 33 |   dispose: mockDispose
 34 | } as unknown as APIRequestContext;
 35 | 
 36 | // Mock server
 37 | const mockServer = {
 38 |   sendMessage: jest.fn()
 39 | };
 40 | 
 41 | // Mock context
 42 | const mockContext = {
 43 |   apiContext: mockApiContext,
 44 |   server: mockServer
 45 | } as ToolContext;
 46 | 
 47 | describe('API Request Tools', () => {
 48 |   let getRequestTool: GetRequestTool;
 49 |   let postRequestTool: PostRequestTool;
 50 |   let putRequestTool: PutRequestTool;
 51 |   let patchRequestTool: PatchRequestTool;
 52 |   let deleteRequestTool: DeleteRequestTool;
 53 | 
 54 |   beforeEach(() => {
 55 |     jest.clearAllMocks();
 56 |     getRequestTool = new GetRequestTool(mockServer);
 57 |     postRequestTool = new PostRequestTool(mockServer);
 58 |     putRequestTool = new PutRequestTool(mockServer);
 59 |     patchRequestTool = new PatchRequestTool(mockServer);
 60 |     deleteRequestTool = new DeleteRequestTool(mockServer);
 61 |   });
 62 | 
 63 |   describe('GetRequestTool', () => {
 64 |     test('should make a GET request', async () => {
 65 |       const args = {
 66 |         url: 'https://api.example.com'
 67 |       };
 68 | 
 69 |       const result = await getRequestTool.execute(args, mockContext);
 70 | 
 71 |       expect(mockGet).toHaveBeenCalledWith('https://api.example.com');
 72 |       expect(result.isError).toBe(false);
 73 |       expect(result.content[0].text).toContain('GET request to');
 74 |     });
 75 | 
 76 |     test('should handle GET request errors', async () => {
 77 |       const args = {
 78 |         url: 'https://api.example.com'
 79 |       };
 80 | 
 81 |       // Mock a request error
 82 |       mockGet.mockImplementationOnce(() => Promise.reject(new Error('Request failed')));
 83 | 
 84 |       const result = await getRequestTool.execute(args, mockContext);
 85 | 
 86 |       expect(mockGet).toHaveBeenCalledWith('https://api.example.com');
 87 |       expect(result.isError).toBe(true);
 88 |       expect(result.content[0].text).toContain('API operation failed');
 89 |     });
 90 | 
 91 |     test('should handle missing API context', async () => {
 92 |       const args = {
 93 |         url: 'https://api.example.com'
 94 |       };
 95 | 
 96 |       const result = await getRequestTool.execute(args, { server: mockServer } as ToolContext);
 97 | 
 98 |       expect(mockGet).not.toHaveBeenCalled();
 99 |       expect(result.isError).toBe(true);
100 |       expect(result.content[0].text).toContain('API context not initialized');
101 |     });
102 |   });
103 | 
104 |   describe('PostRequestTool', () => {
105 |     test('should make a POST request without token', async () => {
106 |       const args = {
107 |         url: 'https://api.example.com',
108 |         value: '{"data": "test"}'
109 |       };
110 | 
111 |       const result = await postRequestTool.execute(args, mockContext);
112 | 
113 |       expect(mockPost).toHaveBeenCalledWith('https://api.example.com', { 
114 |         data: { data: "test" },
115 |         headers: {
116 |           'Content-Type': 'application/json'
117 |         }
118 |       });
119 |       expect(result.isError).toBe(false);
120 |       expect(result.content[0].text).toContain('POST request to');
121 |     });
122 | 
123 |     test('should make a POST request with Bearer token', async () => {
124 |       const args = {
125 |         url: 'https://api.example.com',
126 |         value: '{"data": "test"}',
127 |         token: 'test-token'
128 |       };
129 | 
130 |       const result = await postRequestTool.execute(args, mockContext);
131 | 
132 |       expect(mockPost).toHaveBeenCalledWith('https://api.example.com', { 
133 |         data: { data: "test" },
134 |         headers: {
135 |           'Content-Type': 'application/json',
136 |           'Authorization': 'Bearer test-token'
137 |         }
138 |       });
139 |       expect(result.isError).toBe(false);
140 |       expect(result.content[0].text).toContain('POST request to');
141 |     });
142 | 
143 |     test('should make a POST request with Bearer token and custom headers', async () => {
144 |       const args = {
145 |         url: 'https://api.example.com',
146 |         value: '{"data": "test"}',
147 |         token: 'test-token',
148 |         headers: {
149 |           'X-Custom-Header': 'custom-value'
150 |         }
151 |       };
152 | 
153 |       const result = await postRequestTool.execute(args, mockContext);
154 | 
155 |       expect(mockPost).toHaveBeenCalledWith('https://api.example.com', { 
156 |         data: { data: "test" },
157 |         headers: {
158 |           'Content-Type': 'application/json',
159 |           'Authorization': 'Bearer test-token',
160 |           'X-Custom-Header': 'custom-value'
161 |         }
162 |       });
163 |       expect(result.isError).toBe(false);
164 |       expect(result.content[0].text).toContain('POST request to');
165 |     });
166 |   });
167 | 
168 |   describe('PutRequestTool', () => {
169 |     test('should make a PUT request', async () => {
170 |       const args = {
171 |         url: 'https://api.example.com',
172 |         value: '{"data": "test"}'
173 |       };
174 | 
175 |       const result = await putRequestTool.execute(args, mockContext);
176 | 
177 |       expect(mockPut).toHaveBeenCalledWith('https://api.example.com', { data: args.value });
178 |       expect(result.isError).toBe(false);
179 |       expect(result.content[0].text).toContain('PUT request to');
180 |     });
181 |   });
182 | 
183 |   describe('PatchRequestTool', () => {
184 |     test('should make a PATCH request', async () => {
185 |       const args = {
186 |         url: 'https://api.example.com',
187 |         value: '{"data": "test"}'
188 |       };
189 | 
190 |       const result = await patchRequestTool.execute(args, mockContext);
191 | 
192 |       expect(mockPatch).toHaveBeenCalledWith('https://api.example.com', { data: args.value });
193 |       expect(result.isError).toBe(false);
194 |       expect(result.content[0].text).toContain('PATCH request to');
195 |     });
196 |   });
197 | 
198 |   describe('DeleteRequestTool', () => {
199 |     test('should make a DELETE request', async () => {
200 |       const args = {
201 |         url: 'https://api.example.com/1'
202 |       };
203 | 
204 |       const result = await deleteRequestTool.execute(args, mockContext);
205 | 
206 |       expect(mockDelete).toHaveBeenCalledWith('https://api.example.com/1');
207 |       expect(result.isError).toBe(false);
208 |       expect(result.content[0].text).toContain('DELETE request to');
209 |     });
210 |   });
211 | }); 
```

--------------------------------------------------------------------------------
/src/__tests__/codegen.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ActionRecorder } from '../tools/codegen/recorder';
  2 | import { PlaywrightGenerator } from '../tools/codegen/generator';
  3 | import { handleToolCall } from '../toolHandler';
  4 | import * as fs from 'fs/promises';
  5 | import * as path from 'path';
  6 | 
  7 | jest.mock('../toolHandler');
  8 | const mockedHandleToolCall = jest.mocked(handleToolCall);
  9 | 
 10 | // Test configuration
 11 | const TEST_CONFIG = {
 12 |   OUTPUT_DIR: path.join(__dirname, '../../tests/generated'),
 13 |   MOCK_SESSION_ID: 'test-session-123'
 14 | } as const;
 15 | 
 16 | // Response types
 17 | interface ToolResponseContent {
 18 |   [key: string]: unknown;
 19 |   type: 'text';
 20 |   text: string;
 21 | }
 22 | 
 23 | interface ToolResponse {
 24 |   [key: string]: unknown;
 25 |   content: ToolResponseContent[];
 26 |   isError: boolean;
 27 |   _meta?: Record<string, unknown>;
 28 | }
 29 | 
 30 | function createMockResponse(data: unknown): ToolResponse {
 31 |   return {
 32 |     content: [{
 33 |       type: 'text',
 34 |       text: JSON.stringify(data)
 35 |     }],
 36 |     isError: false,
 37 |     _meta: {}
 38 |   };
 39 | }
 40 | 
 41 | function parseJsonResponse<T>(response: unknown): T {
 42 |   if (!response || typeof response !== 'object' || !('content' in response)) {
 43 |     throw new Error('Invalid response format');
 44 |   }
 45 | 
 46 |   const content = (response as { content: unknown[] }).content;
 47 |   if (!Array.isArray(content) || content.length === 0) {
 48 |     throw new Error('Invalid response content');
 49 |   }
 50 | 
 51 |   const textContent = content.find(c => 
 52 |     typeof c === 'object' && 
 53 |     c !== null && 
 54 |     'type' in c && 
 55 |     (c as { type: string }).type === 'text' && 
 56 |     'text' in c && 
 57 |     typeof (c as { text: unknown }).text === 'string'
 58 |   ) as { type: 'text'; text: string } | undefined;
 59 | 
 60 |   if (!textContent?.text) {
 61 |     throw new Error('No text content found in response');
 62 |   }
 63 | 
 64 |   return JSON.parse(textContent.text) as T;
 65 | }
 66 | 
 67 | describe('Code Generation', () => {
 68 |   beforeAll(async () => {
 69 |     // Ensure test output directory exists
 70 |     await fs.mkdir(TEST_CONFIG.OUTPUT_DIR, { recursive: true });
 71 |   });
 72 | 
 73 |   afterEach(() => {
 74 |     // Clear all mocks
 75 |     jest.clearAllMocks();
 76 |   });
 77 | 
 78 |   afterAll(async () => {
 79 |     // Clean up test files
 80 |     try {
 81 |       const files = await fs.readdir(TEST_CONFIG.OUTPUT_DIR);
 82 |       await Promise.all(
 83 |         files.map(file => fs.unlink(path.join(TEST_CONFIG.OUTPUT_DIR, file)))
 84 |       );
 85 |     } catch (error) {
 86 |       console.error('Error cleaning up test files:', error);
 87 |     }
 88 |   });
 89 | 
 90 |   describe('Action Recording', () => {
 91 |     beforeEach(() => {
 92 |       // Mock session info response
 93 |       mockedHandleToolCall.mockImplementation(async (name, args, server) => {
 94 |         if (name === 'get_codegen_session') {
 95 |           return createMockResponse({
 96 |             id: TEST_CONFIG.MOCK_SESSION_ID,
 97 |             actions: []
 98 |           });
 99 |         }
100 |         return createMockResponse({ success: true });
101 |       });
102 |     });
103 | 
104 |     it('should record navigation actions', async () => {
105 |       // Setup mock for session info
106 |       mockedHandleToolCall.mockImplementation(async (name, args, server) => {
107 |         if (name === 'get_codegen_session') {
108 |           return createMockResponse({
109 |             id: TEST_CONFIG.MOCK_SESSION_ID,
110 |             actions: [{
111 |               toolName: 'playwright_navigate',
112 |               params: { url: 'https://example.com' }
113 |             }]
114 |           });
115 |         }
116 |         return createMockResponse({ success: true });
117 |       });
118 | 
119 |       await handleToolCall('playwright_navigate', {
120 |         url: 'https://example.com'
121 |       }, {});
122 | 
123 |       const sessionInfo = parseJsonResponse<{ id: string; actions: any[] }>(
124 |         await handleToolCall('get_codegen_session', { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, {})
125 |       );
126 | 
127 |       expect(sessionInfo.actions).toHaveLength(1);
128 |       expect(sessionInfo.actions[0].toolName).toBe('playwright_navigate');
129 |       expect(sessionInfo.actions[0].params).toEqual({
130 |         url: 'https://example.com'
131 |       });
132 |     });
133 | 
134 |     it('should record multiple actions in sequence', async () => {
135 |       // Setup mock for session info
136 |       mockedHandleToolCall.mockImplementation(async (name, args, server) => {
137 |         if (name === 'get_codegen_session') {
138 |           return createMockResponse({
139 |             id: TEST_CONFIG.MOCK_SESSION_ID,
140 |             actions: [
141 |               {
142 |                 toolName: 'playwright_navigate',
143 |                 params: { url: 'https://example.com' }
144 |               },
145 |               {
146 |                 toolName: 'playwright_click',
147 |                 params: { selector: '#submit-button' }
148 |               },
149 |               {
150 |                 toolName: 'playwright_fill',
151 |                 params: { selector: '#search-input', value: 'test query' }
152 |               }
153 |             ]
154 |           });
155 |         }
156 |         return createMockResponse({ success: true });
157 |       });
158 | 
159 |       await handleToolCall('playwright_navigate', {
160 |         url: 'https://example.com'
161 |       }, {});
162 | 
163 |       await handleToolCall('playwright_click', {
164 |         selector: '#submit-button'
165 |       }, {});
166 | 
167 |       await handleToolCall('playwright_fill', {
168 |         selector: '#search-input',
169 |         value: 'test query'
170 |       }, {});
171 | 
172 |       const sessionInfo = parseJsonResponse<{ id: string; actions: any[] }>(
173 |         await handleToolCall('get_codegen_session', { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, {})
174 |       );
175 | 
176 |       expect(sessionInfo.actions).toHaveLength(3);
177 |       expect(sessionInfo.actions.map(a => a.toolName)).toEqual([
178 |         'playwright_navigate',
179 |         'playwright_click',
180 |         'playwright_fill'
181 |       ]);
182 |       expect(sessionInfo.actions.map(a => a.params)).toEqual([
183 |         { url: 'https://example.com' },
184 |         { selector: '#submit-button' },
185 |         { selector: '#search-input', value: 'test query' }
186 |       ]);
187 |     });
188 |   });
189 | 
190 |   describe('Test Generation', () => {
191 |     it('should generate valid Playwright test code', async () => {
192 |       // Setup mock for end session response
193 |       mockedHandleToolCall.mockImplementation(async (name, args, server) => {
194 |         if (name === 'end_codegen_session') {
195 |           return createMockResponse({
196 |             filePath: path.join(TEST_CONFIG.OUTPUT_DIR, 'test.spec.ts'),
197 |             testCode: `
198 |               import { test, expect } from '@playwright/test';
199 |               
200 |               test('generated test', async ({ page }) => {
201 |                 await page.goto('https://example.com');
202 |                 await page.click('#submit-button');
203 |                 await page.fill('#search-input', 'test query');
204 |               });
205 |             `
206 |           });
207 |         }
208 |         return createMockResponse({ success: true });
209 |       });
210 | 
211 |       // Record actions
212 |       await handleToolCall('playwright_navigate', {
213 |         url: 'https://example.com'
214 |       }, {});
215 | 
216 |       await handleToolCall('playwright_click', {
217 |         selector: '#submit-button'
218 |       }, {});
219 | 
220 |       await handleToolCall('playwright_fill', {
221 |         selector: '#search-input',
222 |         value: 'test query'
223 |       }, {});
224 | 
225 |       // Generate test
226 |       const endResult = await handleToolCall('end_codegen_session', {
227 |         sessionId: TEST_CONFIG.MOCK_SESSION_ID
228 |       }, {});
229 | 
230 |       const { filePath, testCode } = parseJsonResponse<{ filePath: string; testCode: string }>(endResult);
231 |       
232 |       // Verify test code content
233 |       expect(filePath).toBeDefined();
234 |       expect(testCode).toContain('import { test, expect } from \'@playwright/test\'');
235 |       expect(testCode).toContain('await page.goto(\'https://example.com\')');
236 |       expect(testCode).toContain('await page.click(\'#submit-button\')');
237 |       expect(testCode).toContain('await page.fill(\'#search-input\', \'test query\')');
238 | 
239 |       // Verify mock was called correctly
240 |       expect(mockedHandleToolCall).toHaveBeenCalledWith(
241 |         'end_codegen_session',
242 |         { sessionId: TEST_CONFIG.MOCK_SESSION_ID },
243 |         {}
244 |       );
245 |     });
246 |   });
247 | }); 
```

--------------------------------------------------------------------------------
/docs/docs/playwright-web/Supported-Tools.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | sidebar_position: 1
  3 | ---
  4 | 
  5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
  6 | 
  7 | 
  8 | # 🛠️ Supported Tools
  9 | 
 10 | Playwright MCP for Browser automation has following key features
 11 | - Console log monitoring
 12 | - Code Generation
 13 | - Web Scraping
 14 | - Screenshot capabilities
 15 | - JavaScript execution
 16 | - Basic web interaction (navigation, clicking, form filling, drop down select and hover)
 17 | - Content retrieval (visible text and HTML)
 18 | 
 19 | 
 20 | <YouTubeVideoEmbed videoId="8CcgFUE16HM" />
 21 | 
 22 | ---
 23 | 
 24 | :::warning Note
 25 | Playwright UI automation is supported for very limited feature sets, more features will be added in upcoming days. Please feel free to fork the repo and add the feature and raise PR, will can build the library together!
 26 | :::
 27 | 
 28 | ## Code Generation Tools
 29 | 
 30 | These tools allow you to record and generate reusable Playwright test scripts.
 31 | 
 32 | ### start_codegen_session
 33 | Start a new code generation session to record Playwright actions.
 34 | 
 35 | - **Inputs:**
 36 |   - **`options`** *(object, required)*:  
 37 |     Code generation options:
 38 |     - **`outputPath`** *(string, required)*:  
 39 |       Directory path where generated tests will be saved (use absolute path).
 40 |     - **`testNamePrefix`** *(string, optional)*:  
 41 |       Prefix to use for generated test names (default: 'GeneratedTest').
 42 |     - **`includeComments`** *(boolean, optional)*:  
 43 |       Whether to include descriptive comments in generated tests.
 44 | 
 45 | - **Response:**
 46 |   - Session ID for the newly created code generation session.
 47 | 
 48 | ---
 49 | 
 50 | ### end_codegen_session
 51 | End a code generation session and generate the test file.
 52 | 
 53 | - **Inputs:**
 54 |   - **`sessionId`** *(string, required)*:  
 55 |     ID of the session to end.
 56 | 
 57 | - **Response:**
 58 |   - Information about the generated test file.
 59 | 
 60 | ---
 61 | 
 62 | ### get_codegen_session
 63 | Get information about a code generation session.
 64 | 
 65 | - **Inputs:**
 66 |   - **`sessionId`** *(string, required)*:  
 67 |     ID of the session to retrieve.
 68 | 
 69 | - **Response:**
 70 |   - Session information including recorded actions and status.
 71 | 
 72 | ---
 73 | 
 74 | ### clear_codegen_session
 75 | Clear a code generation session without generating a test.
 76 | 
 77 | - **Inputs:**
 78 |   - **`sessionId`** *(string, required)*:  
 79 |     ID of the session to clear.
 80 | 
 81 | - **Response:**
 82 |   - Confirmation that the session was cleared.
 83 | 
 84 | ---
 85 | 
 86 | ## Browser Automation Tools
 87 | 
 88 | ### Playwright_navigate
 89 | 
 90 | Navigate to a URL in the browser with configurable viewport and browser settings
 91 | 
 92 | - **`url`** *(string, required)*:  
 93 |   URL of the application under test.
 94 | 
 95 | - **`browserType`** *(string, optional, default: "chromium")*:  
 96 |   Browser engine to use. Supported values: "chromium", "firefox", "webkit".
 97 | 
 98 | - **`width`** *(number, optional, default: 1280)*:  
 99 |   Viewport width in pixels.
100 | 
101 | - **`height`** *(number, optional, default: 720)*:  
102 |   Viewport height in pixels.
103 | 
104 | - **`timeout`** *(number, optional)*:  
105 |   Navigation timeout in milliseconds.
106 | 
107 | - **`waitUntil`** *(string, optional)*:  
108 |   Navigation wait condition.
109 | 
110 | - **`headless`** *(boolean, optional, default: false)*:  
111 |   Run browser in headless mode.
112 | 
113 | ---
114 | 
115 | ### Playwright_screenshot
116 | 
117 | Capture screenshots of the entire page or specific elements
118 | 
119 | - **`name`** *(string, required)*:  
120 |   Name for the screenshot.
121 | 
122 | - **`selector`** *(string, optional)*:  
123 |   CSS selector for the element to screenshot.
124 | 
125 | - **`width`** *(number, optional, default: 800)*:  
126 |   Screenshot width.
127 | 
128 | - **`height`** *(number, optional, default: 600)*:  
129 |   Screenshot height.
130 | 
131 | - **`storeBase64`** *(boolean, optional, default: false)*:  
132 |   Store the screenshot as a base64 string.
133 | 
134 | - **`fullPage`** *(boolean, optional, default: false)*:
135 |   Capture a screenshot of the full page.
136 | 
137 | - **`savePng`** *(boolean, optional, default: false)*:
138 |   Save the screenshot as a PNG file.
139 | 
140 | - **`downloadsDir`** *(string, optional)*:
141 |   Directory to save the screenshot.
142 | ---
143 | 
144 | ### Playwright_click
145 | Click elements on the page.
146 | 
147 | - **`selector`** *(string)*:  
148 |   CSS selector for the element to click.
149 | 
150 | ---
151 | 
152 | ### Playwright_iframe_click
153 | Click elements in an iframe on the page.
154 | 
155 | - **`iframeSelector`** *(string)*:  
156 |   CSS selector for the iframe containing the element to click.
157 | 
158 | - **`selector`** *(string)*:  
159 |   CSS selector for the element to click.
160 | 
161 | ---
162 | 
163 | ### Playwright_hover
164 | Hover over elements on the page.
165 | 
166 | - **`selector`** *(string)*:  
167 |   CSS selector for the element to hover.
168 | 
169 | ---
170 | 
171 | ### Playwright_fill
172 | Fill out input fields.
173 | 
174 | - **`selector`** *(string)*:  
175 |   CSS selector for the input field.  
176 | - **`value`** *(string)*:  
177 |   Value to fill.
178 | 
179 | ---
180 | 
181 | ### Playwright_select
182 | Select an element with the `SELECT` tag.
183 | 
184 | - **`selector`** *(string)*:  
185 |   CSS selector for the element to select.  
186 | - **`value`** *(string)*:  
187 |   Value to select.
188 | 
189 | ---
190 | 
191 | ### Playwright_evaluate
192 | Execute JavaScript in the browser console.
193 | 
194 | - **`script`** *(string)*:  
195 |   JavaScript code to execute.
196 | 
197 | ---
198 | 
199 | ### Playwright_console_logs
200 | Retrieve console logs from the browser with filtering options
201 | Supports Retrieval of logs like - all, error, warning, log, info, debug
202 | 
203 | - **`search`** *(string)*:  
204 |   Text to search for in logs (handles text with square brackets).
205 | 
206 | - **`limit`** *(number)*:
207 |   Maximum number of logs to retrieve.
208 | 
209 | - **`type`** *(string)*:
210 |   Type of logs to retrieve (all, error, warning, log, info, debug).
211 | 
212 | - **`clear`** *(boolean)*:
213 |   Whether to clear logs after retrieval (default: false).
214 | 
215 | ---
216 | 
217 | ### Playwright_close
218 |   Close the browser and release all resources.
219 |   Useful while working with Cline, Cursor to release the resources.
220 | 
221 | ---
222 | 
223 | ### Playwright_expect_response
224 | Ask Playwright to start waiting for a HTTP response. This tool initiates the wait operation but does not wait for its completion.
225 | 
226 | - **Inputs:**
227 |   - **`id`** *(string)*:  
228 |     Unique & arbitrary identifier to be used for retrieving this response later with `Playwright_assert_response`.
229 |   - **`url`** *(string)*:  
230 |     URL pattern to match in the response.
231 | 
232 | ---
233 | 
234 | ### Playwright_assert_response
235 | Wait for and validate a previously initiated HTTP response wait operation.
236 | 
237 | - **Inputs:**
238 |   - **`id`** *(string)*:  
239 |     Identifier of the HTTP response initially expected using `Playwright_expect_response`.
240 |   - **`value`** *(string, optional)*:  
241 |     Data to expect in the body of the HTTP response. If provided, the assertion will fail if this value is not found in the response body.
242 | 
243 | - **Response:**
244 |   - **`statusCode`** *(string)*:  
245 |     Status code of the response.
246 |   - **`responseUrl`** *(string)*:  
247 |     Full URL of the captured response.
248 |   - **`responseBody`** *(string)*:  
249 |     Full response body in JSON format.
250 | 
251 | ---
252 | 
253 | ### playwright_custom_user_agent
254 | Set a custom User Agent for the browser.
255 | 
256 | - **Inputs:**
257 |   - **`userAgent`** *(string)*:  
258 |     Custom User Agent for the Playwright browser instance
259 | 
260 | ---
261 | 
262 | ### playwright_get_visible_text
263 | Get the visible text content of the current page.
264 | 
265 | - **Response:**
266 |   - **`content`** *(string)*:  
267 |     The visible text content of the current page, extracted from visible DOM elements.
268 |     Hidden elements (with display:none or visibility:hidden) are excluded.
269 | 
270 | ---
271 | 
272 | ### playwright_get_visible_html
273 | Get the HTML content of the current page.
274 | 
275 | - **Response:**
276 |   - **`content`** *(string)*:  
277 |     The complete HTML content of the current page.
278 | 
279 | ---
280 | 
281 | ### playwright_go_back
282 | Navigate back in browser history.
283 | 
284 | - **Response:**
285 |   - Confirmation message that the browser has navigated back in its history.
286 | 
287 | ---
288 | 
289 | ### playwright_go_forward
290 | Navigate forward in browser history.
291 | 
292 | - **Response:**
293 |   - Confirmation message that the browser has navigated forward in its history.
294 | 
295 | ---
296 | 
297 | ### playwright_drag
298 | Drag an element to a target location.
299 | 
300 | - **Inputs:**
301 |   - **`sourceSelector`** *(string)*:  
302 |     CSS selector for the element to drag.
303 |   - **`targetSelector`** *(string)*:  
304 |     CSS selector for the target location.
305 | 
306 | - **Response:**
307 |   - Confirmation message that the drag operation has been performed.
308 | 
309 | ---
310 | 
311 | ### playwright_press_key
312 | Press a keyboard key.
313 | 
314 | - **Inputs:**
315 |   - **`key`** *(string)*:  
316 |     Key to press (e.g. 'Enter', 'ArrowDown', 'a').
317 |   - **`selector`** *(string, optional)*:  
318 |     CSS selector for an element to focus before pressing the key.
319 | 
320 | - **Response:**
321 |   - Confirmation message indicating which key was pressed.
322 | 
323 | ---
324 | 
325 | ### playwright_save_as_pdf
326 | Save the current page as a PDF file.
327 | 
328 | - **Inputs:**
329 |   - **`outputPath`** *(string)*:  
330 |     Directory path where the PDF will be saved.
331 |   - **`filename`** *(string, optional, default: "page.pdf")*:  
332 |     Name of the PDF file.
333 |   - **`format`** *(string, optional, default: "A4")*:  
334 |     Page format (e.g. 'A4', 'Letter').
335 |   - **`printBackground`** *(boolean, optional, default: true)*:  
336 |     Whether to print background graphics.
337 |   - **`margin`** *(object, optional)*:  
338 |     Page margins with the following properties:
339 |     - **`top`** *(string)*: Top margin (e.g. '1cm').
340 |     - **`right`** *(string)*: Right margin (e.g. '1cm').
341 |     - **`bottom`** *(string)*: Bottom margin (e.g. '1cm').
342 |     - **`left`** *(string)*: Left margin (e.g. '1cm').
343 | 
344 | - **Response:**
345 |   - Path to the saved PDF file.
346 | 
```

--------------------------------------------------------------------------------
/src/__tests__/tools/browser/interaction.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ClickTool, FillTool, SelectTool, HoverTool, EvaluateTool, IframeClickTool } from '../../../tools/browser/interaction.js';
  2 | import { NavigationTool } from '../../../tools/browser/navigation.js';
  3 | import { ToolContext } from '../../../tools/common/types.js';
  4 | import { Page, Browser } from 'playwright';
  5 | import { jest } from '@jest/globals';
  6 | 
  7 | // Mock page functions
  8 | const mockPageClick = jest.fn().mockImplementation(() => Promise.resolve());
  9 | const mockPageFill = jest.fn().mockImplementation(() => Promise.resolve());
 10 | const mockPageSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
 11 | const mockPageHover = jest.fn().mockImplementation(() => Promise.resolve());
 12 | const mockPageWaitForSelector = jest.fn().mockImplementation(() => Promise.resolve());
 13 | 
 14 | // Mock locator functions
 15 | const mockLocatorClick = jest.fn().mockImplementation(() => Promise.resolve());
 16 | const mockLocatorFill = jest.fn().mockImplementation(() => Promise.resolve());
 17 | const mockLocatorSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
 18 | const mockLocatorHover = jest.fn().mockImplementation(() => Promise.resolve());
 19 | 
 20 | // Mock locator
 21 | const mockLocator = jest.fn().mockReturnValue({
 22 |   click: mockLocatorClick,
 23 |   fill: mockLocatorFill,
 24 |   selectOption: mockLocatorSelectOption,
 25 |   hover: mockLocatorHover
 26 | });
 27 | 
 28 | // Mock iframe locator
 29 | const mockIframeLocator = jest.fn().mockReturnValue({
 30 |   click: mockLocatorClick
 31 | });
 32 | 
 33 | // Mock frame locator
 34 | const mockFrameLocator = jest.fn().mockReturnValue({
 35 |   locator: mockIframeLocator
 36 | });
 37 | 
 38 | // Mock evaluate function
 39 | const mockEvaluate = jest.fn().mockImplementation(() => Promise.resolve('test-result'));
 40 | 
 41 | // Mock the Page object with proper typing
 42 | const mockGoto = jest.fn().mockImplementation(() => Promise.resolve());
 43 | const mockIsClosed = jest.fn().mockReturnValue(false);
 44 | 
 45 | const mockPage = {
 46 |   click: mockPageClick,
 47 |   fill: mockPageFill,
 48 |   selectOption: mockPageSelectOption,
 49 |   hover: mockPageHover,
 50 |   waitForSelector: mockPageWaitForSelector,
 51 |   locator: mockLocator,
 52 |   frameLocator: mockFrameLocator,
 53 |   evaluate: mockEvaluate,
 54 |   goto: mockGoto,
 55 |   isClosed: mockIsClosed
 56 | } as unknown as Page;
 57 | 
 58 | // Mock the browser
 59 | const mockIsConnected = jest.fn().mockReturnValue(true);
 60 | const mockBrowser = {
 61 |   isConnected: mockIsConnected
 62 | } as unknown as Browser;
 63 | 
 64 | // Mock the server
 65 | const mockServer = {
 66 |   sendMessage: jest.fn()
 67 | };
 68 | 
 69 | // Mock context
 70 | const mockContext = {
 71 |   page: mockPage,
 72 |   browser: mockBrowser,
 73 |   server: mockServer
 74 | } as ToolContext;
 75 | 
 76 | describe('Browser Interaction Tools', () => {
 77 |   let clickTool: ClickTool;
 78 |   let fillTool: FillTool;
 79 |   let selectTool: SelectTool;
 80 |   let hoverTool: HoverTool;
 81 |   let evaluateTool: EvaluateTool;
 82 |   let iframeClickTool: IframeClickTool;
 83 | 
 84 |   beforeEach(() => {
 85 |     jest.clearAllMocks();
 86 |     clickTool = new ClickTool(mockServer);
 87 |     fillTool = new FillTool(mockServer);
 88 |     selectTool = new SelectTool(mockServer);
 89 |     hoverTool = new HoverTool(mockServer);
 90 |     evaluateTool = new EvaluateTool(mockServer);
 91 |     iframeClickTool = new IframeClickTool(mockServer);
 92 |   });
 93 | 
 94 |   describe('ClickTool', () => {
 95 |     test('should click an element', async () => {
 96 |       const args = {
 97 |         selector: '#test-button'
 98 |       };
 99 | 
100 |       const result = await clickTool.execute(args, mockContext);
101 | 
102 |       // The actual implementation uses page.click directly, not locator
103 |       expect(mockPageClick).toHaveBeenCalledWith('#test-button');
104 |       expect(result.isError).toBe(false);
105 |       expect(result.content[0].text).toContain('Clicked element');
106 |     });
107 | 
108 |     test('should handle click errors', async () => {
109 |       const args = {
110 |         selector: '#test-button'
111 |       };
112 | 
113 |       // Mock a click error
114 |       mockPageClick.mockImplementationOnce(() => Promise.reject(new Error('Click failed')));
115 | 
116 |       const result = await clickTool.execute(args, mockContext);
117 | 
118 |       expect(mockPageClick).toHaveBeenCalledWith('#test-button');
119 |       expect(result.isError).toBe(true);
120 |       expect(result.content[0].text).toContain('Operation failed');
121 |     });
122 | 
123 |     test('should handle missing page', async () => {
124 |       const args = {
125 |         selector: '#test-button'
126 |       };
127 | 
128 |       const result = await clickTool.execute(args, { server: mockServer } as ToolContext);
129 | 
130 |       expect(mockPageClick).not.toHaveBeenCalled();
131 |       expect(result.isError).toBe(true);
132 |       expect(result.content[0].text).toContain('Browser page not initialized');
133 |     });
134 |   });
135 | 
136 |   describe('IframeClickTool', () => {
137 |     test('should click an element in an iframe', async () => {
138 |       const args = {
139 |         iframeSelector: '#test-iframe',
140 |         selector: '#test-button'
141 |       };
142 | 
143 |       const result = await iframeClickTool.execute(args, mockContext);
144 | 
145 |       expect(mockFrameLocator).toHaveBeenCalledWith('#test-iframe');
146 |       expect(mockIframeLocator).toHaveBeenCalledWith('#test-button');
147 |       expect(mockLocatorClick).toHaveBeenCalled();
148 |       expect(result.isError).toBe(false);
149 |       expect(result.content[0].text).toContain('Clicked element');
150 |     });
151 |   });
152 | 
153 |   describe('FillTool', () => {
154 |     test('should fill an input field', async () => {
155 |       const args = {
156 |         selector: '#test-input',
157 |         value: 'test value'
158 |       };
159 | 
160 |       const result = await fillTool.execute(args, mockContext);
161 | 
162 |       expect(mockPageWaitForSelector).toHaveBeenCalledWith('#test-input');
163 |       expect(mockPageFill).toHaveBeenCalledWith('#test-input', 'test value');
164 |       expect(result.isError).toBe(false);
165 |       expect(result.content[0].text).toContain('Filled');
166 |     });
167 |   });
168 | 
169 |   describe('SelectTool', () => {
170 |     test('should select an option', async () => {
171 |       const args = {
172 |         selector: '#test-select',
173 |         value: 'option1'
174 |       };
175 | 
176 |       const result = await selectTool.execute(args, mockContext);
177 | 
178 |       expect(mockPageWaitForSelector).toHaveBeenCalledWith('#test-select');
179 |       expect(mockPageSelectOption).toHaveBeenCalledWith('#test-select', 'option1');
180 |       expect(result.isError).toBe(false);
181 |       expect(result.content[0].text).toContain('Selected');
182 |     });
183 |   });
184 | 
185 |   describe('HoverTool', () => {
186 |     test('should hover over an element', async () => {
187 |       const args = {
188 |         selector: '#test-element'
189 |       };
190 | 
191 |       const result = await hoverTool.execute(args, mockContext);
192 | 
193 |       expect(mockPageWaitForSelector).toHaveBeenCalledWith('#test-element');
194 |       expect(mockPageHover).toHaveBeenCalledWith('#test-element');
195 |       expect(result.isError).toBe(false);
196 |       expect(result.content[0].text).toContain('Hovered');
197 |     });
198 |   });
199 | 
200 |   describe('EvaluateTool', () => {
201 |     test('should evaluate JavaScript', async () => {
202 |       const args = {
203 |         script: 'return document.title'
204 |       };
205 | 
206 |       const result = await evaluateTool.execute(args, mockContext);
207 | 
208 |       expect(mockEvaluate).toHaveBeenCalledWith('return document.title');
209 |       expect(result.isError).toBe(false);
210 |       expect(result.content[0].text).toContain('Executed JavaScript');
211 |     });
212 |   });
213 | });
214 | 
215 | describe('NavigationTool', () => {
216 |   let navigationTool: NavigationTool;
217 | 
218 |   beforeEach(() => {
219 |     jest.clearAllMocks();
220 |     navigationTool = new NavigationTool(mockServer);
221 |     // Reset browser and page mocks
222 |     mockIsConnected.mockReturnValue(true);
223 |     mockIsClosed.mockReturnValue(false);
224 |   });
225 | 
226 |   test('should navigate to a URL', async () => {
227 |     const args = {
228 |       url: 'https://example.com',
229 |       waitUntil: 'networkidle'
230 |     };
231 | 
232 |     const result = await navigationTool.execute(args, mockContext);
233 | 
234 |     expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 });
235 |     expect(result.isError).toBe(false);
236 |     expect(result.content[0].text).toContain('Navigated to');
237 |   });
238 | 
239 |   test('should handle navigation errors', async () => {
240 |     const args = {
241 |       url: 'https://example.com'
242 |     };
243 | 
244 |     // Mock a navigation error
245 |     mockGoto.mockImplementationOnce(() => Promise.reject(new Error('Navigation failed')));
246 | 
247 |     const result = await navigationTool.execute(args, mockContext);
248 | 
249 |     expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 });
250 |     expect(result.isError).toBe(true);
251 |     expect(result.content[0].text).toContain('Operation failed');
252 |   });
253 | 
254 |   test('should handle missing page', async () => {
255 |     const args = {
256 |       url: 'https://example.com'
257 |     };
258 | 
259 |     // Create context with no page but with browser
260 |     const contextWithoutPage = { 
261 |       server: mockServer,
262 |       browser: mockBrowser
263 |     } as unknown as ToolContext;
264 | 
265 |     const result = await navigationTool.execute(args, contextWithoutPage);
266 | 
267 |     expect(mockGoto).not.toHaveBeenCalled();
268 |     expect(result.isError).toBe(true);
269 |     expect(result.content[0].text).toContain('Page is not available');
270 |   });
271 |   
272 |   test('should handle disconnected browser', async () => {
273 |     const args = {
274 |       url: 'https://example.com'
275 |     };
276 |     
277 |     // Mock disconnected browser
278 |     mockIsConnected.mockReturnValueOnce(false);
279 |     
280 |     const result = await navigationTool.execute(args, mockContext);
281 |     
282 |     expect(mockGoto).not.toHaveBeenCalled();
283 |     expect(result.isError).toBe(true);
284 |     expect(result.content[0].text).toContain('Browser is not connected');
285 |   });
286 |   
287 |   test('should handle closed page', async () => {
288 |     const args = {
289 |       url: 'https://example.com'
290 |     };
291 |     
292 |     // Mock closed page
293 |     mockIsClosed.mockReturnValueOnce(true);
294 |     
295 |     const result = await navigationTool.execute(args, mockContext);
296 |     
297 |     expect(mockGoto).not.toHaveBeenCalled();
298 |     expect(result.isError).toBe(true);
299 |     expect(result.content[0].text).toContain('Page is not available or has been closed');
300 |   });
301 | });
```

--------------------------------------------------------------------------------
/src/__tests__/toolHandler.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { handleToolCall, getConsoleLogs, getScreenshots } from '../toolHandler.js';
  2 | import { Browser, Page, chromium, firefox, webkit } from 'playwright';
  3 | import { jest } from '@jest/globals';
  4 | 
  5 | // Mock the Playwright browser and page
  6 | jest.mock('playwright', () => {
  7 |   // Mock page functions
  8 |   const mockGoto = jest.fn().mockImplementation(() => Promise.resolve());
  9 |   const mockScreenshot = jest.fn().mockImplementation(() => Promise.resolve(Buffer.from('mock-screenshot')));
 10 |   const mockClick = jest.fn().mockImplementation(() => Promise.resolve());
 11 |   const mockFill = jest.fn().mockImplementation(() => Promise.resolve());
 12 |   const mockSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
 13 |   const mockHover = jest.fn().mockImplementation(() => Promise.resolve());
 14 |   const mockEvaluate = jest.fn().mockImplementation(() => Promise.resolve());
 15 |   const mockOn = jest.fn();
 16 |   const mockIsClosed = jest.fn().mockReturnValue(false);
 17 |   
 18 |   // Mock iframe click
 19 |   const mockIframeClick = jest.fn().mockImplementation(() => Promise.resolve());
 20 |   const mockIframeLocator = jest.fn().mockReturnValue({
 21 |     click: mockIframeClick
 22 |   });
 23 |   
 24 |   // Mock locator
 25 |   const mockLocatorClick = jest.fn().mockImplementation(() => Promise.resolve());
 26 |   const mockLocatorFill = jest.fn().mockImplementation(() => Promise.resolve());
 27 |   const mockLocatorSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
 28 |   const mockLocatorHover = jest.fn().mockImplementation(() => Promise.resolve());
 29 |   
 30 |   const mockLocator = jest.fn().mockReturnValue({
 31 |     click: mockLocatorClick,
 32 |     fill: mockLocatorFill,
 33 |     selectOption: mockLocatorSelectOption,
 34 |     hover: mockLocatorHover
 35 |   });
 36 |   
 37 |   const mockFrames = jest.fn().mockReturnValue([{
 38 |     locator: mockIframeLocator
 39 |   }]);
 40 | 
 41 |   const mockPage = {
 42 |     goto: mockGoto,
 43 |     screenshot: mockScreenshot,
 44 |     click: mockClick,
 45 |     fill: mockFill,
 46 |     selectOption: mockSelectOption,
 47 |     hover: mockHover,
 48 |     evaluate: mockEvaluate,
 49 |     on: mockOn,
 50 |     frames: mockFrames,
 51 |     locator: mockLocator,
 52 |     isClosed: mockIsClosed
 53 |   };
 54 | 
 55 |   const mockNewPage = jest.fn().mockImplementation(() => Promise.resolve(mockPage));
 56 |   const mockContexts = jest.fn().mockReturnValue([]);
 57 |   const mockContext = {
 58 |     newPage: mockNewPage
 59 |   };
 60 | 
 61 |   const mockNewContext = jest.fn().mockImplementation(() => Promise.resolve(mockContext));
 62 |   const mockClose = jest.fn().mockImplementation(() => Promise.resolve());
 63 |   const mockBrowserOn = jest.fn();
 64 |   const mockIsConnected = jest.fn().mockReturnValue(true);
 65 |   
 66 |   const mockBrowser = {
 67 |     newContext: mockNewContext,
 68 |     close: mockClose,
 69 |     on: mockBrowserOn,
 70 |     isConnected: mockIsConnected,
 71 |     contexts: mockContexts
 72 |   };
 73 | 
 74 |   // Mock API responses
 75 |   const mockStatus200 = jest.fn().mockReturnValue(200);
 76 |   const mockStatus201 = jest.fn().mockReturnValue(201);
 77 |   const mockStatus204 = jest.fn().mockReturnValue(204);
 78 |   const mockText = jest.fn().mockImplementation(() => Promise.resolve('{"success": true}'));
 79 |   const mockEmptyText = jest.fn().mockImplementation(() => Promise.resolve(''));
 80 |   const mockStatusText = jest.fn().mockReturnValue('OK');
 81 | 
 82 |   // Mock API requests
 83 |   const mockGetResponse = {
 84 |     status: mockStatus200,
 85 |     statusText: mockStatusText,
 86 |     text: mockText
 87 |   };
 88 |   
 89 |   const mockPostResponse = {
 90 |     status: mockStatus201,
 91 |     statusText: mockStatusText,
 92 |     text: mockText
 93 |   };
 94 |   
 95 |   const mockPutResponse = {
 96 |     status: mockStatus200,
 97 |     statusText: mockStatusText,
 98 |     text: mockText
 99 |   };
100 |   
101 |   const mockPatchResponse = {
102 |     status: mockStatus200,
103 |     statusText: mockStatusText,
104 |     text: mockText
105 |   };
106 |   
107 |   const mockDeleteResponse = {
108 |     status: mockStatus204,
109 |     statusText: mockStatusText,
110 |     text: mockEmptyText
111 |   };
112 |   
113 |   const mockGet = jest.fn().mockImplementation(() => Promise.resolve(mockGetResponse));
114 |   const mockPost = jest.fn().mockImplementation(() => Promise.resolve(mockPostResponse));
115 |   const mockPut = jest.fn().mockImplementation(() => Promise.resolve(mockPutResponse));
116 |   const mockPatch = jest.fn().mockImplementation(() => Promise.resolve(mockPatchResponse));
117 |   const mockDelete = jest.fn().mockImplementation(() => Promise.resolve(mockDeleteResponse));
118 |   const mockDispose = jest.fn().mockImplementation(() => Promise.resolve());
119 | 
120 |   const mockApiContext = {
121 |     get: mockGet,
122 |     post: mockPost,
123 |     put: mockPut,
124 |     patch: mockPatch,
125 |     delete: mockDelete,
126 |     dispose: mockDispose
127 |   };
128 | 
129 |   const mockLaunch = jest.fn().mockImplementation(() => Promise.resolve(mockBrowser));
130 |   const mockNewApiContext = jest.fn().mockImplementation(() => Promise.resolve(mockApiContext));
131 | 
132 |   return {
133 |     chromium: {
134 |       launch: mockLaunch
135 |     },
136 |     firefox: {
137 |       launch: mockLaunch
138 |     },
139 |     webkit: {
140 |       launch: mockLaunch
141 |     },
142 |     request: {
143 |       newContext: mockNewApiContext
144 |     },
145 |     // Use empty objects for Browser and Page types
146 |     Browser: {},
147 |     Page: {}
148 |   };
149 | });
150 | 
151 | // Mock server
152 | const mockServer = {
153 |   sendMessage: jest.fn(),
154 |   notification: jest.fn()
155 | };
156 | 
157 | // Don't try to mock the module itself - this causes TypeScript errors
158 | // Instead, we'll update our expectations to match the actual implementation
159 | 
160 | describe('Tool Handler', () => {
161 |   beforeEach(() => {
162 |     jest.clearAllMocks();
163 |   });
164 | 
165 |   test('handleToolCall should handle unknown tool', async () => {
166 |     const result = await handleToolCall('unknown_tool', {}, mockServer);
167 |     expect(result.isError).toBe(true);
168 |     expect(result.content[0].text).toContain('Unknown tool');
169 |   });
170 | 
171 |   // In the actual implementation, the tools might succeed or fail depending on how the mocks are set up
172 |   // We'll just test that they complete without throwing exceptions
173 |   
174 |   test('handleToolCall should handle browser tools', async () => {
175 |     // Test a few representative browser tools
176 |     const navigateResult = await handleToolCall('playwright_navigate', { url: 'https://example.com' }, mockServer);
177 |     expect(navigateResult).toBeDefined();
178 |     expect(navigateResult.content).toBeDefined();
179 |     
180 |     const screenshotResult = await handleToolCall('playwright_screenshot', { name: 'test-screenshot' }, mockServer);
181 |     expect(screenshotResult).toBeDefined();
182 |     expect(screenshotResult.content).toBeDefined();
183 |     
184 |     const clickResult = await handleToolCall('playwright_click', { selector: '#test-button' }, mockServer);
185 |     expect(clickResult).toBeDefined();
186 |     expect(clickResult.content).toBeDefined();
187 | 
188 |     // Test new navigation tools
189 |     const goBackResult = await handleToolCall('playwright_go_back', {}, mockServer);
190 |     expect(goBackResult).toBeDefined();
191 |     expect(goBackResult.content).toBeDefined();
192 |     
193 |     const goForwardResult = await handleToolCall('playwright_go_forward', {}, mockServer);
194 |     expect(goForwardResult).toBeDefined();
195 |     expect(goForwardResult.content).toBeDefined();
196 | 
197 |     // Test drag tool
198 |     const dragResult = await handleToolCall('playwright_drag', { 
199 |       sourceSelector: '#source-element',
200 |       targetSelector: '#target-element'
201 |     }, mockServer);
202 |     expect(dragResult).toBeDefined();
203 |     expect(dragResult.content).toBeDefined();
204 |     
205 |     // Test press key tool
206 |     const pressKeyResult = await handleToolCall('playwright_press_key', { 
207 |       key: 'Enter',
208 |       selector: '#input-field'
209 |     }, mockServer);
210 |     expect(pressKeyResult).toBeDefined();
211 |     expect(pressKeyResult.content).toBeDefined();
212 | 
213 |     // Test save as PDF tool
214 |     const saveAsPdfResult = await handleToolCall('playwright_save_as_pdf', { 
215 |       outputPath: '/downloads',
216 |       filename: 'test.pdf'
217 |     }, mockServer);
218 |     expect(saveAsPdfResult).toBeDefined();
219 |     expect(saveAsPdfResult.content).toBeDefined();
220 |   });
221 |   
222 |   test('handleToolCall should handle Firefox browser', async () => {
223 |     const navigateResult = await handleToolCall('playwright_navigate', { 
224 |       url: 'https://example.com',
225 |       browserType: 'firefox'
226 |     }, mockServer);
227 |     expect(navigateResult).toBeDefined();
228 |     expect(navigateResult.content).toBeDefined();
229 |     
230 |     // Verify browser state is reset
231 |     await handleToolCall('playwright_close', {}, mockServer);
232 |   });
233 |   
234 |   test('handleToolCall should handle WebKit browser', async () => {
235 |     const navigateResult = await handleToolCall('playwright_navigate', { 
236 |       url: 'https://example.com',
237 |       browserType: 'webkit'
238 |     }, mockServer);
239 |     expect(navigateResult).toBeDefined();
240 |     expect(navigateResult.content).toBeDefined();
241 |     
242 |     // Verify browser state is reset
243 |     await handleToolCall('playwright_close', {}, mockServer);
244 |   });
245 |   
246 |   test('handleToolCall should handle browser type switching', async () => {
247 |     // Start with default chromium
248 |     await handleToolCall('playwright_navigate', { url: 'https://example.com' }, mockServer);
249 |     
250 |     // Switch to Firefox
251 |     const firefoxResult = await handleToolCall('playwright_navigate', { 
252 |       url: 'https://firefox.com',
253 |       browserType: 'firefox'
254 |     }, mockServer);
255 |     expect(firefoxResult).toBeDefined();
256 |     expect(firefoxResult.content).toBeDefined();
257 |     
258 |     // Switch to WebKit
259 |     const webkitResult = await handleToolCall('playwright_navigate', { 
260 |       url: 'https://webkit.org',
261 |       browserType: 'webkit'
262 |     }, mockServer);
263 |     expect(webkitResult).toBeDefined();
264 |     expect(webkitResult.content).toBeDefined();
265 |     
266 |     // Clean up
267 |     await handleToolCall('playwright_close', {}, mockServer);
268 |   });
269 |   
270 |   test('handleToolCall should handle API tools', async () => {
271 |     // Test a few representative API tools
272 |     const getResult = await handleToolCall('playwright_get', { url: 'https://api.example.com' }, mockServer);
273 |     expect(getResult).toBeDefined();
274 |     expect(getResult.content).toBeDefined();
275 |     
276 |     const postResult = await handleToolCall('playwright_post', { 
277 |       url: 'https://api.example.com', 
278 |       value: '{"data": "test"}' 
279 |     }, mockServer);
280 |     expect(postResult).toBeDefined();
281 |     expect(postResult.content).toBeDefined();
282 |   });
283 | 
284 |   test('getConsoleLogs should return console logs', () => {
285 |     const logs = getConsoleLogs();
286 |     expect(Array.isArray(logs)).toBe(true);
287 |   });
288 | 
289 |   test('getScreenshots should return screenshots map', () => {
290 |     const screenshots = getScreenshots();
291 |     expect(screenshots instanceof Map).toBe(true);
292 |   });
293 | }); 
```

--------------------------------------------------------------------------------
/docs/static/img/undraw_docusaurus_tree.svg:
--------------------------------------------------------------------------------

```
 1 | <svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
 2 |   <title>Focus on What Matters</title>
 3 |   <circle cx="321" cy="321" r="321" fill="#f2f2f2" />
 4 |   <ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
 5 |   <ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
 6 |   <rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
 7 |   <path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
 8 |   <path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
 9 |   <path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
10 |   <circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
11 |   <circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
12 |   <circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
13 |   <rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
14 |   <rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
15 |   <rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
16 |   <rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
17 |   <rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
18 |   <rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
19 |   <rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
20 |   <path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
21 |   <ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
22 |   <ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
23 |   <path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
24 |   <path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
25 |   <path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
26 |   <path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
27 |   <path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
28 |   <path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
29 |   <path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
30 |   <path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
31 |   <path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
32 |   <path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
33 |   <path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
34 |   <path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
35 |   <path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
36 |   <path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
37 |   <path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
38 |   <path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
39 |   <path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
40 | </svg>
41 | 
```

--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { Tool } from "@modelcontextprotocol/sdk/types.js";
  2 | import { codegenTools } from './tools/codegen';
  3 | 
  4 | export function createToolDefinitions() {
  5 |   return [
  6 |     // Codegen tools
  7 |     {
  8 |       name: "start_codegen_session",
  9 |       description: "Start a new code generation session to record Playwright actions",
 10 |       inputSchema: {
 11 |         type: "object",
 12 |         properties: {
 13 |           options: {
 14 |             type: "object",
 15 |             description: "Code generation options",
 16 |             properties: {
 17 |               outputPath: { 
 18 |                 type: "string", 
 19 |                 description: "Directory path where generated tests will be saved (use absolute path)" 
 20 |               },
 21 |               testNamePrefix: { 
 22 |                 type: "string", 
 23 |                 description: "Prefix to use for generated test names (default: 'GeneratedTest')" 
 24 |               },
 25 |               includeComments: { 
 26 |                 type: "boolean", 
 27 |                 description: "Whether to include descriptive comments in generated tests" 
 28 |               }
 29 |             },
 30 |             required: ["outputPath"]
 31 |           }
 32 |         },
 33 |         required: ["options"]
 34 |       }
 35 |     },
 36 |     {
 37 |       name: "end_codegen_session",
 38 |       description: "End a code generation session and generate the test file",
 39 |       inputSchema: {
 40 |         type: "object",
 41 |         properties: {
 42 |           sessionId: { 
 43 |             type: "string", 
 44 |             description: "ID of the session to end" 
 45 |           }
 46 |         },
 47 |         required: ["sessionId"]
 48 |       }
 49 |     },
 50 |     {
 51 |       name: "get_codegen_session",
 52 |       description: "Get information about a code generation session",
 53 |       inputSchema: {
 54 |         type: "object",
 55 |         properties: {
 56 |           sessionId: { 
 57 |             type: "string", 
 58 |             description: "ID of the session to retrieve" 
 59 |           }
 60 |         },
 61 |         required: ["sessionId"]
 62 |       }
 63 |     },
 64 |     {
 65 |       name: "clear_codegen_session",
 66 |       description: "Clear a code generation session without generating a test",
 67 |       inputSchema: {
 68 |         type: "object",
 69 |         properties: {
 70 |           sessionId: { 
 71 |             type: "string", 
 72 |             description: "ID of the session to clear" 
 73 |           }
 74 |         },
 75 |         required: ["sessionId"]
 76 |       }
 77 |     },
 78 |     {
 79 |       name: "playwright_navigate",
 80 |       description: "Navigate to a URL",
 81 |       inputSchema: {
 82 |         type: "object",
 83 |         properties: {
 84 |           url: { type: "string", description: "URL to navigate to the website specified" },
 85 |           browserType: { type: "string", description: "Browser type to use (chromium, firefox, webkit). Defaults to chromium", enum: ["chromium", "firefox", "webkit"] },
 86 |           width: { type: "number", description: "Viewport width in pixels (default: 1280)" },
 87 |           height: { type: "number", description: "Viewport height in pixels (default: 720)" },
 88 |           timeout: { type: "number", description: "Navigation timeout in milliseconds" },
 89 |           waitUntil: { type: "string", description: "Navigation wait condition" },
 90 |           headless: { type: "boolean", description: "Run browser in headless mode (default: false)" }
 91 |         },
 92 |         required: ["url"],
 93 |       },
 94 |     },
 95 |     {
 96 |       name: "playwright_screenshot",
 97 |       description: "Take a screenshot of the current page or a specific element",
 98 |       inputSchema: {
 99 |         type: "object",
100 |         properties: {
101 |           name: { type: "string", description: "Name for the screenshot" },
102 |           selector: { type: "string", description: "CSS selector for element to screenshot" },
103 |           width: { type: "number", description: "Width in pixels (default: 800)" },
104 |           height: { type: "number", description: "Height in pixels (default: 600)" },
105 |           storeBase64: { type: "boolean", description: "Store screenshot in base64 format (default: true)" },
106 |           fullPage: { type: "boolean", description: "Store screenshot of the entire page (default: false)" },
107 |           savePng: { type: "boolean", description: "Save screenshot as PNG file (default: false)" },
108 |           downloadsDir: { type: "string", description: "Custom downloads directory path (default: user's Downloads folder)" },
109 |         },
110 |         required: ["name"],
111 |       },
112 |     },
113 |     {
114 |       name: "playwright_click",
115 |       description: "Click an element on the page",
116 |       inputSchema: {
117 |         type: "object",
118 |         properties: {
119 |           selector: { type: "string", description: "CSS selector for the element to click" },
120 |         },
121 |         required: ["selector"],
122 |       },
123 |     },
124 |     {
125 |       name: "playwright_iframe_click",
126 |       description: "Click an element in an iframe on the page",
127 |       inputSchema: {
128 |         type: "object",
129 |         properties: {
130 |           iframeSelector: { type: "string", description: "CSS selector for the iframe containing the element to click" },
131 |           selector: { type: "string", description: "CSS selector for the element to click" },
132 |         },
133 |         required: ["iframeSelector", "selector"],
134 |       },
135 |     },
136 |     {
137 |       name: "playwright_fill",
138 |       description: "fill out an input field",
139 |       inputSchema: {
140 |         type: "object",
141 |         properties: {
142 |           selector: { type: "string", description: "CSS selector for input field" },
143 |           value: { type: "string", description: "Value to fill" },
144 |         },
145 |         required: ["selector", "value"],
146 |       },
147 |     },
148 |     {
149 |       name: "playwright_select",
150 |       description: "Select an element on the page with Select tag",
151 |       inputSchema: {
152 |         type: "object",
153 |         properties: {
154 |           selector: { type: "string", description: "CSS selector for element to select" },
155 |           value: { type: "string", description: "Value to select" },
156 |         },
157 |         required: ["selector", "value"],
158 |       },
159 |     },
160 |     {
161 |       name: "playwright_hover",
162 |       description: "Hover an element on the page",
163 |       inputSchema: {
164 |         type: "object",
165 |         properties: {
166 |           selector: { type: "string", description: "CSS selector for element to hover" },
167 |         },
168 |         required: ["selector"],
169 |       },
170 |     },
171 |     {
172 |       name: "playwright_evaluate",
173 |       description: "Execute JavaScript in the browser console",
174 |       inputSchema: {
175 |         type: "object",
176 |         properties: {
177 |           script: { type: "string", description: "JavaScript code to execute" },
178 |         },
179 |         required: ["script"],
180 |       },
181 |     },
182 |     {
183 |       name: "playwright_console_logs",
184 |       description: "Retrieve console logs from the browser with filtering options",
185 |       inputSchema: {
186 |         type: "object",
187 |         properties: {
188 |           type: {
189 |             type: "string",
190 |             description: "Type of logs to retrieve (all, error, warning, log, info, debug)",
191 |             enum: ["all", "error", "warning", "log", "info", "debug"]
192 |           },
193 |           search: {
194 |             type: "string",
195 |             description: "Text to search for in logs (handles text with square brackets)"
196 |           },
197 |           limit: {
198 |             type: "number",
199 |             description: "Maximum number of logs to return"
200 |           },
201 |           clear: {
202 |             type: "boolean",
203 |             description: "Whether to clear logs after retrieval (default: false)"
204 |           }
205 |         },
206 |         required: [],
207 |       },
208 |     },
209 |     {
210 |       name: "playwright_close",
211 |       description: "Close the browser and release all resources",
212 |       inputSchema: {
213 |         type: "object",
214 |         properties: {},
215 |         required: [],
216 |       },
217 |     },
218 |     {
219 |       name: "playwright_get",
220 |       description: "Perform an HTTP GET request",
221 |       inputSchema: {
222 |         type: "object",
223 |         properties: {
224 |           url: { type: "string", description: "URL to perform GET operation" }
225 |         },
226 |         required: ["url"],
227 |       },
228 |     },
229 |     {
230 |       name: "playwright_post",
231 |       description: "Perform an HTTP POST request",
232 |       inputSchema: {
233 |         type: "object",
234 |         properties: {
235 |           url: { type: "string", description: "URL to perform POST operation" },
236 |           value: { type: "string", description: "Data to post in the body" },
237 |           token: { type: "string", description: "Bearer token for authorization" },
238 |           headers: { 
239 |             type: "object", 
240 |             description: "Additional headers to include in the request",
241 |             additionalProperties: { type: "string" }
242 |           }
243 |         },
244 |         required: ["url", "value"],
245 |       },
246 |     },
247 |     {
248 |       name: "playwright_put",
249 |       description: "Perform an HTTP PUT request",
250 |       inputSchema: {
251 |         type: "object",
252 |         properties: {
253 |           url: { type: "string", description: "URL to perform PUT operation" },
254 |           value: { type: "string", description: "Data to PUT in the body" },
255 |         },
256 |         required: ["url", "value"],
257 |       },
258 |     },
259 |     {
260 |       name: "playwright_patch",
261 |       description: "Perform an HTTP PATCH request",
262 |       inputSchema: {
263 |         type: "object",
264 |         properties: {
265 |           url: { type: "string", description: "URL to perform PUT operation" },
266 |           value: { type: "string", description: "Data to PATCH in the body" },
267 |         },
268 |         required: ["url", "value"],
269 |       },
270 |     },
271 |     {
272 |       name: "playwright_delete",
273 |       description: "Perform an HTTP DELETE request",
274 |       inputSchema: {
275 |         type: "object",
276 |         properties: {
277 |           url: { type: "string", description: "URL to perform DELETE operation" }
278 |         },
279 |         required: ["url"],
280 |       },
281 |     },
282 |     {
283 |       name: "playwright_expect_response",
284 |       description: "Ask Playwright to start waiting for a HTTP response. This tool initiates the wait operation but does not wait for its completion.",
285 |       inputSchema: {
286 |         type: "object",
287 |         properties: {
288 |           id: { type: "string", description: "Unique & arbitrary identifier to be used for retrieving this response later with `Playwright_assert_response`." },
289 |           url: { type: "string", description: "URL pattern to match in the response." }
290 |         },
291 |         required: ["id", "url"],
292 |       },
293 |     },
294 |     {
295 |       name: "playwright_assert_response",
296 |       description: "Wait for and validate a previously initiated HTTP response wait operation.",
297 |       inputSchema: {
298 |         type: "object",
299 |         properties: {
300 |           id: { type: "string", description: "Identifier of the HTTP response initially expected using `Playwright_expect_response`." },
301 |           value: { type: "string", description: "Data to expect in the body of the HTTP response. If provided, the assertion will fail if this value is not found in the response body." }
302 |         },
303 |         required: ["id"],
304 |       },
305 |     },
306 |     {
307 |       name: "playwright_custom_user_agent",
308 |       description: "Set a custom User Agent for the browser",
309 |       inputSchema: {
310 |         type: "object",
311 |         properties: {
312 |           userAgent: { type: "string", description: "Custom User Agent for the Playwright browser instance" }
313 |         },
314 |         required: ["userAgent"],
315 |       },
316 |     },
317 |     {
318 |       name: "playwright_get_visible_text",
319 |       description: "Get the visible text content of the current page",
320 |       inputSchema: {
321 |         type: "object",
322 |         properties: {},
323 |         required: [],
324 |       },
325 |     },
326 |     {
327 |       name: "playwright_get_visible_html",
328 |       description: "Get the HTML content of the current page",
329 |       inputSchema: {
330 |         type: "object",
331 |         properties: {},
332 |         required: [],
333 |       },
334 |     },
335 |     {
336 |       name: "playwright_go_back",
337 |       description: "Navigate back in browser history",
338 |       inputSchema: {
339 |         type: "object",
340 |         properties: {},
341 |         required: [],
342 |       },
343 |     },
344 |     {
345 |       name: "playwright_go_forward",
346 |       description: "Navigate forward in browser history",
347 |       inputSchema: {
348 |         type: "object",
349 |         properties: {},
350 |         required: [],
351 |       },
352 |     },
353 |     {
354 |       name: "playwright_drag",
355 |       description: "Drag an element to a target location",
356 |       inputSchema: {
357 |         type: "object",
358 |         properties: {
359 |           sourceSelector: { type: "string", description: "CSS selector for the element to drag" },
360 |           targetSelector: { type: "string", description: "CSS selector for the target location" }
361 |         },
362 |         required: ["sourceSelector", "targetSelector"],
363 |       },
364 |     },
365 |     {
366 |       name: "playwright_press_key",
367 |       description: "Press a keyboard key",
368 |       inputSchema: {
369 |         type: "object",
370 |         properties: {
371 |           key: { type: "string", description: "Key to press (e.g. 'Enter', 'ArrowDown', 'a')" },
372 |           selector: { type: "string", description: "Optional CSS selector to focus before pressing key" }
373 |         },
374 |         required: ["key"],
375 |       },
376 |     },
377 |     {
378 |       name: "playwright_save_as_pdf",
379 |       description: "Save the current page as a PDF file",
380 |       inputSchema: {
381 |         type: "object",
382 |         properties: {
383 |           outputPath: { type: "string", description: "Directory path where PDF will be saved" },
384 |           filename: { type: "string", description: "Name of the PDF file (default: page.pdf)" },
385 |           format: { type: "string", description: "Page format (e.g. 'A4', 'Letter')" },
386 |           printBackground: { type: "boolean", description: "Whether to print background graphics" },
387 |           margin: {
388 |             type: "object",
389 |             description: "Page margins",
390 |             properties: {
391 |               top: { type: "string" },
392 |               right: { type: "string" },
393 |               bottom: { type: "string" },
394 |               left: { type: "string" }
395 |             }
396 |           }
397 |         },
398 |         required: ["outputPath"],
399 |       },
400 |     },
401 |   ] as const satisfies Tool[];
402 | }
403 | 
404 | // Browser-requiring tools for conditional browser launch
405 | export const BROWSER_TOOLS = [
406 |   "playwright_navigate",
407 |   "playwright_screenshot",
408 |   "playwright_click",
409 |   "playwright_iframe_click",
410 |   "playwright_fill",
411 |   "playwright_select",
412 |   "playwright_hover",
413 |   "playwright_evaluate",
414 |   "playwright_close",
415 |   "playwright_expect_response",
416 |   "playwright_assert_response",
417 |   "playwright_custom_user_agent",
418 |   "playwright_get_visible_text",
419 |   "playwright_get_visible_html",
420 |   "playwright_go_back",
421 |   "playwright_go_forward",
422 |   "playwright_drag",
423 |   "playwright_press_key",
424 |   "playwright_save_as_pdf"
425 | ];
426 | 
427 | // API Request tools for conditional launch
428 | export const API_TOOLS = [
429 |   "playwright_get",
430 |   "playwright_post",
431 |   "playwright_put",
432 |   "playwright_delete",
433 |   "playwright_patch"
434 | ];
435 | 
436 | // Codegen tools
437 | export const CODEGEN_TOOLS = [
438 |   'start_codegen_session',
439 |   'end_codegen_session',
440 |   'get_codegen_session',
441 |   'clear_codegen_session'
442 | ];
443 | 
444 | // All available tools
445 | export const tools = [
446 |   ...BROWSER_TOOLS,
447 |   ...API_TOOLS,
448 |   ...CODEGEN_TOOLS
449 | ];
```

--------------------------------------------------------------------------------
/src/toolHandler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { Browser, Page } from "rebrowser-playwright";
  2 | import { chromium, firefox, webkit, request } from "rebrowser-playwright";
  3 | import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
  4 | import { BROWSER_TOOLS, API_TOOLS } from "./tools.js";
  5 | import type { ToolContext } from "./tools/common/types.js";
  6 | import { ActionRecorder } from "./tools/codegen/recorder.js";
  7 | import {
  8 |   startCodegenSession,
  9 |   endCodegenSession,
 10 |   getCodegenSession,
 11 |   clearCodegenSession,
 12 | } from "./tools/codegen/index.js";
 13 | import {
 14 |   ScreenshotTool,
 15 |   NavigationTool,
 16 |   CloseBrowserTool,
 17 |   ConsoleLogsTool,
 18 |   ExpectResponseTool,
 19 |   AssertResponseTool,
 20 |   CustomUserAgentTool,
 21 | } from "./tools/browser/index.js";
 22 | import {
 23 |   ClickTool,
 24 |   IframeClickTool,
 25 |   FillTool,
 26 |   SelectTool,
 27 |   HoverTool,
 28 |   EvaluateTool,
 29 | } from "./tools/browser/interaction.js";
 30 | import {
 31 |   VisibleTextTool,
 32 |   VisibleHtmlTool,
 33 | } from "./tools/browser/visiblePage.js";
 34 | import {
 35 |   GetRequestTool,
 36 |   PostRequestTool,
 37 |   PutRequestTool,
 38 |   PatchRequestTool,
 39 |   DeleteRequestTool,
 40 | } from "./tools/api/requests.js";
 41 | import { GoBackTool, GoForwardTool } from "./tools/browser/navigation.js";
 42 | import { DragTool, PressKeyTool } from "./tools/browser/interaction.js";
 43 | import { SaveAsPdfTool } from "./tools/browser/output.js";
 44 | 
 45 | // Global state
 46 | let browser: Browser | undefined;
 47 | let page: Page | undefined;
 48 | let currentBrowserType: "chromium" | "firefox" | "webkit" = "chromium";
 49 | 
 50 | /**
 51 |  * Resets browser and page variables
 52 |  * Used when browser is closed
 53 |  */
 54 | export function resetBrowserState() {
 55 |   browser = undefined;
 56 |   page = undefined;
 57 |   currentBrowserType = "chromium";
 58 | }
 59 | 
 60 | // Tool instances
 61 | let screenshotTool: ScreenshotTool;
 62 | let navigationTool: NavigationTool;
 63 | let closeBrowserTool: CloseBrowserTool;
 64 | let consoleLogsTool: ConsoleLogsTool;
 65 | let clickTool: ClickTool;
 66 | let iframeClickTool: IframeClickTool;
 67 | let fillTool: FillTool;
 68 | let selectTool: SelectTool;
 69 | let hoverTool: HoverTool;
 70 | let evaluateTool: EvaluateTool;
 71 | let expectResponseTool: ExpectResponseTool;
 72 | let assertResponseTool: AssertResponseTool;
 73 | let customUserAgentTool: CustomUserAgentTool;
 74 | let visibleTextTool: VisibleTextTool;
 75 | let visibleHtmlTool: VisibleHtmlTool;
 76 | 
 77 | let getRequestTool: GetRequestTool;
 78 | let postRequestTool: PostRequestTool;
 79 | let putRequestTool: PutRequestTool;
 80 | let patchRequestTool: PatchRequestTool;
 81 | let deleteRequestTool: DeleteRequestTool;
 82 | 
 83 | // Add these variables at the top with other tool declarations
 84 | let goBackTool: GoBackTool;
 85 | let goForwardTool: GoForwardTool;
 86 | let dragTool: DragTool;
 87 | let pressKeyTool: PressKeyTool;
 88 | let saveAsPdfTool: SaveAsPdfTool;
 89 | 
 90 | interface BrowserSettings {
 91 |   viewport?: {
 92 |     width?: number;
 93 |     height?: number;
 94 |   };
 95 |   userAgent?: string;
 96 |   headless?: boolean;
 97 |   browserType?: "chromium" | "firefox" | "webkit";
 98 | }
 99 | 
100 | /**
101 |  * Ensures a browser is launched and returns the page
102 |  */
103 | async function ensureBrowser(browserSettings?: BrowserSettings) {
104 |   try {
105 |     // Check if browser exists but is disconnected
106 |     if (browser && !browser.isConnected()) {
107 |       console.error("Browser exists but is disconnected. Cleaning up...");
108 |       try {
109 |         await browser
110 |           .close()
111 |           .catch((err) =>
112 |             console.error("Error closing disconnected browser:", err)
113 |           );
114 |       } catch (e) {
115 |         // Ignore errors when closing disconnected browser
116 |       }
117 |       // Reset browser and page references
118 |       resetBrowserState();
119 |     }
120 | 
121 |     // Launch new browser if needed
122 |     if (!browser) {
123 |       const {
124 |         viewport,
125 |         userAgent,
126 |         headless = false,
127 |         browserType = "chromium",
128 |       } = browserSettings ?? {};
129 | 
130 |       // If browser type is changing, force a new browser instance
131 |       if (browser && currentBrowserType !== browserType) {
132 |         try {
133 |           await browser
134 |             .close()
135 |             .catch((err) =>
136 |               console.error("Error closing browser on type change:", err)
137 |             );
138 |         } catch (e) {
139 |           // Ignore errors
140 |         }
141 |         resetBrowserState();
142 |       }
143 | 
144 |       console.error(`Launching new ${browserType} browser instance...`);
145 | 
146 |       // Use the appropriate browser engine
147 |       let browserInstance;
148 |       switch (browserType) {
149 |         case "firefox":
150 |           browserInstance = firefox;
151 |           break;
152 |         case "webkit":
153 |           browserInstance = webkit;
154 |           break;
155 |         case "chromium":
156 |         default:
157 |           browserInstance = chromium;
158 |           break;
159 |       }
160 | 
161 |       browser = await browserInstance.launch({ headless });
162 |       currentBrowserType = browserType;
163 | 
164 |       // Add cleanup logic when browser is disconnected
165 |       browser.on("disconnected", () => {
166 |         console.error("Browser disconnected event triggered");
167 |         browser = undefined;
168 |         page = undefined;
169 |       });
170 | 
171 |       const context = await browser.newContext({
172 |         ...(userAgent && { userAgent }),
173 |         viewport: {
174 |           width: viewport?.width ?? 1280,
175 |           height: viewport?.height ?? 720,
176 |         },
177 |         deviceScaleFactor: 1,
178 |       });
179 | 
180 |       page = await context.newPage();
181 | 
182 |       // Register console message handler
183 |       page.on("console", (msg) => {
184 |         if (consoleLogsTool) {
185 |           consoleLogsTool.registerConsoleMessage(msg.type(), msg.text());
186 |         }
187 |       });
188 |     }
189 | 
190 |     // Verify page is still valid
191 |     if (!page || page.isClosed()) {
192 |       console.error("Page is closed or invalid. Creating new page...");
193 |       // Create a new page if the current one is invalid
194 |       const context = browser.contexts()[0] || (await browser.newContext());
195 |       page = await context.newPage();
196 | 
197 |       // Re-register console message handler
198 |       page.on("console", (msg) => {
199 |         if (consoleLogsTool) {
200 |           consoleLogsTool.registerConsoleMessage(msg.type(), msg.text());
201 |         }
202 |       });
203 |     }
204 | 
205 |     return page!;
206 |   } catch (error) {
207 |     console.error("Error ensuring browser:", error);
208 |     // If something went wrong, clean up completely and retry once
209 |     try {
210 |       if (browser) {
211 |         await browser.close().catch(() => {});
212 |       }
213 |     } catch (e) {
214 |       // Ignore errors during cleanup
215 |     }
216 | 
217 |     resetBrowserState();
218 | 
219 |     // Try one more time from scratch
220 |     const {
221 |       viewport,
222 |       userAgent,
223 |       headless = false,
224 |       browserType = "chromium",
225 |     } = browserSettings ?? {};
226 | 
227 |     // Use the appropriate browser engine
228 |     let browserInstance;
229 |     switch (browserType) {
230 |       case "firefox":
231 |         browserInstance = firefox;
232 |         break;
233 |       case "webkit":
234 |         browserInstance = webkit;
235 |         break;
236 |       case "chromium":
237 |       default:
238 |         browserInstance = chromium;
239 |         break;
240 |     }
241 | 
242 |     browser = await browserInstance.launch({ headless });
243 |     currentBrowserType = browserType;
244 | 
245 |     browser.on("disconnected", () => {
246 |       console.error("Browser disconnected event triggered (retry)");
247 |       browser = undefined;
248 |       page = undefined;
249 |     });
250 | 
251 |     const context = await browser.newContext({
252 |       ...(userAgent && { userAgent }),
253 |       viewport: {
254 |         width: viewport?.width ?? 1280,
255 |         height: viewport?.height ?? 720,
256 |       },
257 |       deviceScaleFactor: 1,
258 |     });
259 | 
260 |     page = await context.newPage();
261 | 
262 |     page.on("console", (msg) => {
263 |       if (consoleLogsTool) {
264 |         consoleLogsTool.registerConsoleMessage(msg.type(), msg.text());
265 |       }
266 |     });
267 | 
268 |     return page!;
269 |   }
270 | }
271 | 
272 | /**
273 |  * Creates a new API request context
274 |  */
275 | async function ensureApiContext(url: string) {
276 |   return await request.newContext({
277 |     baseURL: url,
278 |   });
279 | }
280 | 
281 | /**
282 |  * Initialize all tool instances
283 |  */
284 | function initializeTools(server: any) {
285 |   // Browser tools
286 |   if (!screenshotTool) screenshotTool = new ScreenshotTool(server);
287 |   if (!navigationTool) navigationTool = new NavigationTool(server);
288 |   if (!closeBrowserTool) closeBrowserTool = new CloseBrowserTool(server);
289 |   if (!consoleLogsTool) consoleLogsTool = new ConsoleLogsTool(server);
290 |   if (!clickTool) clickTool = new ClickTool(server);
291 |   if (!iframeClickTool) iframeClickTool = new IframeClickTool(server);
292 |   if (!fillTool) fillTool = new FillTool(server);
293 |   if (!selectTool) selectTool = new SelectTool(server);
294 |   if (!hoverTool) hoverTool = new HoverTool(server);
295 |   if (!evaluateTool) evaluateTool = new EvaluateTool(server);
296 |   if (!expectResponseTool) expectResponseTool = new ExpectResponseTool(server);
297 |   if (!assertResponseTool) assertResponseTool = new AssertResponseTool(server);
298 |   if (!customUserAgentTool)
299 |     customUserAgentTool = new CustomUserAgentTool(server);
300 |   if (!visibleTextTool) visibleTextTool = new VisibleTextTool(server);
301 |   if (!visibleHtmlTool) visibleHtmlTool = new VisibleHtmlTool(server);
302 | 
303 |   // API tools
304 |   if (!getRequestTool) getRequestTool = new GetRequestTool(server);
305 |   if (!postRequestTool) postRequestTool = new PostRequestTool(server);
306 |   if (!putRequestTool) putRequestTool = new PutRequestTool(server);
307 |   if (!patchRequestTool) patchRequestTool = new PatchRequestTool(server);
308 |   if (!deleteRequestTool) deleteRequestTool = new DeleteRequestTool(server);
309 | 
310 |   // Initialize new tools
311 |   if (!goBackTool) goBackTool = new GoBackTool(server);
312 |   if (!goForwardTool) goForwardTool = new GoForwardTool(server);
313 |   if (!dragTool) dragTool = new DragTool(server);
314 |   if (!pressKeyTool) pressKeyTool = new PressKeyTool(server);
315 |   if (!saveAsPdfTool) saveAsPdfTool = new SaveAsPdfTool(server);
316 | }
317 | 
318 | /**
319 |  * Main handler for tool calls
320 |  */
321 | export async function handleToolCall(
322 |   name: string,
323 |   args: any,
324 |   server: any
325 | ): Promise<CallToolResult> {
326 |   // Initialize tools
327 |   initializeTools(server);
328 | 
329 |   try {
330 |     // Handle codegen tools
331 |     switch (name) {
332 |       case "start_codegen_session":
333 |         return await handleCodegenResult(startCodegenSession.handler(args));
334 |       case "end_codegen_session":
335 |         return await handleCodegenResult(endCodegenSession.handler(args));
336 |       case "get_codegen_session":
337 |         return await handleCodegenResult(getCodegenSession.handler(args));
338 |       case "clear_codegen_session":
339 |         return await handleCodegenResult(clearCodegenSession.handler(args));
340 |     }
341 | 
342 |     // Record tool action if there's an active session
343 |     const recorder = ActionRecorder.getInstance();
344 |     const activeSession = recorder.getActiveSession();
345 |     if (activeSession && name !== "playwright_close") {
346 |       recorder.recordAction(name, args);
347 |     }
348 | 
349 |     // Special case for browser close to ensure it always works
350 |     if (name === "playwright_close") {
351 |       if (browser) {
352 |         try {
353 |           if (browser.isConnected()) {
354 |             await browser
355 |               .close()
356 |               .catch((e) => console.error("Error closing browser:", e));
357 |           }
358 |         } catch (error) {
359 |           console.error("Error during browser close in handler:", error);
360 |         } finally {
361 |           resetBrowserState();
362 |         }
363 |         return {
364 |           content: [
365 |             {
366 |               type: "text",
367 |               text: "Browser closed successfully",
368 |             },
369 |           ],
370 |           isError: false,
371 |         };
372 |       }
373 |       return {
374 |         content: [
375 |           {
376 |             type: "text",
377 |             text: "No browser instance to close",
378 |           },
379 |         ],
380 |         isError: false,
381 |       };
382 |     }
383 | 
384 |     // Check if we have a disconnected browser that needs cleanup
385 |     if (browser && !browser.isConnected() && BROWSER_TOOLS.includes(name)) {
386 |       console.error(
387 |         "Detected disconnected browser before tool execution, cleaning up..."
388 |       );
389 |       try {
390 |         await browser.close().catch(() => {}); // Ignore errors
391 |       } catch (e) {
392 |         // Ignore any errors during cleanup
393 |       }
394 |       resetBrowserState();
395 |     }
396 | 
397 |     // Prepare context based on tool requirements
398 |     const context: ToolContext = {
399 |       server,
400 |     };
401 | 
402 |     // Set up browser if needed
403 |     if (BROWSER_TOOLS.includes(name)) {
404 |       const browserSettings = {
405 |         viewport: {
406 |           width: args.width,
407 |           height: args.height,
408 |         },
409 |         userAgent:
410 |           name === "playwright_custom_user_agent" ? args.userAgent : undefined,
411 |         headless: args.headless,
412 |         browserType: args.browserType || "chromium",
413 |       };
414 | 
415 |       try {
416 |         context.page = await ensureBrowser(browserSettings);
417 |         context.browser = browser;
418 |       } catch (error) {
419 |         console.error("Failed to ensure browser:", error);
420 |         return {
421 |           content: [
422 |             {
423 |               type: "text",
424 |               text: `Failed to initialize browser: ${
425 |                 (error as Error).message
426 |               }. Please try again.`,
427 |             },
428 |           ],
429 |           isError: true,
430 |         };
431 |       }
432 |     }
433 | 
434 |     // Set up API context if needed
435 |     if (API_TOOLS.includes(name)) {
436 |       try {
437 |         context.apiContext = await ensureApiContext(args.url);
438 |       } catch (error) {
439 |         return {
440 |           content: [
441 |             {
442 |               type: "text",
443 |               text: `Failed to initialize API context: ${
444 |                 (error as Error).message
445 |               }`,
446 |             },
447 |           ],
448 |           isError: true,
449 |         };
450 |       }
451 |     }
452 | 
453 |     // Route to appropriate tool
454 |     switch (name) {
455 |       // Browser tools
456 |       case "playwright_navigate":
457 |         return await navigationTool.execute(args, context);
458 | 
459 |       case "playwright_screenshot":
460 |         return await screenshotTool.execute(args, context);
461 | 
462 |       case "playwright_close":
463 |         return await closeBrowserTool.execute(args, context);
464 | 
465 |       case "playwright_console_logs":
466 |         return await consoleLogsTool.execute(args, context);
467 | 
468 |       case "playwright_click":
469 |         return await clickTool.execute(args, context);
470 | 
471 |       case "playwright_iframe_click":
472 |         return await iframeClickTool.execute(args, context);
473 | 
474 |       case "playwright_fill":
475 |         return await fillTool.execute(args, context);
476 | 
477 |       case "playwright_select":
478 |         return await selectTool.execute(args, context);
479 | 
480 |       case "playwright_hover":
481 |         return await hoverTool.execute(args, context);
482 | 
483 |       case "playwright_evaluate":
484 |         return await evaluateTool.execute(args, context);
485 | 
486 |       case "playwright_expect_response":
487 |         return await expectResponseTool.execute(args, context);
488 | 
489 |       case "playwright_assert_response":
490 |         return await assertResponseTool.execute(args, context);
491 | 
492 |       case "playwright_custom_user_agent":
493 |         return await customUserAgentTool.execute(args, context);
494 | 
495 |       case "playwright_get_visible_text":
496 |         return await visibleTextTool.execute(args, context);
497 | 
498 |       case "playwright_get_visible_html":
499 |         return await visibleHtmlTool.execute(args, context);
500 | 
501 |       // API tools
502 |       case "playwright_get":
503 |         return await getRequestTool.execute(args, context);
504 | 
505 |       case "playwright_post":
506 |         return await postRequestTool.execute(args, context);
507 | 
508 |       case "playwright_put":
509 |         return await putRequestTool.execute(args, context);
510 | 
511 |       case "playwright_patch":
512 |         return await patchRequestTool.execute(args, context);
513 | 
514 |       case "playwright_delete":
515 |         return await deleteRequestTool.execute(args, context);
516 | 
517 |       // New tools
518 |       case "playwright_go_back":
519 |         return await goBackTool.execute(args, context);
520 |       case "playwright_go_forward":
521 |         return await goForwardTool.execute(args, context);
522 |       case "playwright_drag":
523 |         return await dragTool.execute(args, context);
524 |       case "playwright_press_key":
525 |         return await pressKeyTool.execute(args, context);
526 |       case "playwright_save_as_pdf":
527 |         return await saveAsPdfTool.execute(args, context);
528 | 
529 |       default:
530 |         return {
531 |           content: [
532 |             {
533 |               type: "text",
534 |               text: `Unknown tool: ${name}`,
535 |             },
536 |           ],
537 |           isError: true,
538 |         };
539 |     }
540 |   } catch (error) {
541 |     console.error(`Error handling tool ${name}:`, error);
542 | 
543 |     // Handle browser-specific errors at the top level
544 |     if (BROWSER_TOOLS.includes(name)) {
545 |       const errorMessage = (error as Error).message;
546 |       if (
547 |         errorMessage.includes(
548 |           "Target page, context or browser has been closed"
549 |         ) ||
550 |         errorMessage.includes("Browser has been disconnected") ||
551 |         errorMessage.includes("Target closed") ||
552 |         errorMessage.includes("Protocol error") ||
553 |         errorMessage.includes("Connection closed")
554 |       ) {
555 |         // Reset browser state if it's a connection issue
556 |         resetBrowserState();
557 |         return {
558 |           content: [
559 |             {
560 |               type: "text",
561 |               text: `Browser connection error: ${errorMessage}. Browser state has been reset, please try again.`,
562 |             },
563 |           ],
564 |           isError: true,
565 |         };
566 |       }
567 |     }
568 | 
569 |     return {
570 |       content: [
571 |         {
572 |           type: "text",
573 |           text: error instanceof Error ? error.message : String(error),
574 |         },
575 |       ],
576 |       isError: true,
577 |     };
578 |   }
579 | }
580 | 
581 | /**
582 |  * Helper function to handle codegen tool results
583 |  */
584 | async function handleCodegenResult(
585 |   resultPromise: Promise<any>
586 | ): Promise<CallToolResult> {
587 |   try {
588 |     const result = await resultPromise;
589 |     return {
590 |       content: [
591 |         {
592 |           type: "text",
593 |           text: JSON.stringify(result),
594 |         },
595 |       ],
596 |       isError: false,
597 |     };
598 |   } catch (error) {
599 |     return {
600 |       content: [
601 |         {
602 |           type: "text",
603 |           text: error instanceof Error ? error.message : String(error),
604 |         },
605 |       ],
606 |       isError: true,
607 |     };
608 |   }
609 | }
610 | 
611 | /**
612 |  * Get console logs
613 |  */
614 | export function getConsoleLogs(): string[] {
615 |   return consoleLogsTool?.getConsoleLogs() ?? [];
616 | }
617 | 
618 | /**
619 |  * Get screenshots
620 |  */
621 | export function getScreenshots(): Map<string, string> {
622 |   return screenshotTool?.getScreenshots() ?? new Map();
623 | }
624 | 
```
Page 2/5FirstPrevNextLast