@be-link/smart-test 1.0.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -0
- package/dist/core/ai-assistant.d.ts +30 -0
- package/dist/core/ai-assistant.d.ts.map +1 -0
- package/dist/core/config.d.ts +36 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/types.d.ts +96 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +247 -0
- package/dist/index.js +272 -0
- package/dist/utils/screenshot.d.ts +17 -0
- package/dist/utils/screenshot.d.ts.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# @be-link/smart-test
|
|
2
|
+
|
|
3
|
+
> AI-powered visual testing for Playwright - 基于 AI 视觉模型的 Playwright 测试工具
|
|
4
|
+
|
|
5
|
+
使用 AI 视觉识别能力,让你的 E2E 测试更智能、更可靠。
|
|
6
|
+
|
|
7
|
+
## ✨ 特性
|
|
8
|
+
|
|
9
|
+
- 🤖 **AI 视觉检查**:基于通义千问等视觉模型,智能检查页面是否符合预期
|
|
10
|
+
- 🎯 **零侵入设计**:与 Playwright 无缝集成,无需修改现有测试代码
|
|
11
|
+
- 🔧 **灵活配置**:支持全局配置和单次调用配置
|
|
12
|
+
- 📸 **自动截图**:自动保存测试截图,方便排查问题
|
|
13
|
+
- 🚫 **优雅降级**:无 API Key 时自动跳过,不影响其他测试
|
|
14
|
+
- 📦 **TypeScript**:完整的类型定义支持
|
|
15
|
+
|
|
16
|
+
## 📦 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 使用 pnpm
|
|
20
|
+
pnpm add -D @be-link/smart-test
|
|
21
|
+
|
|
22
|
+
# 使用 npm
|
|
23
|
+
npm install -D @be-link/smart-test
|
|
24
|
+
|
|
25
|
+
# 使用 yarn
|
|
26
|
+
yarn add -D @be-link/smart-test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🚀 快速开始
|
|
30
|
+
|
|
31
|
+
### 1. 配置 API Key
|
|
32
|
+
|
|
33
|
+
在项目根目录创建 `.env` 文件:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
AI_API_KEY=sk-your-api-key-here
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. 初始化配置
|
|
40
|
+
|
|
41
|
+
在 `playwright.config.ts` 中配置:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { defineConfig } from '@playwright/test';
|
|
45
|
+
import { configure } from '@be-link/smart-test';
|
|
46
|
+
|
|
47
|
+
// 初始化 AI 配置
|
|
48
|
+
configure({
|
|
49
|
+
apiKey: process.env.AI_API_KEY,
|
|
50
|
+
screenshotDir: './test-results/ai-screenshots',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
// ... 其他 Playwright 配置
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. 在测试中使用
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { test, expect } from '@playwright/test';
|
|
62
|
+
import { aiReviewScreenshot } from '@be-link/smart-test';
|
|
63
|
+
|
|
64
|
+
test.describe('优惠券页面', () => {
|
|
65
|
+
test('AI 检查页面布局', async ({ page }) => {
|
|
66
|
+
await page.goto('/coupon');
|
|
67
|
+
|
|
68
|
+
// 使用 AI 检查页面
|
|
69
|
+
const result = await aiReviewScreenshot(page, '页面应该显示优惠券列表,包含标题、优惠券卡片和底部按钮');
|
|
70
|
+
|
|
71
|
+
// 处理结果
|
|
72
|
+
if (result.skipped) {
|
|
73
|
+
test.skip(); // 无 API Key 时跳过
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(result.ok, result.issues?.join(', ')).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 📖 API 文档
|
|
82
|
+
|
|
83
|
+
### `configure(config)`
|
|
84
|
+
|
|
85
|
+
配置全局设置。
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { configure } from '@be-link/smart-test';
|
|
89
|
+
|
|
90
|
+
configure({
|
|
91
|
+
apiKey: 'sk-xxx', // AI API 密钥
|
|
92
|
+
baseURL: 'https://...', // AI 服务地址(可选)
|
|
93
|
+
model: 'qwen3-vl-plus', // AI 模型(可选)
|
|
94
|
+
screenshotDir: './screenshots', // 截图保存目录(可选)
|
|
95
|
+
saveScreenshots: true, // 是否保存截图(可选)
|
|
96
|
+
timeout: 30000, // 超时时间(可选)
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `aiReviewScreenshot(page, options)`
|
|
101
|
+
|
|
102
|
+
使用 AI 检查页面截图。
|
|
103
|
+
|
|
104
|
+
**参数:**
|
|
105
|
+
|
|
106
|
+
- `page: Page` - Playwright Page 对象
|
|
107
|
+
- `options: string | AiCheckOptions` - 检查选项
|
|
108
|
+
|
|
109
|
+
**返回值:** `Promise<AiCheckResult>`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface AiCheckResult {
|
|
113
|
+
ok: boolean; // 检查是否通过
|
|
114
|
+
issues?: string[]; // 问题列表
|
|
115
|
+
skipped?: boolean; // 是否跳过
|
|
116
|
+
reason?: string; // 跳过原因
|
|
117
|
+
screenshotPath?: string; // 截图路径
|
|
118
|
+
rawResponse?: string; // AI 原始响应
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 🎨 使用示例
|
|
123
|
+
|
|
124
|
+
### 基础用法
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// 简单字符串描述
|
|
128
|
+
const result = await aiReviewScreenshot(page, '页面应该显示登录表单');
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 带配置的用法
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// 带配置对象
|
|
135
|
+
const result = await aiReviewScreenshot(page, {
|
|
136
|
+
expected: '页面应该显示商品列表',
|
|
137
|
+
model: 'gpt-4-vision-preview', // 使用其他模型
|
|
138
|
+
saveScreenshots: false, // 不保存截图
|
|
139
|
+
screenshotPrefix: 'product', // 自定义截图前缀
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 完整测试示例
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { test, expect } from '@playwright/test';
|
|
147
|
+
import { aiReviewScreenshot } from '@be-link/smart-test';
|
|
148
|
+
|
|
149
|
+
test.describe('商品页面测试', () => {
|
|
150
|
+
test.beforeEach(async ({ page }) => {
|
|
151
|
+
await page.goto('/products');
|
|
152
|
+
await page.waitForLoadState('networkidle');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('检查页面布局', async ({ page }) => {
|
|
156
|
+
const result = await aiReviewScreenshot(page, {
|
|
157
|
+
expected: '页面顶部应有搜索框,中间显示商品网格布局,底部有分页器',
|
|
158
|
+
fullPage: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (result.skipped) {
|
|
162
|
+
console.log('跳过原因:', result.reason);
|
|
163
|
+
test.skip();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!result.ok && result.issues) {
|
|
167
|
+
console.warn('发现的问题:', result.issues);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
expect(result.ok, `AI 检查失败: ${result.issues?.join(', ')}`).toBeTruthy();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('检查响应式布局', async ({ page }) => {
|
|
174
|
+
// 切换到移动端视图
|
|
175
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
176
|
+
|
|
177
|
+
const result = await aiReviewScreenshot(page, '移动端布局:商品应该以单列显示,每个卡片占满宽度');
|
|
178
|
+
|
|
179
|
+
if (!result.skipped) {
|
|
180
|
+
expect(result.ok).toBeTruthy();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 批量检查多个元素
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
test('检查多个区域', async ({ page }) => {
|
|
190
|
+
const checks = [
|
|
191
|
+
{ selector: '.header', expected: '顶部导航栏应显示 logo 和菜单' },
|
|
192
|
+
{ selector: '.main-content', expected: '主内容区应显示文章列表' },
|
|
193
|
+
{ selector: '.sidebar', expected: '侧边栏应显示热门标签' },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
for (const check of checks) {
|
|
197
|
+
const element = await page.locator(check.selector);
|
|
198
|
+
const screenshot = await element.screenshot();
|
|
199
|
+
|
|
200
|
+
// 这里可以扩展为单独检查某个元素的功能
|
|
201
|
+
// 当前版本检查整页,后续可以增强
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 🔧 高级配置
|
|
207
|
+
|
|
208
|
+
### 支持多种 AI 模型
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// 使用通义千问(默认)
|
|
212
|
+
configure({
|
|
213
|
+
apiKey: process.env.QWEN_API_KEY,
|
|
214
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
215
|
+
model: 'qwen3-vl-plus',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 使用 OpenAI GPT-4 Vision
|
|
219
|
+
configure({
|
|
220
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
221
|
+
baseURL: 'https://api.openai.com/v1',
|
|
222
|
+
model: 'gpt-4-vision-preview',
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 自定义截图存储
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
configure({
|
|
230
|
+
screenshotDir: './custom-screenshots',
|
|
231
|
+
saveScreenshots: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 单次调用自定义
|
|
235
|
+
const result = await aiReviewScreenshot(page, {
|
|
236
|
+
expected: '...',
|
|
237
|
+
screenshotPrefix: 'my-feature',
|
|
238
|
+
screenshotDir: './feature-screenshots',
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 在 CI 环境中使用
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// 在 CI 中可能不需要保存截图,减少存储开销
|
|
246
|
+
configure({
|
|
247
|
+
apiKey: process.env.AI_API_KEY,
|
|
248
|
+
saveScreenshots: process.env.CI !== 'true',
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 🤔 常见问题
|
|
253
|
+
|
|
254
|
+
### Q: 没有配置 API Key 会怎样?
|
|
255
|
+
|
|
256
|
+
A: 测试会自动跳过 AI 检查,返回 `skipped: true`,不会影响其他测试的执行。
|
|
257
|
+
|
|
258
|
+
### Q: 支持哪些 AI 模型?
|
|
259
|
+
|
|
260
|
+
A: 理论上支持所有兼容 OpenAI Chat Completions API 的视觉模型,包括:
|
|
261
|
+
|
|
262
|
+
- 通义千问 (qwen3-vl-plus) - 默认
|
|
263
|
+
- OpenAI GPT-4 Vision
|
|
264
|
+
- 其他兼容的模型服务
|
|
265
|
+
|
|
266
|
+
### Q: 截图保存在哪里?
|
|
267
|
+
|
|
268
|
+
A: 默认保存在 `./test-results/ai-screenshots/` 目录,可以通过 `screenshotDir` 配置修改。
|
|
269
|
+
|
|
270
|
+
### Q: 如何查看 AI 的原始响应?
|
|
271
|
+
|
|
272
|
+
A: 检查结果中的 `rawResponse` 字段包含了 AI 的原始响应内容。
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const result = await aiReviewScreenshot(page, '...');
|
|
276
|
+
console.log('AI 原始响应:', result.rawResponse);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## 📝 License
|
|
280
|
+
|
|
281
|
+
MIT
|
|
282
|
+
|
|
283
|
+
## 🤝 贡献
|
|
284
|
+
|
|
285
|
+
欢迎提交 Issue 和 Pull Request!
|
|
286
|
+
|
|
287
|
+
## 📮 联系方式
|
|
288
|
+
|
|
289
|
+
如有问题,请在 [GitHub Issues](https://github.com/snowmountain-top/be-link/issues) 中提出。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
import type { AiCheckOptions, AiCheckResult } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* 使用 AI 视觉模型检查页面截图是否符合预期
|
|
5
|
+
*
|
|
6
|
+
* @param page - Playwright Page 对象
|
|
7
|
+
* @param options - 检查选项(字符串或配置对象)
|
|
8
|
+
* @returns 检查结果
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // 简单使用
|
|
13
|
+
* const result = await aiReviewScreenshot(page, '页面应该显示优惠券列表');
|
|
14
|
+
*
|
|
15
|
+
* // 带配置使用
|
|
16
|
+
* const result = await aiReviewScreenshot(page, {
|
|
17
|
+
* expected: '页面应该显示优惠券列表',
|
|
18
|
+
* model: 'gpt-4-vision-preview',
|
|
19
|
+
* saveScreenshots: false,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // 处理结果
|
|
23
|
+
* if (result.skipped) {
|
|
24
|
+
* test.skip();
|
|
25
|
+
* }
|
|
26
|
+
* expect(result.ok, result.issues?.join(', ')).toBeTruthy();
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function aiReviewScreenshot(page: Page, options: string | AiCheckOptions): Promise<AiCheckResult>;
|
|
30
|
+
//# sourceMappingURL=ai-assistant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-assistant.d.ts","sourceRoot":"","sources":["../../src/core/ai-assistant.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,MAAM,SAAS,CAAC;AAI7E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA2E7G"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AiVisionConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 配置 AI 视觉检查的全局设置
|
|
4
|
+
*
|
|
5
|
+
* @param config - 配置选项
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { configure } from '@be-link/smart-test';
|
|
10
|
+
*
|
|
11
|
+
* configure({
|
|
12
|
+
* apiKey: process.env.AI_API_KEY,
|
|
13
|
+
* model: 'qwen3-vl-plus',
|
|
14
|
+
* screenshotDir: './screenshots',
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function configure(config: Partial<AiVisionConfig>): void;
|
|
19
|
+
/**
|
|
20
|
+
* 获取当前的全局配置
|
|
21
|
+
*
|
|
22
|
+
* @returns 当前配置
|
|
23
|
+
*/
|
|
24
|
+
export declare function getConfig(): Required<AiVisionConfig>;
|
|
25
|
+
/**
|
|
26
|
+
* 重置配置为默认值
|
|
27
|
+
*/
|
|
28
|
+
export declare function resetConfig(): void;
|
|
29
|
+
/**
|
|
30
|
+
* 合并全局配置和局部配置
|
|
31
|
+
*
|
|
32
|
+
* @param localConfig - 局部配置(会覆盖全局配置)
|
|
33
|
+
* @returns 合并后的配置
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergeConfig(localConfig?: Partial<AiVisionConfig>): Required<AiVisionConfig>;
|
|
36
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAmB9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAK/D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,QAAQ,CAAC,cAAc,CAAC,CAEpD;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAElC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,WAAW,GAAE,OAAO,CAAC,cAAc,CAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,CAK/F"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 视觉检查配置
|
|
3
|
+
*/
|
|
4
|
+
export interface AiVisionConfig {
|
|
5
|
+
/**
|
|
6
|
+
* AI API 密钥
|
|
7
|
+
*/
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
/**
|
|
10
|
+
* AI 服务的 base URL
|
|
11
|
+
* @default 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
|
12
|
+
*/
|
|
13
|
+
baseURL?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 使用的 AI 模型
|
|
16
|
+
* @default 'qwen3-vl-plus'
|
|
17
|
+
*/
|
|
18
|
+
model?: string;
|
|
19
|
+
/**
|
|
20
|
+
* 截图保存目录
|
|
21
|
+
* @default './test-results/ai-screenshots'
|
|
22
|
+
*/
|
|
23
|
+
screenshotDir?: string;
|
|
24
|
+
/**
|
|
25
|
+
* 是否保存截图到文件
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
saveScreenshots?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* AI 请求超时时间(毫秒)
|
|
31
|
+
* @default 30000
|
|
32
|
+
*/
|
|
33
|
+
timeout?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* AI 检查结果
|
|
37
|
+
*/
|
|
38
|
+
export interface AiCheckResult {
|
|
39
|
+
/**
|
|
40
|
+
* 检查是否通过
|
|
41
|
+
*/
|
|
42
|
+
ok: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* 检查发现的问题列表
|
|
45
|
+
*/
|
|
46
|
+
issues?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* 是否跳过了检查(例如:无 API key)
|
|
49
|
+
*/
|
|
50
|
+
skipped?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* 跳过的原因
|
|
53
|
+
*/
|
|
54
|
+
reason?: string;
|
|
55
|
+
/**
|
|
56
|
+
* 截图保存的路径(如果启用了保存)
|
|
57
|
+
*/
|
|
58
|
+
screenshotPath?: string;
|
|
59
|
+
/**
|
|
60
|
+
* AI 原始响应内容
|
|
61
|
+
*/
|
|
62
|
+
rawResponse?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* AI 检查选项(单次调用)
|
|
66
|
+
*/
|
|
67
|
+
export interface AiCheckOptions extends Partial<AiVisionConfig> {
|
|
68
|
+
/**
|
|
69
|
+
* 期望的页面描述或检查条件
|
|
70
|
+
*/
|
|
71
|
+
expected: string;
|
|
72
|
+
/**
|
|
73
|
+
* 是否截取完整页面
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
fullPage?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* 自定义截图文件名前缀
|
|
79
|
+
* @default 'ai-review'
|
|
80
|
+
*/
|
|
81
|
+
screenshotPrefix?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* AI 响应的 JSON 格式
|
|
85
|
+
*/
|
|
86
|
+
export interface AiResponseJson {
|
|
87
|
+
/**
|
|
88
|
+
* 检查是否通过
|
|
89
|
+
*/
|
|
90
|
+
pass: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* 发现的问题列表
|
|
93
|
+
*/
|
|
94
|
+
issues?: string[];
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IAEZ;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,OAAO,CAAC,cAAc,CAAC;IAC7D;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @be-link/smart-test
|
|
3
|
+
*
|
|
4
|
+
* AI-powered visual testing for Playwright
|
|
5
|
+
* 基于 AI 视觉模型的 Playwright 测试工具
|
|
6
|
+
*/
|
|
7
|
+
export { aiReviewScreenshot } from './core/ai-assistant';
|
|
8
|
+
export { configure, getConfig, resetConfig } from './core/config';
|
|
9
|
+
export type { AiVisionConfig, AiCheckResult, AiCheckOptions, AiResponseJson } from './core/types';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { OpenAI } from 'openai';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 默认配置
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
apiKey: '',
|
|
10
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
11
|
+
model: 'qwen3-vl-plus',
|
|
12
|
+
screenshotDir: './test-results/ai-screenshots',
|
|
13
|
+
saveScreenshots: true,
|
|
14
|
+
timeout: 30000,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 全局配置
|
|
18
|
+
*/
|
|
19
|
+
let globalConfig = { ...DEFAULT_CONFIG };
|
|
20
|
+
/**
|
|
21
|
+
* 配置 AI 视觉检查的全局设置
|
|
22
|
+
*
|
|
23
|
+
* @param config - 配置选项
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { configure } from '@be-link/smart-test';
|
|
28
|
+
*
|
|
29
|
+
* configure({
|
|
30
|
+
* apiKey: process.env.AI_API_KEY,
|
|
31
|
+
* model: 'qwen3-vl-plus',
|
|
32
|
+
* screenshotDir: './screenshots',
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function configure(config) {
|
|
37
|
+
globalConfig = {
|
|
38
|
+
...globalConfig,
|
|
39
|
+
...config,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 获取当前的全局配置
|
|
44
|
+
*
|
|
45
|
+
* @returns 当前配置
|
|
46
|
+
*/
|
|
47
|
+
function getConfig() {
|
|
48
|
+
return { ...globalConfig };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 重置配置为默认值
|
|
52
|
+
*/
|
|
53
|
+
function resetConfig() {
|
|
54
|
+
globalConfig = { ...DEFAULT_CONFIG };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 合并全局配置和局部配置
|
|
58
|
+
*
|
|
59
|
+
* @param localConfig - 局部配置(会覆盖全局配置)
|
|
60
|
+
* @returns 合并后的配置
|
|
61
|
+
*/
|
|
62
|
+
function mergeConfig(localConfig = {}) {
|
|
63
|
+
return {
|
|
64
|
+
...globalConfig,
|
|
65
|
+
...localConfig,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 保存截图到文件
|
|
71
|
+
*
|
|
72
|
+
* @param screenshot - 截图 Buffer
|
|
73
|
+
* @param dir - 保存目录
|
|
74
|
+
* @param prefix - 文件名前缀
|
|
75
|
+
* @returns 截图文件的绝对路径
|
|
76
|
+
*/
|
|
77
|
+
async function saveScreenshot(screenshot, dir, prefix = 'ai-review') {
|
|
78
|
+
// 确保目录存在
|
|
79
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
80
|
+
// 生成文件名(使用时间戳避免冲突)
|
|
81
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
82
|
+
const filename = `${prefix}-${timestamp}.png`;
|
|
83
|
+
const screenshotPath = path.resolve(dir, filename);
|
|
84
|
+
// 写入文件
|
|
85
|
+
await fs.promises.writeFile(screenshotPath, screenshot);
|
|
86
|
+
return screenshotPath;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 将截图转换为 base64 编码
|
|
90
|
+
*
|
|
91
|
+
* @param screenshot - 截图 Buffer
|
|
92
|
+
* @returns base64 编码的字符串
|
|
93
|
+
*/
|
|
94
|
+
function screenshotToBase64(screenshot) {
|
|
95
|
+
return screenshot.toString('base64');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 使用 AI 视觉模型检查页面截图是否符合预期
|
|
100
|
+
*
|
|
101
|
+
* @param page - Playwright Page 对象
|
|
102
|
+
* @param options - 检查选项(字符串或配置对象)
|
|
103
|
+
* @returns 检查结果
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* // 简单使用
|
|
108
|
+
* const result = await aiReviewScreenshot(page, '页面应该显示优惠券列表');
|
|
109
|
+
*
|
|
110
|
+
* // 带配置使用
|
|
111
|
+
* const result = await aiReviewScreenshot(page, {
|
|
112
|
+
* expected: '页面应该显示优惠券列表',
|
|
113
|
+
* model: 'gpt-4-vision-preview',
|
|
114
|
+
* saveScreenshots: false,
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* // 处理结果
|
|
118
|
+
* if (result.skipped) {
|
|
119
|
+
* test.skip();
|
|
120
|
+
* }
|
|
121
|
+
* expect(result.ok, result.issues?.join(', ')).toBeTruthy();
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
async function aiReviewScreenshot(page, options) {
|
|
125
|
+
// 规范化选项
|
|
126
|
+
const opts = typeof options === 'string' ? { expected: options } : options;
|
|
127
|
+
// 合并配置
|
|
128
|
+
const config = mergeConfig(opts);
|
|
129
|
+
// 检查 API key
|
|
130
|
+
if (!config.apiKey) {
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
skipped: true,
|
|
134
|
+
reason: '未配置 AI API Key,跳过 AI 检查',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
// 截取页面截图
|
|
139
|
+
const screenshot = await page.screenshot({
|
|
140
|
+
fullPage: opts.fullPage !== false,
|
|
141
|
+
});
|
|
142
|
+
// 保存截图(如果启用)
|
|
143
|
+
let screenshotPath;
|
|
144
|
+
if (config.saveScreenshots) {
|
|
145
|
+
screenshotPath = await saveScreenshot(screenshot, config.screenshotDir, opts.screenshotPrefix || 'ai-review');
|
|
146
|
+
console.log(`[smart-test] 截图已保存: ${screenshotPath}`);
|
|
147
|
+
}
|
|
148
|
+
// 构建 AI 请求
|
|
149
|
+
const prompt = buildPrompt(opts.expected);
|
|
150
|
+
const openai = new OpenAI({
|
|
151
|
+
apiKey: config.apiKey,
|
|
152
|
+
baseURL: config.baseURL,
|
|
153
|
+
});
|
|
154
|
+
// 调用 AI 模型
|
|
155
|
+
const completion = await openai.chat.completions.create({
|
|
156
|
+
model: config.model,
|
|
157
|
+
messages: [
|
|
158
|
+
{
|
|
159
|
+
role: 'user',
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'image_url',
|
|
163
|
+
image_url: {
|
|
164
|
+
url: `data:image/png;base64,${screenshotToBase64(screenshot)}`,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: prompt,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
// 解析 AI 响应
|
|
176
|
+
const content = completion?.choices[0]?.message?.content || '';
|
|
177
|
+
const result = parseAiResponse(content);
|
|
178
|
+
return {
|
|
179
|
+
...result,
|
|
180
|
+
screenshotPath,
|
|
181
|
+
rawResponse: content,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error('[smart-test] AI 检查失败:', error);
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
skipped: true,
|
|
189
|
+
reason: `AI 检查失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 构建 AI 检查的 prompt
|
|
195
|
+
*
|
|
196
|
+
* @param expected - 期望描述
|
|
197
|
+
* @returns prompt 字符串
|
|
198
|
+
*/
|
|
199
|
+
function buildPrompt(expected) {
|
|
200
|
+
return `请检查这张页面截图是否符合预期:${expected}。
|
|
201
|
+
|
|
202
|
+
要求:
|
|
203
|
+
1. 仔细检查页面的布局、内容、文本是否符合描述
|
|
204
|
+
2. 如果发现问题,请详细说明问题的位置和具体情况
|
|
205
|
+
3. 返回标准 JSON 格式,不要包含其他文本
|
|
206
|
+
|
|
207
|
+
返回格式:
|
|
208
|
+
{
|
|
209
|
+
"pass": true/false,
|
|
210
|
+
"issues": ["问题1的详细描述", "问题2的详细描述"]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
如果检查通过,issues 可以为空数组。`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 解析 AI 的响应内容
|
|
217
|
+
*
|
|
218
|
+
* @param content - AI 返回的文本内容
|
|
219
|
+
* @returns 检查结果
|
|
220
|
+
*/
|
|
221
|
+
function parseAiResponse(content) {
|
|
222
|
+
try {
|
|
223
|
+
// 提取 JSON 对象(AI 可能返回带有前后文本的内容)
|
|
224
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
225
|
+
if (!jsonMatch) {
|
|
226
|
+
console.warn('[smart-test] AI 响应无法解析为 JSON:', content);
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
issues: [`AI 响应格式异常,无法解析`],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
233
|
+
return {
|
|
234
|
+
ok: parsed.pass,
|
|
235
|
+
issues: parsed.issues || [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
console.error('[smart-test] 解析 AI 响应失败:', error);
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
issues: [`AI 响应解析失败: ${error instanceof Error ? error.message : String(error)}`],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { aiReviewScreenshot, configure, getConfig, resetConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var openai = require('openai');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
|
|
7
|
+
function _interopNamespaceDefault(e) {
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
25
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 默认配置
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_CONFIG = {
|
|
31
|
+
apiKey: '',
|
|
32
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
33
|
+
model: 'qwen3-vl-plus',
|
|
34
|
+
screenshotDir: './test-results/ai-screenshots',
|
|
35
|
+
saveScreenshots: true,
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 全局配置
|
|
40
|
+
*/
|
|
41
|
+
let globalConfig = { ...DEFAULT_CONFIG };
|
|
42
|
+
/**
|
|
43
|
+
* 配置 AI 视觉检查的全局设置
|
|
44
|
+
*
|
|
45
|
+
* @param config - 配置选项
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { configure } from '@be-link/smart-test';
|
|
50
|
+
*
|
|
51
|
+
* configure({
|
|
52
|
+
* apiKey: process.env.AI_API_KEY,
|
|
53
|
+
* model: 'qwen3-vl-plus',
|
|
54
|
+
* screenshotDir: './screenshots',
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
function configure(config) {
|
|
59
|
+
globalConfig = {
|
|
60
|
+
...globalConfig,
|
|
61
|
+
...config,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 获取当前的全局配置
|
|
66
|
+
*
|
|
67
|
+
* @returns 当前配置
|
|
68
|
+
*/
|
|
69
|
+
function getConfig() {
|
|
70
|
+
return { ...globalConfig };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 重置配置为默认值
|
|
74
|
+
*/
|
|
75
|
+
function resetConfig() {
|
|
76
|
+
globalConfig = { ...DEFAULT_CONFIG };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 合并全局配置和局部配置
|
|
80
|
+
*
|
|
81
|
+
* @param localConfig - 局部配置(会覆盖全局配置)
|
|
82
|
+
* @returns 合并后的配置
|
|
83
|
+
*/
|
|
84
|
+
function mergeConfig(localConfig = {}) {
|
|
85
|
+
return {
|
|
86
|
+
...globalConfig,
|
|
87
|
+
...localConfig,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 保存截图到文件
|
|
93
|
+
*
|
|
94
|
+
* @param screenshot - 截图 Buffer
|
|
95
|
+
* @param dir - 保存目录
|
|
96
|
+
* @param prefix - 文件名前缀
|
|
97
|
+
* @returns 截图文件的绝对路径
|
|
98
|
+
*/
|
|
99
|
+
async function saveScreenshot(screenshot, dir, prefix = 'ai-review') {
|
|
100
|
+
// 确保目录存在
|
|
101
|
+
await fs__namespace.promises.mkdir(dir, { recursive: true });
|
|
102
|
+
// 生成文件名(使用时间戳避免冲突)
|
|
103
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
104
|
+
const filename = `${prefix}-${timestamp}.png`;
|
|
105
|
+
const screenshotPath = path__namespace.resolve(dir, filename);
|
|
106
|
+
// 写入文件
|
|
107
|
+
await fs__namespace.promises.writeFile(screenshotPath, screenshot);
|
|
108
|
+
return screenshotPath;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 将截图转换为 base64 编码
|
|
112
|
+
*
|
|
113
|
+
* @param screenshot - 截图 Buffer
|
|
114
|
+
* @returns base64 编码的字符串
|
|
115
|
+
*/
|
|
116
|
+
function screenshotToBase64(screenshot) {
|
|
117
|
+
return screenshot.toString('base64');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 使用 AI 视觉模型检查页面截图是否符合预期
|
|
122
|
+
*
|
|
123
|
+
* @param page - Playwright Page 对象
|
|
124
|
+
* @param options - 检查选项(字符串或配置对象)
|
|
125
|
+
* @returns 检查结果
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* // 简单使用
|
|
130
|
+
* const result = await aiReviewScreenshot(page, '页面应该显示优惠券列表');
|
|
131
|
+
*
|
|
132
|
+
* // 带配置使用
|
|
133
|
+
* const result = await aiReviewScreenshot(page, {
|
|
134
|
+
* expected: '页面应该显示优惠券列表',
|
|
135
|
+
* model: 'gpt-4-vision-preview',
|
|
136
|
+
* saveScreenshots: false,
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* // 处理结果
|
|
140
|
+
* if (result.skipped) {
|
|
141
|
+
* test.skip();
|
|
142
|
+
* }
|
|
143
|
+
* expect(result.ok, result.issues?.join(', ')).toBeTruthy();
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
async function aiReviewScreenshot(page, options) {
|
|
147
|
+
// 规范化选项
|
|
148
|
+
const opts = typeof options === 'string' ? { expected: options } : options;
|
|
149
|
+
// 合并配置
|
|
150
|
+
const config = mergeConfig(opts);
|
|
151
|
+
// 检查 API key
|
|
152
|
+
if (!config.apiKey) {
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
skipped: true,
|
|
156
|
+
reason: '未配置 AI API Key,跳过 AI 检查',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
// 截取页面截图
|
|
161
|
+
const screenshot = await page.screenshot({
|
|
162
|
+
fullPage: opts.fullPage !== false,
|
|
163
|
+
});
|
|
164
|
+
// 保存截图(如果启用)
|
|
165
|
+
let screenshotPath;
|
|
166
|
+
if (config.saveScreenshots) {
|
|
167
|
+
screenshotPath = await saveScreenshot(screenshot, config.screenshotDir, opts.screenshotPrefix || 'ai-review');
|
|
168
|
+
console.log(`[smart-test] 截图已保存: ${screenshotPath}`);
|
|
169
|
+
}
|
|
170
|
+
// 构建 AI 请求
|
|
171
|
+
const prompt = buildPrompt(opts.expected);
|
|
172
|
+
const openai$1 = new openai.OpenAI({
|
|
173
|
+
apiKey: config.apiKey,
|
|
174
|
+
baseURL: config.baseURL,
|
|
175
|
+
});
|
|
176
|
+
// 调用 AI 模型
|
|
177
|
+
const completion = await openai$1.chat.completions.create({
|
|
178
|
+
model: config.model,
|
|
179
|
+
messages: [
|
|
180
|
+
{
|
|
181
|
+
role: 'user',
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: 'image_url',
|
|
185
|
+
image_url: {
|
|
186
|
+
url: `data:image/png;base64,${screenshotToBase64(screenshot)}`,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
text: prompt,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
// 解析 AI 响应
|
|
198
|
+
const content = completion?.choices[0]?.message?.content || '';
|
|
199
|
+
const result = parseAiResponse(content);
|
|
200
|
+
return {
|
|
201
|
+
...result,
|
|
202
|
+
screenshotPath,
|
|
203
|
+
rawResponse: content,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error('[smart-test] AI 检查失败:', error);
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
skipped: true,
|
|
211
|
+
reason: `AI 检查失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 构建 AI 检查的 prompt
|
|
217
|
+
*
|
|
218
|
+
* @param expected - 期望描述
|
|
219
|
+
* @returns prompt 字符串
|
|
220
|
+
*/
|
|
221
|
+
function buildPrompt(expected) {
|
|
222
|
+
return `请检查这张页面截图是否符合预期:${expected}。
|
|
223
|
+
|
|
224
|
+
要求:
|
|
225
|
+
1. 仔细检查页面的布局、内容、文本是否符合描述
|
|
226
|
+
2. 如果发现问题,请详细说明问题的位置和具体情况
|
|
227
|
+
3. 返回标准 JSON 格式,不要包含其他文本
|
|
228
|
+
|
|
229
|
+
返回格式:
|
|
230
|
+
{
|
|
231
|
+
"pass": true/false,
|
|
232
|
+
"issues": ["问题1的详细描述", "问题2的详细描述"]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
如果检查通过,issues 可以为空数组。`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 解析 AI 的响应内容
|
|
239
|
+
*
|
|
240
|
+
* @param content - AI 返回的文本内容
|
|
241
|
+
* @returns 检查结果
|
|
242
|
+
*/
|
|
243
|
+
function parseAiResponse(content) {
|
|
244
|
+
try {
|
|
245
|
+
// 提取 JSON 对象(AI 可能返回带有前后文本的内容)
|
|
246
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
247
|
+
if (!jsonMatch) {
|
|
248
|
+
console.warn('[smart-test] AI 响应无法解析为 JSON:', content);
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
issues: [`AI 响应格式异常,无法解析`],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
255
|
+
return {
|
|
256
|
+
ok: parsed.pass,
|
|
257
|
+
issues: parsed.issues || [],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
console.error('[smart-test] 解析 AI 响应失败:', error);
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
issues: [`AI 响应解析失败: ${error instanceof Error ? error.message : String(error)}`],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exports.aiReviewScreenshot = aiReviewScreenshot;
|
|
270
|
+
exports.configure = configure;
|
|
271
|
+
exports.getConfig = getConfig;
|
|
272
|
+
exports.resetConfig = resetConfig;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 保存截图到文件
|
|
3
|
+
*
|
|
4
|
+
* @param screenshot - 截图 Buffer
|
|
5
|
+
* @param dir - 保存目录
|
|
6
|
+
* @param prefix - 文件名前缀
|
|
7
|
+
* @returns 截图文件的绝对路径
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveScreenshot(screenshot: Buffer, dir: string, prefix?: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* 将截图转换为 base64 编码
|
|
12
|
+
*
|
|
13
|
+
* @param screenshot - 截图 Buffer
|
|
14
|
+
* @returns base64 编码的字符串
|
|
15
|
+
*/
|
|
16
|
+
export declare function screenshotToBase64(screenshot: Buffer): string;
|
|
17
|
+
//# sourceMappingURL=screenshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/utils/screenshot.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAanH;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE7D"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@be-link/smart-test",
|
|
3
|
+
"version": "1.0.1-beta.0",
|
|
4
|
+
"description": "AI-powered visual testing for Playwright - 基于 AI 视觉模型的 Playwright 测试工具",
|
|
5
|
+
"homepage": "https://github.com/snowmountain-top/be-link",
|
|
6
|
+
"author": "shian",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.esm.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.esm.js",
|
|
15
|
+
"require": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.esm.js"
|
|
17
|
+
},
|
|
18
|
+
"./dist/*": "./dist/*"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"openai": "^6.15.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@playwright/test": "^1.40.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@playwright/test": "^1.50.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"packageInfo": {
|
|
37
|
+
"repoPath": "packages/smart-test"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"playwright",
|
|
41
|
+
"ai",
|
|
42
|
+
"testing",
|
|
43
|
+
"visual-testing",
|
|
44
|
+
"e2e",
|
|
45
|
+
"qwen",
|
|
46
|
+
"vision",
|
|
47
|
+
"screenshot"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"clean": "rimraf ./dist && rimraf .rollup.cache",
|
|
51
|
+
"build:js": "rollup --config rollup.config.ts --configPlugin typescript",
|
|
52
|
+
"build:types": "tsc --project tsconfig.json",
|
|
53
|
+
"build": "pnpm clean && pnpm build:js && pnpm build:types",
|
|
54
|
+
"lint": "eslint src --ext .ts --fix"
|
|
55
|
+
}
|
|
56
|
+
}
|