Cursor结合Playwright MCP Server支持自动化
今天分享一下 playwright MCP Server,其提供了浏览器自动化能力,使大型语言模型能够在真实的浏览器环境中与网页交互,
也可以执行任务,例如运行JavaScript、截屏和导航网页元素,同时无缝处理 API 测试以验证端点并确保可靠性。
特性介绍:
一个模型上下文协议(Model Context Protocol)服务器,提供使用 Playwright 的浏览器自动化功能
使 LLM(大语言模型)能够与网页交互、截取屏幕截图,并在真实的浏览器环境中执行 JavaScript
具备的功能
- 🌐 完整的浏览器自动化能力
- 📸 整个页面或特定元素的屏幕截图捕获
- 🖱️ 全面的网页交互(导航、点击、表单填写)
- 📊 控制台日志监控
- 🔧 在浏览器上下文中执行 JavaScript
一、本地安装
1. 使用 npx安装playwright浏览器
npx playwright install
本地安装时,检查了兼容性:
2. 按照提示,需要升级node的版本后,版本如下
3. 再次执行, 安装成功结束截图如下
注意: 确保您已安装 Node.js,并且 npx
在您的系统 PATH 中可用。
二、在Cursor的配置方法
方法一、使用 Smithery 为 Cursor 安装 MCP Server Playwright**:
npx -y @smithery/cli install @automatalabs/mcp-server-playwright --client cursor
方法二、在cursor手动配置
1. cursor打开MCP配置
在 Cursor 的 Cursor Settings 中找到 MCP,点击右侧上方的 Add new global MCP server 按钮,便自动打开 mcp.json文件,然后将mcp server 的配置信息粘贴进去。
2. 填写如下配置
{"mcpServers": {"playwright": {"command": "npx","args": ["-y", "@automatalabs/mcp-server-playwright"]}}
}
3. 安装后展示如下
案例展示
案例一、使用其进行自动化访问项目捕获
1. 访问公网项目,进行界面捕获
2. 服务自动书写脚本
const puppeteer = require('puppeteer');async function screenshotBaidu() {try {// 使用系统Chrome浏览器const browser = await puppeteer.launch({executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',headless: true,args: ['--no-sandbox', '--disable-setuid-sandbox']});const page = await browser.newPage();// 设置视口大小await page.setViewport({ width: 1280, height: 720 });// 访问百度console.log('正在访问百度网站...');await page.goto('https://www.baidu.com/', { waitUntil: 'networkidle2' });// 截图console.log('正在截图...');await page.screenshot({path: 'baidu_screenshot.png',fullPage: true});// 生成PDFconsole.log('正在生成PDF...');await page.pdf({path: 'baidu_page.pdf',format: 'A4',printBackground: true});console.log('完成!截图保存为 baidu_screenshot.png,PDF保存为 baidu_page.pdf');await browser.close();} catch (error) {console.error('发生错误:', error);}
}screenshotBaidu();
三、生成的PDF如下
案例二、访问公司项目,自动化测试执行测试
1. 发布命令
帮我访问zmp.iwhalecloud.com项目,帮我输入账号密码登录测试,账号为0027022977 密码为 xxx
2. 系统生成了执行js的内容
const puppeteer = require('puppeteer');async function loginZmp() {try {// 使用系统Chrome浏览器,添加更多启动参数const browser = await puppeteer.launch({executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',headless: false, // 设置为false以便观察登录过程args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-accelerated-2d-canvas','--no-first-run','--no-zygote','--disable-gpu'],timeout: 60000});const page = await browser.newPage();// 设置视口大小await page.setViewport({ width: 1280, height: 720 });// 访问zmp.iwhalecloud.comconsole.log('正在访问zmp.iwhalecloud.com...');await page.goto('https://zmp.iwhalecloud.com', {waitUntil: 'networkidle2',timeout: 60000});// 等待页面加载await page.waitForTimeout(3000);// 截图记录初始页面await page.screenshot({path: 'zmp_initial.png',fullPage: true});console.log('正在查找登录表单...');// 尝试查找用户名输入框const usernameSelectors = ['input[name="username"]','input[name="user"]','input[name="account"]','input[type="text"]','input[placeholder*="用户名"]','input[placeholder*="账号"]','input[placeholder*="用户名"]','#username','#user','#account'];let usernameInput = null;for (const selector of usernameSelectors) {try {usernameInput = await page.$(selector);if (usernameInput) {console.log(`找到用户名输入框: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}// 尝试查找密码输入框const passwordSelectors = ['input[name="password"]','input[type="password"]','input[placeholder*="密码"]','#password','#pwd'];let passwordInput = null;for (const selector of passwordSelectors) {try {passwordInput = await page.$(selector);if (passwordInput) {console.log(`找到密码输入框: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (usernameInput && passwordInput) {console.log('正在输入登录信息...');// 清空并输入用户名await usernameInput.click();await usernameInput.clear();await usernameInput.type('0027022977');// 清空并输入密码await passwordInput.click();await passwordInput.clear();await passwordInput.type('xxxxx');// 截图记录输入后的状态await page.screenshot({path: 'zmp_login_filled.png',fullPage: true});console.log('正在查找登录按钮...');// 尝试查找登录按钮const loginButtonSelectors = ['button[type="submit"]','input[type="submit"]','button:contains("登录")','button:contains("Login")','input[value="登录"]','input[value="Login"]','.login-btn','#login-btn','.btn-login','button.login','input.login'];let loginButton = null;for (const selector of loginButtonSelectors) {try {loginButton = await page.$(selector);if (loginButton) {console.log(`找到登录按钮: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (loginButton) {console.log('正在点击登录按钮...');await loginButton.click();// 等待登录响应await page.waitForTimeout(5000);// 截图记录登录后的状态await page.screenshot({path: 'zmp_after_login.png',fullPage: true});console.log('登录完成!');} else {console.log('未找到登录按钮,尝试按回车键...');await page.keyboard.press('Enter');await page.waitForTimeout(3000);await page.screenshot({path: 'zmp_after_enter.png',fullPage: true});}} else {console.log('未找到登录表单,截图记录当前页面状态...');await page.screenshot({path: 'zmp_no_form.png',fullPage: true});}// 获取页面内容以便分析const pageContent = await page.content();console.log('页面标题:', await page.title());// 保存页面HTML内容const fs = require('fs');fs.writeFileSync('zmp_page.html', pageContent);console.log('页面HTML已保存为 zmp_page.html');console.log('测试完成!');console.log('生成的文件:');console.log('- zmp_initial.png: 初始页面截图');console.log('- zmp_login_filled.png: 填写登录信息后截图');console.log('- zmp_after_login.png: 登录后截图');console.log('- zmp_page.html: 页面HTML内容');// 保持浏览器打开以便观察console.log('浏览器将保持打开状态,您可以观察登录结果...');// await browser.close();} catch (error) {console.error('发生错误:', error);}
}loginZmp();
案例三、访问自建项目,自动化测试执行测试
1. 输入登录指令,要求如下:
登录系统地址:http://localhost:3000,用户名为admin,密码为123456,打开系统管理下的用户管理,在查询框输入admin,点击搜索按钮进行筛选,筛选出来后,第一条数据的操作列有编辑按钮,点击编辑按钮打开弹窗,在弹窗界面修改其手机号为18724145878,然后点击确认按钮,将每一步截图,把测试过程的操作过程测试报告使用mardown文件写到项目目录的test-report目录,截图放到相同目录
2. cursor开始自动自动化脚本
3. 编写后的自动化脚本如下
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');async function detailedUserTest() {let browser;try {console.log('=== 开始详细用户管理系统测试 ===');// 创建测试报告const testReport = {testName: '详细用户管理系统测试',startTime: new Date().toISOString(),steps: [],screenshots: [],details: []};// 使用系统Chrome浏览器browser = await puppeteer.launch({executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',headless: false,args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-web-security','--disable-features=VizDisplayCompositor','--disable-gpu','--no-first-run','--no-zygote'],timeout: 60000});const page = await browser.newPage();// 步骤1: 访问登录页面console.log('步骤1: 访问登录页面');testReport.steps.push({step: 1,action: '访问登录页面',description: '打开 http://localhost:3000'});await page.goto('http://localhost:3000', {waitUntil: 'domcontentloaded',timeout: 60000});await new Promise(resolve => setTimeout(resolve, 2000));await page.screenshot({ path: 'test-report/01-login-page.png' });testReport.screenshots.push('01-login-page.png');testReport.details.push('✓ 登录页面截图已保存');console.log('✓ 登录页面截图已保存');// 步骤2: 输入用户名console.log('步骤2: 输入用户名');testReport.steps.push({step: 2,action: '输入用户名',description: '输入用户名: admin'});const usernameInput = await page.$('input[name="username"], input[type="text"], #username');if (usernameInput) {await usernameInput.click();await usernameInput.evaluate(el => el.value = '');await usernameInput.type('admin');testReport.details.push('✓ 用户名输入完成: admin');console.log('✓ 用户名输入完成');} else {testReport.details.push('⚠ 未找到用户名输入框');console.log('⚠ 未找到用户名输入框');}// 步骤3: 输入密码console.log('步骤3: 输入密码');testReport.steps.push({step: 3,action: '输入密码',description: '输入密码: 123456'});const passwordInput = await page.$('input[name="password"], input[type="password"], #password');if (passwordInput) {await passwordInput.click();await passwordInput.evaluate(el => el.value = '');await passwordInput.type('123456');testReport.details.push('✓ 密码输入完成: 123456');console.log('✓ 密码输入完成');} else {testReport.details.push('⚠ 未找到密码输入框');console.log('⚠ 未找到密码输入框');}await page.screenshot({ path: 'test-report/02-login-form-filled.png' });testReport.screenshots.push('02-login-form-filled.png');testReport.details.push('✓ 登录表单填写完成截图已保存');console.log('✓ 登录表单填写完成截图已保存');// 步骤4: 点击登录按钮console.log('步骤4: 点击登录按钮');testReport.steps.push({step: 4,action: '点击登录按钮',description: '提交登录表单'});const loginButton = await page.$('button[type="submit"], input[type="submit"], .login-btn, #login-btn');if (loginButton) {await loginButton.click();testReport.details.push('✓ 登录按钮点击完成');console.log('✓ 登录按钮点击完成');} else {testReport.details.push('⚠ 未找到登录按钮,尝试按回车键');console.log('⚠ 未找到登录按钮,尝试按回车键');await page.keyboard.press('Enter');}// 等待登录响应await new Promise(resolve => setTimeout(resolve, 3000));await page.screenshot({ path: 'test-report/03-after-login.png' });testReport.screenshots.push('03-after-login.png');testReport.details.push('✓ 登录后页面截图已保存');console.log('✓ 登录后页面截图已保存');// 步骤5: 导航到系统管理console.log('步骤5: 导航到系统管理');testReport.steps.push({step: 5,action: '导航到系统管理',description: '点击系统管理菜单'});// 尝试查找系统管理菜单const systemMenuSelectors = ['a[href*="system"]','span[class*="system"]','.system-management','#system-management','li a[href*="system"]','div[class*="system"]','a[title*="系统"]','span[title*="系统"]'];let systemMenu = null;for (const selector of systemMenuSelectors) {try {systemMenu = await page.$(selector);if (systemMenu) {testReport.details.push(`✓ 找到系统管理菜单: ${selector}`);console.log(`✓ 找到系统管理菜单: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (systemMenu) {await systemMenu.click();testReport.details.push('✓ 系统管理菜单点击完成');console.log('✓ 系统管理菜单点击完成');} else {testReport.details.push('⚠ 未找到系统管理菜单,尝试直接访问用户管理');console.log('⚠ 未找到系统管理菜单,尝试直接访问用户管理');}await new Promise(resolve => setTimeout(resolve, 2000));await page.screenshot({ path: 'test-report/04-system-management.png' });testReport.screenshots.push('04-system-management.png');testReport.details.push('✓ 系统管理页面截图已保存');console.log('✓ 系统管理页面截图已保存');// 步骤6: 导航到用户管理console.log('步骤6: 导航到用户管理');testReport.steps.push({step: 6,action: '导航到用户管理',description: '点击用户管理子菜单'});// 尝试查找用户管理菜单const userManagementSelectors = ['a[href*="user"]','span[class*="user"]','.user-management','#user-management','li a[href*="user"]','div[class*="user"]','a[title*="用户"]','span[title*="用户"]'];let userManagementMenu = null;for (const selector of userManagementSelectors) {try {userManagementMenu = await page.$(selector);if (userManagementMenu) {testReport.details.push(`✓ 找到用户管理菜单: ${selector}`);console.log(`✓ 找到用户管理菜单: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (userManagementMenu) {await userManagementMenu.click();testReport.details.push('✓ 用户管理菜单点击完成');console.log('✓ 用户管理菜单点击完成');} else {testReport.details.push('⚠ 未找到用户管理菜单,尝试直接访问用户管理页面');console.log('⚠ 未找到用户管理菜单,尝试直接访问用户管理页面');// 尝试直接访问用户管理页面await page.goto('http://localhost:3000/system/user', { waitUntil: 'domcontentloaded' });}await new Promise(resolve => setTimeout(resolve, 3000));await page.screenshot({ path: 'test-report/05-user-management.png' });testReport.screenshots.push('05-user-management.png');testReport.details.push('✓ 用户管理页面截图已保存');console.log('✓ 用户管理页面截图已保存');// 步骤7: 在查询框输入adminconsole.log('步骤7: 在查询框输入admin');testReport.steps.push({step: 7,action: '在查询框输入admin',description: '在搜索框中输入admin'});// 查找搜索框const searchInput = await page.$('input[placeholder*="搜索"], input[placeholder*="查询"], input[name="search"], #search');if (searchInput) {await searchInput.click();await searchInput.evaluate(el => el.value = '');await searchInput.type('admin');testReport.details.push('✓ 查询条件输入完成: admin');console.log('✓ 查询条件输入完成');} else {testReport.details.push('⚠ 未找到查询框');console.log('⚠ 未找到查询框');}await page.screenshot({ path: 'test-report/06-search-input.png' });testReport.screenshots.push('06-search-input.png');testReport.details.push('✓ 查询条件输入后截图已保存');console.log('✓ 查询条件输入后截图已保存');// 步骤8: 点击搜索按钮console.log('步骤8: 点击搜索按钮');testReport.steps.push({step: 8,action: '点击搜索按钮',description: '点击搜索按钮执行查询'});// 查找搜索按钮const searchButtonSelectors = ['button[class*="search"]','button[class*="query"]','.search-btn','#search-btn','button[type="submit"]','input[type="submit"]'];let searchButton = null;for (const selector of searchButtonSelectors) {try {searchButton = await page.$(selector);if (searchButton) {testReport.details.push(`✓ 找到搜索按钮: ${selector}`);console.log(`✓ 找到搜索按钮: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (searchButton) {await searchButton.click();testReport.details.push('✓ 搜索按钮点击完成');console.log('✓ 搜索按钮点击完成');} else {testReport.details.push('⚠ 未找到搜索按钮,尝试按回车键');console.log('⚠ 未找到搜索按钮,尝试按回车键');await page.keyboard.press('Enter');}await new Promise(resolve => setTimeout(resolve, 2000));await page.screenshot({ path: 'test-report/07-search-results.png' });testReport.screenshots.push('07-search-results.png');testReport.details.push('✓ 搜索结果截图已保存');console.log('✓ 搜索结果截图已保存');// 步骤9: 点击第一条数据的编辑按钮console.log('步骤9: 点击第一条数据的编辑按钮');testReport.steps.push({step: 9,action: '点击第一条数据的编辑按钮',description: '点击admin用户的编辑按钮打开弹窗'});// 查找编辑按钮const editButtonSelectors = ['button[class*="edit"]','a[class*="edit"]','.edit-btn','#edit-btn','button[title="编辑"]','a[title="编辑"]','tr:first-child button[class*="edit"]','tr:first-child a[class*="edit"]','table button[class*="edit"]','table a[class*="edit"]','button[onclick*="edit"]','a[onclick*="edit"]'];let editButton = null;for (const selector of editButtonSelectors) {try {editButton = await page.$(selector);if (editButton) {testReport.details.push(`✓ 找到编辑按钮: ${selector}`);console.log(`✓ 找到编辑按钮: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (editButton) {await editButton.click();testReport.details.push('✓ 编辑按钮点击完成');console.log('✓ 编辑按钮点击完成');} else {testReport.details.push('⚠ 未找到编辑按钮');console.log('⚠ 未找到编辑按钮');}await new Promise(resolve => setTimeout(resolve, 2000));await page.screenshot({ path: 'test-report/08-edit-modal.png' });testReport.screenshots.push('08-edit-modal.png');testReport.details.push('✓ 编辑弹窗截图已保存');console.log('✓ 编辑弹窗截图已保存');// 步骤10: 在弹窗中修改手机号console.log('步骤10: 在弹窗中修改手机号');testReport.steps.push({step: 10,action: '在弹窗中修改手机号',description: '将手机号修改为18724145878'});// 查找手机号输入框const phoneInput = await page.$('input[name="phone"], input[placeholder*="手机"], #phone, input[type="tel"]');if (phoneInput) {await phoneInput.click();await phoneInput.evaluate(el => el.value = '');await phoneInput.type('18724145878');testReport.details.push('✓ 手机号修改完成: 18724145878');console.log('✓ 手机号修改完成');} else {testReport.details.push('⚠ 未找到手机号输入框');console.log('⚠ 未找到手机号输入框');}await page.screenshot({ path: 'test-report/09-phone-modified.png' });testReport.screenshots.push('09-phone-modified.png');testReport.details.push('✓ 手机号修改后截图已保存');console.log('✓ 手机号修改后截图已保存');// 步骤11: 点击保存按钮console.log('步骤11: 点击保存按钮');testReport.steps.push({step: 11,action: '点击保存按钮',description: '点击保存按钮提交修改'});// 查找保存按钮const saveButtonSelectors = ['button[class*="save"]','button[class*="confirm"]','button[class*="submit"]','.save-btn','#save-btn','button[type="submit"]','.modal button[class*="save"]','.dialog button[class*="save"]','button[onclick*="save"]','button[onclick*="confirm"]'];let saveButton = null;for (const selector of saveButtonSelectors) {try {saveButton = await page.$(selector);if (saveButton) {testReport.details.push(`✓ 找到保存按钮: ${selector}`);console.log(`✓ 找到保存按钮: ${selector}`);break;}} catch (e) {// 继续尝试下一个选择器}}if (saveButton) {await saveButton.click();testReport.details.push('✓ 保存按钮点击完成');console.log('✓ 保存按钮点击完成');} else {testReport.details.push('⚠ 未找到保存按钮');console.log('⚠ 未找到保存按钮');}await new Promise(resolve => setTimeout(resolve, 2000));await page.screenshot({ path: 'test-report/10-save-completed.png' });testReport.screenshots.push('10-save-completed.png');testReport.details.push('✓ 保存完成截图已保存');console.log('✓ 保存完成截图已保存');// 完成测试testReport.endTime = new Date().toISOString();testReport.status = 'completed';// 保存测试报告const reportPath = path.join(__dirname, 'test-report.json');fs.writeFileSync(reportPath, JSON.stringify(testReport, null, 2));console.log('=== 测试完成 ===');console.log('测试报告已保存到: test-report/test-report.json');console.log('所有截图已保存到: test-report/ 目录');} catch (error) {console.error('测试过程中发生错误:', error);} finally {if (browser) {console.log('浏览器将保持打开状态,您可以观察测试结果...');// await browser.close();}}
}detailedUserTest();
4. 处理完成后,生成了测试报告
5. 重新分析界面元素,修改脚本
6. 生成最终的结果报告
7. 结果为修改成功
案例四 访问某个项目,采集页面捕获页面,供二次开发
1. 输入指令
帮我访问此界面@https://www.iwhalecloud.com/ ,将页面所有元素捕获,生成静态文件,供二次开发 ,将页面所有元素捕获,生成静态文件,供二次开发
2.系统开始捕获