142.实习日志系统(SSD+BMAD+Harness系统体系的模拟使用)+playwright自动化 登录+填写_测试
继上篇https://www.cnblogs.com/gaodiyuanjin/p/19888191
我们用一个项目来验证使用
SSD+BMAD+Harness验证
这是我根据AI来有所侧重的一套体系
- 规范化都规定了每个角色的应用(应用BMAD的思想)
- spec撰写开发前的文档(应用SSD思想)
- 整个Agent开发在约束规范下进行(Harness思想)
目的就是 以后填写实习日志系统让Agent驱动完全自动化完成
其实简单点说就是为了---偷懒
一.实习日志系统开发(SSD+BMAD+Harness)
并没有找现成的日志系统来开发 而是正好测试一下这套体系的代码质量
工具还是TRAE(MiniMax2.7模型)
1.先编写spec
这个真的很重要!!!
你前面铺垫越规范越详细 AI后面就不容易翻车
当然这个项目太常见了 我同样的需求给GPT和Claude来撰写spec文档
---
id: SPEC-INTERNSHIP-SYSTEM
version: 1.0.0
title: 实习日志管理系统 · 完整开发规范
status: Ready for Agent Codegen
created: 2025-01
tech: SpringBoot + Vue3
storage: LocalStorage (无数据库)
scope: Module 01 登录页 / Module 02 主页面 / Module 03 实习日志模块
---
# 实习日志管理系统 · 项目开发规范 (Spec)
---
## 一、项目全局约定
### 1.1 技术栈选型
| 层级 | 选型 |
|------|------|
| 前端框架 | Vue 3 + JavaScript + Vite |
| 路由 | Vue Router v4 |
| 状态管理 | Pinia |
| 样式 | Tailwind CSS |
| UI 组件库 | Element Plus |
| 日历组件 | Element Plus ElCalendar |
| 图标库 | Element Plus Icons |
| 表单校验 | Element Plus Form Rules |
| HTTP 客户端 | axios |
| 后端框架 | Spring Boot 3.x |
| 数据存储 | 前端 LocalStorage(无数据库) |
### 1.2 存储策略总览
> 本项目不接入任何数据库,所有数据均持久化在浏览器 LocalStorage 中,
> 后端 SpringBoot 仅提供静态接口 mock / 验证码生成 / 会话校验逻辑,不做持久化。
| 存储 Key | 存储内容 | 生命周期 |
|----------|----------|----------|
| `internship_username` | 上次登录的账号 | 永久,勾选记住账号时写入 |
| `internship_autologin` | 是否开启自动登录 flag | 永久 |
| `internship_token` | 登录态 token(mock字符串)| 自动登录时永久 / 否则 sessionStorage |
| `internship_user` | 当前用户信息 JSON | 同 token 策略 |
| `internship_diary` | 实习日志数据 JSON Array | 永久 |
### 1.3 项目目录结构
project-root/
│
├── frontend/ # Vue3 前端工程
│ ├── src/
│ │ ├── pages/
│ │ │ ├── LoginPage.vue # 登录页
│ │ │ ├── DashboardPage.vue # 主页面
│ │ │ └── InternshipPage.vue # 实习日志页
│ │ ├── components/
│ │ │ ├── layout/
│ │ │ │ ├── AppLayout.vue # 整体布局容器
│ │ │ │ ├── SidebarNav.vue # 左侧导航
│ │ │ │ └── TopHeader.vue # 顶部 Header
│ │ │ ├── internship/
│ │ │ │ ├── DiaryCalendar.vue # 日历组件
│ │ │ │ └── DiaryDialog.vue # 填写弹框组件
│ │ │ └── shared/
│ │ │ └── CaptchaImage.vue # 验证码图片组件
│ │ ├── store/
│ │ │ ├── auth.store.ts # 登录态 store
│ │ │ └── diary.store.ts # 日志数据 store
│ │ ├── router/
│ │ │ └── index.ts # 路由配置 + 路由守卫
│ │ ├── services/
│ │ │ └── auth.service.ts # 登录接口调用
│ │ ├── types/
│ │ │ ├── auth.types.ts # 登录相关类型
│ │ │ └── diary.types.ts # 日志相关类型
│ │ └── utils/
│ │ └── storage.ts # LocalStorage 封装工具
│ └── vite.config.ts
│
└── backend/ # SpringBoot 后端工程
└── src/main/java/
└── com/internship/
├── controller/
│ ├── AuthController.java # 登录 / 验证码接口
│ └── MockController.java # mock 数据接口
├── model/
│ ├── LoginRequest.java
│ └── LoginResponse.java
└── InternshipApplication.java
### 1.4 路由规划
| 路径 | 页面组件 | 路由守卫规则 |
|------|----------|-------------|
| `/login` | `LoginPage.vue` | 已登录自动跳转 `/dashboard` |
| `/dashboard` | `DashboardPage.vue` | 未登录跳转 `/login` |
| `/internship` | `InternshipPage.vue` | 未登录跳转 `/login` |
| `/` | — | 重定向到 `/dashboard` |
### 1.5 data-testid 命名规范
所有可交互元素必须附加 `data-testid`,Playwright 通过此属性定位,
不依赖 CSS 类名或元素文案,保证测试脚本稳定性。
命名格式:`{元素类型}-{功能模块}-{描述}`
| 元素类型前缀 | 含义 |
|-------------|------|
| `input-` | 输入框 |
| `btn-` | 按钮 |
| `checkbox-` | 复选框 |
| `img-` | 图片 |
| `nav-` | 导航项 |
| `dialog-` | 弹框容器 |
| `calendar-` | 日历相关 |
| `text-` | 纯展示文本 |
---
## 二、Module 01 — 登录页面 (LoginPage)
### 2.1 页面职责
- 对用户账号密码进行校验(SpringBoot 接口 mock 验证)
- 生成并校验图形验证码(SpringBoot 后端生成 base64 图片)
- 处理"记住账号"和"自动登录"两个持久化选项
- 登录成功后跳转主页面,失败给出明确提示
### 2.2 页面视觉布局
┌─────────────────────────────────────────────┐
│ │
│ [Logo] 实习日志管理系统 │ ← 顶部品牌区
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 账号 │ │
│ │ [______***\**\*\*\*\*\*\*\*\*\*\*\*\*\*] │ │ ← 账号输入框
│ │ │ │
│ │ 密码 │ │
│ │ [\*\*\*\*________________] [👁] │ │ ← 密码输入框 + 眼睛图标
│ │ │ │
│ │ 验证码 │ │
│ │ [\*\*\*\*\*\*\*\*\*\**\***] [验证码图片🔄] │ │ ← 验证码 + 图片(点击刷新)
│ │ │ │
│ │ ☑ 记住账号 ☑ 自动登录 │ │ ← 选项行
│ │ │ │
│ │ [ 登 录 ] │ │ ← 登录按钮
│ └─────────────────────────────────────┘ │
│ │
│ © 2025 实习日志管理系统 │ ← 页脚
└─────────────────────────────────────────────┘
### 2.3 各元素详细规范
#### 账号输入框
- 标签:`账号`
- placeholder:`请输入学号 / 工号`
- 校验:必填 / 长度 4~20 / 仅允许字母、数字、下划线
- 错误提示位置:输入框正下方红色小字
- 自动回填:页面 `onMounted` 时读取 `localStorage.internship_username`
- `data-testid`:`input-username`
#### 密码输入框
- 标签:`密码`
- placeholder:`请输入密码`
- 默认 `type="password"`
- 右侧眼睛图标点击后切换 `type="text"`,再次点击还原
- 校验:必填
- `data-testid`:`input-password` / 眼睛图标 `btn-toggle-password`
#### 验证码区域
- 标签:`验证码`
- 输入框 placeholder:`请输入验证码`
- 输入框固定接受 4 位字符,自动转大写
- 右侧展示验证码图片(后端返回 base64)
- 点击图片触发刷新,请求 `/api/auth/captcha` 重新获取
- 图片加载中展示灰色占位块
- `data-testid`:`input-captcha` / `img-captcha`
#### 记住账号复选框
- 文字:`记住账号`
- 默认值:若 localStorage 存在 `internship_username` 则默认勾选,否则不勾选
- 勾选且登录成功:将 username 写入 `localStorage.internship_username`
- 取消勾选且登录成功:删除 `localStorage.internship_username`
- `data-testid`:`checkbox-remember`
#### 自动登录复选框
- 文字:`自动登录`
- 依赖关系:仅当"记住账号"已勾选时可操作,否则禁用并显示 tooltip 提示
- 勾选且登录成功:token 写入 `localStorage`,下次访问系统自动跳转
- 未勾选:token 写入 `sessionStorage`,关闭浏览器后失效
- `data-testid`:`checkbox-autologin`
#### 登录按钮
- 文字:`登 录`
- 点击触发:① 前端表单校验 → ② 调用登录接口 → ③ 处理响应
- Loading 状态:按钮显示 spinner,文字改为"登录中...",按钮禁用
- 支持 `Enter` 键提交
- `data-testid`:`btn-login`
### 2.4 后端接口规范(SpringBoot Mock)
#### GET /api/auth/captcha
- 无需认证
- 后端生成随机 4 位字母数字字符串,返回 base64 图片 + 会话标识
- Response:
```json
{
"captchaToken": "uuid-string",
"captchaImage": "data:image/png;base64,..."
}
#### POST /api/auth/login
- Request Body:
{
"username": "string",
"password": "string",
"captcha": "string",
"captchaToken": "string"
}
- Response 200(成功):
{
"token": "mock-jwt-string",
"user": {
"id": "001",
"name": "张三",
"role": "student"
}
}
- Response 400(失败):
{
"code": "INVALID_CAPTCHA | INVALID_CREDENTIALS",
"message": "验证码错误 | 账号或密码错误"
}
> Mock 账号数据(硬编码在 SpringBoot Controller 中):
>
> - 账号:`student001` 密码:`123456`
> - 账号:`admin` 密码:`admin123`
### 2.5 登录流程状态机
[初始态]
│
▼
用户填写表单
│
▼
点击登录 → 前端校验失败 → 显示字段错误,停留登录页
│
前端校验通过
│
▼
调用 POST /api/auth/login → Loading 态
│
├── 成功 ──► 存储 token + user → 跳转 /dashboard
│
└── 失败
├── INVALID_CAPTCHA ──► 刷新验证码 + 清空验证码输入框 + 错误提示
└── INVALID_CREDENTIALS ──► 清空密码输入框 + 错误提示
### 2.6 Playwright 测试锚点清单
| data-testid | 元素 | 用途 |
| --------------------- | ------------ | -------------- |
| `input-username` | 账号输入框 | 填入账号 |
| `input-password` | 密码输入框 | 填入密码 |
| `input-captcha` | 验证码输入框 | 填入验证码 |
| `img-captcha` | 验证码图片 | 点击刷新 |
| `btn-toggle-password` | 眼睛图标 | 切换密码可见性 |
| `checkbox-remember` | 记住账号 | 勾选/取消 |
| `checkbox-autologin` | 自动登录 | 勾选/取消 |
| `btn-login` | 登录按钮 | 提交表单 |
| `text-error-message` | 错误提示 | 断言错误文案 |
------
## 三、Module 02 — 主页面 (DashboardPage)
### 3.1 页面职责
- 提供整体应用布局容器(左侧导航 + 右侧内容区 + 顶部 Header)
- 左侧导航包含所有功能模块入口,含实习模块
- 右侧内容区根据路由动态渲染对应模块页面
- 顶部 Header 展示当前用户信息及退出登录
### 3.2 页面视觉布局
┌──────────────────────────────────────────────────────────────┐
│ [≡] 实习日志管理系统 LOGO 👤 张三 [退出登录] │ ← TopHeader
├────────────┬─────────────────────────────────────────────────┤
│ │ │
│ 🏠 首页 │ │
│ │ │
│ 📋 实习 │ 右侧内容区 │
│ 日志 │ (RouterView 渲染区域) │
│ │ │
│ 📊 统计 │ │
│ │ │
│ ⚙️ 设置 │ │
│ │ │
└────────────┴─────────────────────────────────────────────────┘
← SidebarNav →
宽度 220px
### 3.3 TopHeader 规范
- 左侧:汉堡菜单图标(控制侧边栏展开/收起)+ 系统名称
- 右侧:用户头像(圆形占位符,无头像时显示姓名首字)+ 用户姓名 + 退出登录按钮
- 退出登录:清除 token 和 user 的 storage loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
// Mock 验证码 API 返回固定值
await page.route('/api/auth/captcha', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
captchaToken: 'mock-token-001',
captchaImage: 'data:image/svg+xml;base64,...'
})
})
);
});
test.describe('正向场景', () => {
test('LOGIN-P01: 有效凭据登录成功并跳转', async ({ page }) => {
await page.route('/api/auth/login', route =>
route.fulfill({
status: 200,
body: JSON.stringify({
accessToken: 'mock-access-token',
refreshToken: 'mock-refresh-token',
user: { id: '1', name: '张三', role: 'student', avatar: null }
})
})
);
const { valid } = AUTH_FIXTURES;
await loginPage.loginWith(valid.username, valid.password, valid.captcha);
await loginPage.assertRedirectedToDash
上述是Claude生成的文档 GPT生成的我也会上传的gitee就不再展示
2.将spec转给Planner
用了TRAE的项目规则 它做任何事情都会扫描这个
同样你也可以直接#选择那些文档
因为TRAE也提供了@这种的方式 第一步写完spec找Planner呗
所以就不在上下文加入 直接@它
然后开始写计划
3.拿着Planner的规划去找Coder编写代码
我们同样是找到对应的人物完成 其实本质都是规范约束罢了

然后看一下生成的代码

4.然后Tester测试验证代码准确性
我们不去检查代码 而是交给对应Tester

同样的如果出现问题 它会生成报告 其中的错误
5.Fixer修改
如果有错误我们让其修复

根据Tester测试的结果进行修复
其实这里已经可以形成一套循环了
写代码->测试->修复
不过我这里全是手动的 并非真正实现自动化
那样的就需要脚本来驱动 等后续的深入学习再总结
6.Reviewer测评一下
类似一个大佬 测测这段代码怎么样 给我生成个报告

这已经打通了一套体系规范
结果看看生成的实习日志系统


这是最基础的 并没有美化页面 但是基本逻辑开发测试用都没有问题
二.Playwright自动化测试
这个才是关键东西
至于介绍 我让AI通过官网生成了一篇文档

最重要的是安装的时候出来很多问题:
npm install -g playwright
这条命令没啥问题
就是后续的安装chromium出现问题
应为我们为了使用chrome来测试
但是下载就会报错 镜像各种都试过了
参考: https://blog.kangyaocoding.top/archives/playwright-shou-dong-an-zhuang-chromium-wan-zheng-zhi-nan-guo-nei-jia-su-ban#:~:text=原因:Linux 手动放置浏览器后,缺少 Playwright 用于判定安装完成的标记文件 INSTALLATION_COMPLETE。 解决方案:在对应 revision ,%E7%9B%AE%E5%BD%95%E4%B8%8B%E6%89%8B%E5%8A%A8%E5%88%9B%E5%BB%BA%E6%A0%87%E8%AE%B0%E6%96%87%E4%BB%B6%E3%80%82%20%23%20%E4%BB%A5%20revision%3D1208%20%E4%B8%BA%E4%BE%8B%20touch%20%2Froot%2F.cache%2Fms-playwright%2Fchromium-1208%2FINSTALLATION_COMPLETE
然后也听过DeepseekV4来完成最终的配置

此目录下得有这个才能识别使用
就去把它下载下来对应的
npx playwright install --dry-run chromium
找到对应版本
然后把它手动加载到缓存里
但是系统以为未标记 还会消失
这就是windows
cd /d "%USERPROFILE%\AppData\Local\ms-playwright\chromium-1217"
type nul > INSTALLATION_COMPLETE
手动给它添加一个标记
npx playwright install chromium
再安装mcp
npm install -g @playwright/mcp@latest
在TRAE中配置
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
可以命令行测试一下:npx playwright open google.com
也可以通过Agent驱动看看
接下来就有意思了 其实就两步:
- 登录
- 提交实习日记
这里先说半自动化
使用playwright工具完成实习日志自动填写任务:
前后端已经启动 打开浏览器访问http://localhost:3000/
登录我将辅助你完成 后续直接都是自动登录了
找到实习日志填写 自动给我生成一段实习日记有关计算机工作的 填写提交
然后会打开 第一次登陆后保存信息 之后AI就会自动填写
其实它也是写了一个脚本 然后playwright驱动
重点来了 验证码问题
数字字母混合图片验证码其实就是为了防止自动化的
playwright无法绕开这个 所以就需要第三方来帮忙
我们肯定为的就是测试完全自动化 你不能一个登录还得让我来干预吧
结合DeepseekV4完成一个脚本来驱动完成自动化
这里借助三方平台 图鉴 api简单
http://www.ttshitu.com/
import { chromium } from 'playwright';
import axios from 'axios';
// ==================== 配置区 ====================
const USERNAME = 'student001';
const PASSWORD = '123456';
const SYSTEM_URL = 'http://localhost:3000';
// 图鉴账号(请改成你自己的)
const TT_USER = '账号';
const TT_PASS = '密码';
const TEST_TT_BALANCE_BEFORE_LOGIN = true;
// ==================== 日期 ====================
const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const DIARY_CONTENT = `今天的主要工作:
1. 参加项目晨会,了解本周迭代目标。
2. 学习代码规范,配置本地环境。
3. 协助导师完成模块测试。
4. 撰写测试报告初稿。
心得体会:对开发流程有了更深入的理解,继续加油。`;
// ==================== 图鉴工具函数 ====================
function checkTTConfig() {
if (!TT_USER || TT_USER.includes('您的图鉴')) throw new Error('TT_USER 未正确配置');
if (!TT_PASS || TT_PASS.includes('您的图鉴')) throw new Error('TT_PASS 未正确配置');
}
async function checkTTBalance() {
checkTTConfig();
console.log('▶ 正在测试图鉴账号余额...');
const queryUrl = `http://api.ttshitu.com/queryAccountInfo.json?username=${encodeURIComponent(TT_USER)}&password=${encodeURIComponent(TT_PASS)}`;
const res = await axios.get(queryUrl, { timeout: 60000 });
console.log('图鉴账户信息:', res.data);
if (!res.data.success) throw new Error(`图鉴余额查询失败:${JSON.stringify(res.data)}`);
console.log(`✅ 图鉴账户正常,当前余额:${res.data.data.balance} 元`);
return res.data;
}
// ==================== 打码函数(数字+字母,typeid=3) ====================
async function recognizeCaptcha(base64Image) {
checkTTConfig();
let pureBase64 = String(base64Image);
if (pureBase64.includes(',')) {
pureBase64 = pureBase64.split(',')[1];
}
console.log('最终提交的 base64 前 30 字符:', pureBase64.substring(0, 30));
const reqBody = {
username: TT_USER,
password: TT_PASS,
typeid: '3',
image: pureBase64
};
const res = await axios.post('http://api.ttshitu.com/predict', reqBody, {
headers: { 'Content-Type': 'application/json' },
timeout: 60000
});
console.log('图鉴接口返回:', res.data);
if (res.data.success && res.data.data && res.data.data.result) {
return res.data.data.result;
} else {
throw new Error(`图鉴识别失败:${res.data.message || '未知错误'}`);
}
}
// ==================== 获取验证码 base64 ====================
async function getCaptchaBase64(page) {
const img = page.locator('img[alt="验证码"]');
if (await img.count() > 0) {
await img.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
const buffer = await img.screenshot();
let base64 = buffer.toString('base64');
if (base64.includes(',')) {
base64 = base64.split(',')[1];
}
if (!base64 || base64.length < 100) throw new Error('验证码截图为空或过小');
console.log('获取到 img 验证码,base64 长度:', base64.length);
return base64;
}
const canvas = page.locator('canvas');
if (await canvas.count() > 0) {
const dataUrl = await canvas.evaluate(c => c.toDataURL('image/png'));
const base64 = dataUrl.split(',')[1];
if (!base64 || base64.length < 100) throw new Error('canvas 验证码为空或过小');
console.log('获取到 canvas 验证码,base64 长度:', base64.length);
return base64;
}
throw new Error('未找到验证码元素');
}
// ==================== 刷新验证码(已修复 detached 问题) ====================
async function refreshCaptcha(page) {
const imgLocator = page.locator('img[alt="验证码"]');
const canvasLocator = page.locator('canvas');
if (await imgLocator.count() > 0) {
await imgLocator.click({ timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
// 等待新图片出现,超时则抛出明确错误,避免无限等待
await page.waitForSelector('img[alt="验证码"]', { state: 'attached', timeout: 5000 });
await page.waitForTimeout(300);
return;
}
if (await canvasLocator.count() > 0) {
await page.waitForTimeout(1000);
return;
}
// 如果没有任何验证码元素,说明可能已不在登录页面
throw new Error('当前页面未找到验证码,可能已登录或跳转');
}
// ==================== 等待登录结果(增强判断) ====================
async function waitForLoginResult(page) {
try {
// 同时等待多种成功标志
await Promise.race([
page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 8000 }),
page.waitForSelector('.el-message--success, .el-notification, text=登录成功', { timeout: 8000 }),
page.waitForSelector('text=欢迎回来', { timeout: 8000 }) // 主页欢迎语
]);
return true;
} catch {
// 超时后直接检查当前 URL,若已跳转则视为成功
const currentUrl = page.url();
if (!currentUrl.includes('/login')) {
console.log('检测到 URL 已跳转,视为登录成功');
return true;
}
return false;
}
}
// ==================== 主流程 ====================
(async () => {
let browser;
try {
if (TEST_TT_BALANCE_BEFORE_LOGIN) {
await checkTTBalance();
}
browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
console.log('▶ 打开登录页...');
await page.goto(`${SYSTEM_URL}/login`, { waitUntil: 'networkidle' });
await page.locator('input[placeholder="请输入学号 / 工号"]').fill(USERNAME);
await page.locator('input[placeholder="请输入密码"]').fill(PASSWORD);
let loggedIn = false;
for (let i = 0; i < 5; i++) {
// 重试前检查是否已离开登录页(可能上一轮已真正登录)
const currentUrl = page.url();
if (!currentUrl.includes('/login')) {
console.log('已不在登录页面,直接标记为登录成功');
loggedIn = true;
break;
}
console.log(`▶ 第 ${i + 1} 次尝试登录`);
let base64;
try {
base64 = await getCaptchaBase64(page);
} catch (e) {
console.log('获取验证码失败:', e.message);
// 如果此时页面已无验证码,直接检测登录状态
if (!currentUrl.includes('/login')) {
loggedIn = true;
break;
}
await refreshCaptcha(page).catch(() => {});
continue;
}
let captchaText;
try {
captchaText = await recognizeCaptcha(base64);
console.log('识别结果:', captchaText);
} catch (err) {
console.log('识别失败:', err.message);
await refreshCaptcha(page).catch(() => {});
continue;
}
const captchaInput = page.locator('input[placeholder="请输入验证码"]');
await captchaInput.click();
const isMac = process.platform === 'darwin';
await captchaInput.press(isMac ? 'Meta+A' : 'Control+A');
await captchaInput.press('Backspace');
await captchaInput.type(captchaText, { delay: 50 });
await page.locator('button:has-text("登 录")').click();
const success = await waitForLoginResult(page);
if (success) {
loggedIn = true;
break;
}
console.log('登录尝试未检测到成功标志,准备重试...');
// 再次检查 URL,若已跳转则直接成功
if (!page.url().includes('/login')) {
loggedIn = true;
break;
}
// 否则刷新验证码再试
try {
await refreshCaptcha(page);
} catch {
console.log('刷新验证码失败,可能已不在登录页');
if (!page.url().includes('/login')) {
loggedIn = true;
break;
}
}
}
if (!loggedIn) {
throw new Error('登录失败:多次尝试后仍未成功');
}
console.log('✅ 登录成功');
await context.storageState({ path: 'auth.json' });
console.log('▶ 进入实习日志页面...');
await page.goto(`${SYSTEM_URL}/internship`, { waitUntil: 'networkidle' });
await page.waitForSelector('.el-calendar', { timeout: 10000 });
let dialog = page.locator('.el-dialog:visible');
const isDialogOpen = await dialog.count() > 0;
if (!isDialogOpen) {
const todayCell = page.locator('.el-calendar td.is-today');
if (await todayCell.count() === 0) {
throw new Error('未找到今天的日历单元格');
}
await todayCell.click();
dialog = page.locator('.el-dialog');
await dialog.waitFor({ state: 'visible', timeout: 10000 });
} else {
console.log('对话框已打开,跳过点击日期');
}
const textarea = dialog.locator('textarea').first();
await textarea.waitFor({ state: 'visible', timeout: 5000 });
await textarea.fill(DIARY_CONTENT);
console.log('✅ 日志内容已填入,正在保存...');
const saveBtn = dialog.locator('button:has-text("保存")');
await saveBtn.click();
await dialog.waitFor({ state: 'hidden', timeout: 10000 });
console.log('✅ 日志提交成功');
console.log('🎉 全流程完成');
} catch (err) {
console.error('❌ 出错:', err.message);
} finally {
// 调试时可先不关闭浏览器,观察页面情况
// if (browser) await browser.close();
}
})();
当然测试肯定需要money 充1元验证验证
启动前后端项目 然后运行脚本验证
node auto_2.js
最终完成填写
使用playwright完成自动化填写 同样现在流行的openclaw和harness agent也可以完成
等后续我们再用这个常用的agent来试试 大概也得接一点技能否则也很难绕过验证码
代码提交gitee:https://gitee.com/gao-yuan187/Test_internship_playwright








浙公网安备 33010602011771号