@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 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"}
@@ -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
+ }