@kkeve/suite 0.0.13 → 0.0.17

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.
Files changed (117) hide show
  1. package/dist/bin/keve.js +2 -1
  2. package/dist/commands/clean.js +2 -2
  3. package/dist/commands/config.js +7 -7
  4. package/dist/commands/init.js +2 -2
  5. package/dist/commands/report.js +7 -7
  6. package/dist/commands/run.js +16 -12
  7. package/dist/commands/test.js +7 -7
  8. package/dist/config.js +7 -7
  9. package/dist/core/action-log-writer.js +4 -1
  10. package/dist/core/decorator/keve-decorators.d.ts +128 -0
  11. package/dist/core/decorator/keve-decorators.js +254 -0
  12. package/dist/core/decorator/keve-host.d.ts +40 -0
  13. package/dist/core/decorator/keve-host.js +33 -0
  14. package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
  15. package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
  16. package/dist/core/generateReport/report.d.ts +1 -1
  17. package/dist/core/generateReport/report.js +2 -2
  18. package/dist/core/generateReport/reportData.d.ts +3 -0
  19. package/dist/core/generateReport/reportData.js +55 -18
  20. package/dist/core/judge/confidence.d.ts +119 -0
  21. package/dist/core/judge/confidence.js +295 -0
  22. package/dist/core/judge/goal-heuristics.d.ts +32 -0
  23. package/dist/core/judge/goal-heuristics.js +128 -0
  24. package/dist/core/judge/pngCodec.d.ts +23 -0
  25. package/dist/core/judge/pngCodec.js +215 -0
  26. package/dist/core/judge/screenshotDiff.d.ts +112 -0
  27. package/dist/core/judge/screenshotDiff.js +420 -0
  28. package/dist/core/page-object/definePage.d.ts +95 -0
  29. package/dist/core/page-object/definePage.js +62 -0
  30. package/dist/core/page-object/elementResolver.d.ts +32 -0
  31. package/dist/core/page-object/elementResolver.js +236 -0
  32. package/dist/core/page-object/engine-types.d.ts +140 -0
  33. package/dist/core/page-object/engine-types.js +34 -0
  34. package/dist/core/sso-cookie.d.ts +18 -0
  35. package/dist/core/sso-cookie.js +10 -0
  36. package/dist/dsl/index.d.ts +15 -0
  37. package/dist/dsl/index.js +29 -0
  38. package/dist/dsl/locatorSource.d.ts +25 -0
  39. package/dist/dsl/locatorSource.js +323 -0
  40. package/dist/dsl/ops.d.ts +46 -0
  41. package/dist/dsl/ops.js +95 -0
  42. package/dist/dsl/toDecorator.d.ts +98 -0
  43. package/dist/dsl/toDecorator.js +747 -0
  44. package/dist/engine-cypress/adapter.d.ts +108 -0
  45. package/dist/engine-cypress/adapter.js +514 -0
  46. package/dist/engine-cypress/agent-page.d.ts +159 -0
  47. package/dist/engine-cypress/agent-page.js +566 -0
  48. package/dist/engine-cypress/agentDom.d.ts +22 -0
  49. package/dist/engine-cypress/agentDom.js +590 -0
  50. package/dist/engine-cypress/bridge.d.ts +39 -0
  51. package/dist/engine-cypress/bridge.js +598 -0
  52. package/dist/engine-cypress/cdp.d.ts +78 -0
  53. package/dist/engine-cypress/cdp.js +339 -0
  54. package/dist/engine-cypress/host.d.ts +22 -0
  55. package/dist/engine-cypress/host.js +480 -0
  56. package/dist/engine-cypress/index.d.ts +26 -0
  57. package/dist/engine-cypress/index.js +58 -0
  58. package/dist/engine-cypress/keveGoal.d.ts +43 -0
  59. package/dist/engine-cypress/keveGoal.js +265 -0
  60. package/dist/engine-cypress/pageOps.d.ts +41 -0
  61. package/dist/engine-cypress/pageOps.js +439 -0
  62. package/dist/engine-cypress/reporter.d.ts +20 -0
  63. package/dist/engine-cypress/reporter.js +128 -0
  64. package/dist/engine-cypress/setup.d.ts +21 -0
  65. package/dist/engine-cypress/setup.js +27 -0
  66. package/dist/engine-cypress/shotBuffer.d.ts +40 -0
  67. package/dist/engine-cypress/shotBuffer.js +41 -0
  68. package/dist/engine-cypress/state.d.ts +56 -0
  69. package/dist/engine-cypress/state.js +81 -0
  70. package/dist/engine-playwright/engineAdapter.d.ts +78 -0
  71. package/dist/engine-playwright/engineAdapter.js +371 -0
  72. package/dist/engine-playwright/global-setup.d.ts +28 -0
  73. package/dist/engine-playwright/global-setup.js +142 -0
  74. package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
  75. package/dist/engine-playwright/keve-report.js +137 -0
  76. package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
  77. package/dist/{core → engine-playwright}/keve-test.js +257 -207
  78. package/dist/{core → engine-playwright}/pw-config.js +8 -7
  79. package/dist/index.d.ts +13 -8
  80. package/dist/index.js +43 -28
  81. package/dist/page-agent/agent.d.ts +98 -7
  82. package/dist/page-agent/agent.js +338 -84
  83. package/dist/page-agent/diagnostics.d.ts +184 -0
  84. package/dist/page-agent/diagnostics.js +377 -0
  85. package/dist/page-agent/hooks.d.ts +3 -2
  86. package/dist/page-agent/hooks.js +4 -3
  87. package/dist/page-agent/page-like.d.ts +116 -0
  88. package/dist/page-agent/page-like.js +14 -0
  89. package/dist/page-agent/playwright-page.d.ts +20 -0
  90. package/dist/page-agent/playwright-page.js +98 -0
  91. package/dist/page-agent/system-prompt.d.ts +7 -0
  92. package/dist/page-agent/system-prompt.js +10 -0
  93. package/dist/page-agent/system_prompt.md +1 -1
  94. package/dist/page-agent/tools.d.ts +1 -1
  95. package/dist/page-agent/tools.js +441 -9
  96. package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +11 -0
  97. package/package.json +24 -11
  98. package/scripts/gen-system-prompt.mjs +36 -0
  99. package/scripts/run-dsl-task.mjs +619 -0
  100. package/scripts/runtime/cypress-preprocessor.mjs +44 -0
  101. package/scripts/runtime/cypress.config.mjs +35 -0
  102. package/scripts/runtime/playwright.config.mjs +41 -0
  103. package/scripts/verify-shot-diff.mjs +243 -0
  104. package/dist/commands/auth-update.d.ts +0 -16
  105. package/dist/commands/auth-update.js +0 -88
  106. package/dist/core/auth.d.ts +0 -17
  107. package/dist/core/auth.js +0 -224
  108. package/dist/core/cdp-resolver.d.ts +0 -5
  109. package/dist/core/cdp-resolver.js +0 -93
  110. package/dist/core/global-setup.d.ts +0 -6
  111. package/dist/core/global-setup.js +0 -113
  112. package/dist/core/keve-decorators.d.ts +0 -51
  113. package/dist/core/keve-decorators.js +0 -95
  114. package/dist/core/keve-report.js +0 -376
  115. /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
  116. /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
  117. /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Playwright 任务级配置(静态模板)
3
+ *
4
+ * 该文件不再随任务生成,任务差异全部通过环境变量注入:
5
+ * KEVE_TASK_DIR 任务根(specs 位于其下)
6
+ * KEVE_RESULT_DIR 当前轮次产物目录
7
+ * 因此任务目录只保留 specs / reports / runs / cases,不再落 package.json 与配置副本。
8
+ */
9
+ import { defineConfig } from '@playwright/test';
10
+ import * as path from 'node:path';
11
+ import * as url from 'node:url';
12
+
13
+ const runtimeDir = path.dirname(url.fileURLToPath(import.meta.url));
14
+ const suiteRoot = path.resolve(runtimeDir, '..', '..');
15
+
16
+ const taskDir = process.env.KEVE_TASK_DIR || path.resolve(suiteRoot, '.keve', 'default');
17
+ const resultDir = process.env.KEVE_RESULT_DIR || path.join(taskDir, 'reports', 'round-latest');
18
+
19
+ export default defineConfig({
20
+ testDir: path.join(taskDir, 'specs'),
21
+ globalSetup: '@kkeve/suite/global-setup',
22
+ fullyParallel: false,
23
+ workers: 1,
24
+ retries: 0,
25
+ timeout: 120000,
26
+ expect: { timeout: 10000 },
27
+ reporter: [
28
+ ['list'],
29
+ ['json', { outputFile: path.join(resultDir, 'test-results.json') }],
30
+ ['@kkeve/suite/keve-report'],
31
+ ],
32
+ use: {
33
+ baseURL: process.env.KEVE_TARGET_URL || '',
34
+ storageState: process.env.KEVE_STORAGE_STATE,
35
+ trace: 'off',
36
+ screenshot: 'off',
37
+ video: 'on',
38
+ actionTimeout: 10000,
39
+ },
40
+ outputDir: path.join(resultDir, 'test-results'),
41
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * verify-shot-diff — 截图基线对比核心逻辑验证(零浏览器、零外部依赖)
3
+ *
4
+ * 前置:EVE/packages/suite 已构建(npm run build,产物 dist/core/*.js)。
5
+ * 运行:cd EVE/packages/suite && node scripts/verify-shot-diff.mjs
6
+ *
7
+ * 覆盖(对齐设计方案 §3 判定语义):
8
+ * 1. pngCodec 编解码往返(RGB/RGBA)
9
+ * 2. 解码器对 PNG 全部 4 种行滤波(Sub/Up/Average/Paeth)的正确性
10
+ * —— Playwright 截图会用任意滤波,必须都能解
11
+ * 3. 基线生命周期:无基线 → baseline_required(候选基线,不判通过)→ 模拟审核转正
12
+ * → 二跑 identical → matched;未审核再跑仍 baseline_required(候选门控 / A06)
13
+ * 4. 超阈值 → 抛 ScreenshotDiffError(exceeded),差异区块/差异图/meta 齐全
14
+ * 5. 掩码生效:把差异区写进 meta.masks → matched(动态区域治理闭环)
15
+ * 6. 视口变化:同 key 其它视口基线存在 → size_mismatch(不自举新基线 / A20)
16
+ * 7. goal 捕获缓冲:begin → record → drain 语义
17
+ */
18
+
19
+ import * as fs from 'node:fs';
20
+ import * as os from 'node:os';
21
+ import * as path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import * as zlib from 'node:zlib';
24
+
25
+ // ── 隔离环境:基线/产物写进临时目录,绝不污染真实 baselines ──
26
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'keve-shotdiff-verify-'));
27
+ process.env.KEVE_BASELINE_DIR = path.join(tmp, 'baselines');
28
+ process.env.KEVE_TASK_DIR = path.join(tmp, 'task');
29
+ delete process.env.KEVE_ENV; // 默认 test
30
+ process.env.KEVE_SHOT_DIFF_THRESHOLD = '1.0';
31
+
32
+ const here = path.dirname(fileURLToPath(import.meta.url));
33
+ const dist = path.resolve(here, '../dist/core');
34
+ let decodePng, encodePng, screenshotDiff;
35
+ try {
36
+ ({ decodePng, encodePng } = await import(`${dist}/pngCodec.js`));
37
+ screenshotDiff = await import(`${dist}/screenshotDiff.js`);
38
+ } catch (err) {
39
+ console.error('✗ 无法加载 dist 产物 —— 请先构建:cd EVE/packages/suite && npm run build');
40
+ console.error(` (${err.message})`);
41
+ process.exit(2);
42
+ }
43
+
44
+ let passed = 0, failed = 0;
45
+ function ok(name, cond, extra = '') {
46
+ if (cond) { passed++; console.log(` ✓ ${name}`); }
47
+ else { failed++; console.error(` ✗ ${name}${extra ? ` — ${extra}` : ''}`); }
48
+ }
49
+
50
+ // ── 造图工具 ──
51
+
52
+ const W = 96, H = 64;
53
+ function fillImage(w, h, fn) {
54
+ const data = new Uint8Array(w * h * 4);
55
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
56
+ const [r, g, b, a = 255] = fn(x, y);
57
+ const i = (y * w + x) * 4;
58
+ data[i] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = a;
59
+ }
60
+ return data;
61
+ }
62
+ const solid = (r, g, b) => fillImage(W, H, () => [r, g, b]);
63
+ function withSquare(base, x0, y0, size, [r, g, b]) {
64
+ const out = Uint8Array.from(base);
65
+ for (let y = y0; y < y0 + size; y++) for (let x = x0; x < x0 + size; x++) {
66
+ const i = (y * W + x) * 4;
67
+ out[i] = r; out[i + 1] = g; out[i + 2] = b;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ // ── 手工组 PNG(验证解码器对滤波 1-4 的支持;encodePng 只产滤波 0) ──
73
+
74
+ function paeth(a, b, c) {
75
+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
76
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
77
+ }
78
+ const CRC_TABLE = (() => {
79
+ const t = new Uint32Array(256);
80
+ for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; }
81
+ return t;
82
+ })();
83
+ const crc32 = (buf) => { let c = 0xffffffff; for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; };
84
+ function chunk(type, data) {
85
+ const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
86
+ const t = Buffer.from(type, 'ascii');
87
+ const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([t, data])));
88
+ return Buffer.concat([len, t, data, crc]);
89
+ }
90
+ /** raw RGBA + 每行滤波类型 → 合法 PNG Buffer(colorType 6, 8bit) */
91
+ function craftPng(w, h, raw, filters) {
92
+ const bpp = 4, stride = w * bpp;
93
+ const body = Buffer.alloc((stride + 1) * h);
94
+ let prev = new Uint8Array(stride);
95
+ for (let y = 0; y < h; y++) {
96
+ const f = filters[y % filters.length];
97
+ body[y * (stride + 1)] = f;
98
+ for (let i = 0; i < stride; i++) {
99
+ const x = raw[y * stride + i];
100
+ const a = i >= bpp ? raw[y * stride + i - bpp] : 0;
101
+ const b = prev[i];
102
+ const c = i >= bpp ? prev[i - bpp] : 0;
103
+ let v;
104
+ if (f === 0) v = x;
105
+ else if (f === 1) v = x - a;
106
+ else if (f === 2) v = x - b;
107
+ else if (f === 3) v = x - ((a + b) >> 1);
108
+ else v = x - paeth(a, b, c);
109
+ body[y * (stride + 1) + 1 + i] = v & 0xff;
110
+ }
111
+ prev = raw.slice(y * stride, (y + 1) * stride);
112
+ }
113
+ const ihdr = Buffer.alloc(13);
114
+ ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
115
+ ihdr[8] = 8; ihdr[9] = 6;
116
+ return Buffer.concat([
117
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
118
+ chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(body)), chunk('IEND', Buffer.alloc(0)),
119
+ ]);
120
+ }
121
+
122
+ // ═══ 1. 编解码往返 ═══
123
+ console.log('\n[1] pngCodec 往返');
124
+ {
125
+ const img = solid(200, 30, 90);
126
+ const dec = decodePng(encodePng(W, H, img));
127
+ ok('RGBA 往返像素一致', Buffer.compare(Buffer.from(dec.data), Buffer.from(img)) === 0);
128
+ const rgb = fillImage(W, H, (x) => [x & 0xff, (x * 3) & 0xff, 7]); // 非纯色
129
+ const dec2 = decodePng(encodePng(W, H, rgb));
130
+ ok('非纯色往返像素一致', Buffer.compare(Buffer.from(dec2.data), Buffer.from(rgb)) === 0);
131
+ let threw = false;
132
+ try { decodePng(Buffer.from('not a png')); } catch { threw = true; }
133
+ ok('非 PNG 输入抛错', threw);
134
+ }
135
+
136
+ // ═══ 2. 滤波 1-4 解码 ═══
137
+ console.log('\n[2] 解码器滤波支持(Playwright 截图会用任意滤波)');
138
+ {
139
+ const raw = fillImage(W, H, (x, y) => [(x * 2) & 0xff, (y * 3) & 0xff, (x + y) & 0xff]);
140
+ const filters = [1, 2, 3, 4]; // Sub / Up / Average / Paeth 逐行轮换
141
+ const dec = decodePng(craftPng(W, H, raw, filters));
142
+ ok('滤波 1-4 混合解码像素一致', Buffer.compare(Buffer.from(dec.data), Buffer.from(raw)) === 0);
143
+ }
144
+
145
+ // ═══ 3-5. 基线生命周期 ═══
146
+ const { createBaseline, compareWithBaseline, ScreenshotDiffError, beginGoalShotDiffCapture, drainGoalShotDiff, resolveBaselinePaths } = screenshotDiff;
147
+ const KEY = 'step-24497';
148
+ const baselinePng = () => encodePng(W, H, solid(200, 30, 90));
149
+ const samePng = () => encodePng(W, H, solid(200, 30, 90));
150
+
151
+ console.log('\n[3] 首跑:无基线 → baseline_required(候选基线落盘,不判通过)');
152
+ let created;
153
+ {
154
+ beginGoalShotDiffCapture();
155
+ created = createBaseline(KEY, baselinePng());
156
+ ok('status=baseline_required', created.status === 'baseline_required', `实际 ${created.status}`);
157
+ ok('diffRatio=null(无差异率可言)', created.diffRatio === null, `实际 ${created.diffRatio}`);
158
+ ok('基线文件已写盘', fs.existsSync(created.baselinePath));
159
+ const meta = JSON.parse(fs.readFileSync(created.baselinePath.replace(/\.png$/, '.json'), 'utf-8'));
160
+ ok('meta:v1 / candidate / first-run', meta.version === 1 && meta.state === 'candidate' && meta.sourceType === 'first-run');
161
+ const paths = resolveBaselinePaths(KEY, W, H);
162
+ ok('key+env+viewport 进基线文件名', paths.pngPath.endsWith(`step-24497.test.${W}x${H}.png`));
163
+ }
164
+
165
+ console.log('\n[4] 候选门控:未审核再跑 → 仍 baseline_required(不比对不判过 / A06)');
166
+ {
167
+ const r = compareWithBaseline(KEY, samePng());
168
+ ok('status=baseline_required', r.status === 'baseline_required', `实际 ${r.status}`);
169
+ ok('diffRatio=null', r.diffRatio === null, `实际 ${r.diffRatio}`);
170
+ ok('基线版本未推进(v1)', r.baselineVersion === 1, `实际 v${r.baselineVersion}`);
171
+ }
172
+
173
+ console.log('\n[5] 模拟基线审核转正(meta.state candidate → active)');
174
+ {
175
+ const metaPath = resolveBaselinePaths(KEY, W, H).metaPath;
176
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
177
+ meta.state = 'active';
178
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
179
+ ok('meta.state 已置为 active', true);
180
+ }
181
+
182
+ console.log('\n[6] 转正后二跑 identical → matched');
183
+ {
184
+ beginGoalShotDiffCapture();
185
+ const r = compareWithBaseline(KEY, samePng());
186
+ ok('status=matched', r.status === 'matched');
187
+ ok('message 含差异率与阈值', /差异率.*阈值/.test(r.message));
188
+ ok('基线状态 active', r.baselineState === 'active', `实际 ${r.baselineState}`);
189
+ const drained = drainGoalShotDiff();
190
+ ok('goal 缓冲收集到 1 条', drained.length === 1 && drained[0].status === 'matched');
191
+ }
192
+
193
+ console.log('\n[7] 超阈值 → ScreenshotDiffError(确定性 fail)');
194
+ let diffResult;
195
+ {
196
+ const changed = encodePng(W, H, withSquare(solid(200, 30, 90), 8, 8, 24, [20, 90, 230]));
197
+ beginGoalShotDiffCapture();
198
+ let threw = false;
199
+ try { compareWithBaseline(KEY, changed); } catch (e) {
200
+ threw = e instanceof ScreenshotDiffError;
201
+ diffResult = e.shotDiff;
202
+ }
203
+ ok('抛 ScreenshotDiffError', threw);
204
+ ok('status=exceeded 且携带差异结果', diffResult?.status === 'exceeded');
205
+ ok('差异区块 ≥1 且坐标在图内', (diffResult?.regions?.length ?? 0) >= 1
206
+ && diffResult.regions.every(rg => rg.x >= 0 && rg.y >= 0 && rg.x + rg.w <= W && rg.y + rg.h <= H));
207
+ ok('差异图已落盘', diffResult?.diffPath && fs.existsSync(diffResult.diffPath) && diffResult.diffPath !== diffResult.actualPath);
208
+ ok('diffRatio 在 (1, 100] 区间', diffResult?.diffRatio > 1 && diffResult.diffRatio <= 100);
209
+ const drained = drainGoalShotDiff();
210
+ ok('异常路径下 goal 缓冲仍收集到', drained.length === 1 && drained[0].status === 'exceeded');
211
+ }
212
+
213
+ console.log('\n[8] 掩码生效:差异区写进 meta.masks → matched');
214
+ {
215
+ // 把 [5] 的主差异区写回 meta(模拟「忽略此区域」闭环)
216
+ const metaPath = resolveBaselinePaths(KEY, W, H).metaPath;
217
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
218
+ const rg = diffResult.regions[0];
219
+ meta.masks = [{ x: rg.x, y: rg.y, w: rg.w, h: rg.h }];
220
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
221
+ const changed = encodePng(W, H, withSquare(solid(200, 30, 90), 8, 8, 24, [20, 90, 230]));
222
+ const r = compareWithBaseline(KEY, changed);
223
+ ok('掩码覆盖差异区后 matched', r.status === 'matched', `实际 status=${r.status} ratio=${r.diffRatio}`);
224
+ }
225
+
226
+ console.log('\n[9] 视口变化:同 key 其它视口基线存在 → size_mismatch(不自举,杜绝掩盖变化 / A20)');
227
+ {
228
+ const other = encodePng(W, H + 16, fillImage(W, H + 16, () => [200, 30, 90]));
229
+ beginGoalShotDiffCapture();
230
+ const r = compareWithBaseline(KEY, other);
231
+ ok('不同视口 → size_mismatch', r.status === 'size_mismatch', `实际 ${r.status}`);
232
+ ok('diffRatio=null(无差异率可言)', r.diffRatio === null, `实际 ${r.diffRatio}`);
233
+ ok('未自举新视口基线(96x80 文件不存在)', !fs.existsSync(resolveBaselinePaths(KEY, W, H + 16).pngPath));
234
+ const drained = drainGoalShotDiff();
235
+ ok('goal 缓冲收集到 size_mismatch', drained.length === 1 && drained[0].status === 'size_mismatch');
236
+ // 原视口基线不受影响:同尺寸二跑仍 matched
237
+ const r2 = compareWithBaseline(KEY, samePng());
238
+ ok('原视口基线不受污染(仍 matched)', r2.status === 'matched');
239
+ }
240
+
241
+ console.log(`\n结果:${passed} 通过 / ${failed} 失败`);
242
+ console.log(`(临时目录:${tmp},验证后保留供人工查看)`);
243
+ process.exit(failed ? 1 : 0);
@@ -1,16 +0,0 @@
1
- /**
2
- * keve auth-update - Update auth storage state (cookies/localStorage)
3
- *
4
- * Opens a browser for the user to manually log in, then saves the
5
- * storage state to `.auth/storage-state.json`.
6
- *
7
- * Usage:
8
- * npx keve auth-update
9
- * npx keve auth-update --url http://localhost:3000
10
- * npx keve auth-update --config .keve/keve-test.config.ts
11
- */
12
- export interface AuthUpdateOptions {
13
- url?: string;
14
- config?: string;
15
- }
16
- export declare function authUpdate(options?: AuthUpdateOptions): Promise<void>;
@@ -1,88 +0,0 @@
1
- "use strict";
2
- /**
3
- * keve auth-update - Update auth storage state (cookies/localStorage)
4
- *
5
- * Opens a browser for the user to manually log in, then saves the
6
- * storage state to `.auth/storage-state.json`.
7
- *
8
- * Usage:
9
- * npx keve auth-update
10
- * npx keve auth-update --url http://localhost:3000
11
- * npx keve auth-update --config .keve/keve-test.config.ts
12
- */
13
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
- if (k2 === undefined) k2 = k;
15
- var desc = Object.getOwnPropertyDescriptor(m, k);
16
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
- desc = { enumerable: true, get: function() { return m[k]; } };
18
- }
19
- Object.defineProperty(o, k2, desc);
20
- }) : (function(o, m, k, k2) {
21
- if (k2 === undefined) k2 = k;
22
- o[k2] = m[k];
23
- }));
24
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
- Object.defineProperty(o, "default", { enumerable: true, value: v });
26
- }) : function(o, v) {
27
- o["default"] = v;
28
- });
29
- var __importStar = (this && this.__importStar) || (function () {
30
- var ownKeys = function(o) {
31
- ownKeys = Object.getOwnPropertyNames || function (o) {
32
- var ar = [];
33
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
- return ar;
35
- };
36
- return ownKeys(o);
37
- };
38
- return function (mod) {
39
- if (mod && mod.__esModule) return mod;
40
- var result = {};
41
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
- __setModuleDefault(result, mod);
43
- return result;
44
- };
45
- })();
46
- var __importDefault = (this && this.__importDefault) || function (mod) {
47
- return (mod && mod.__esModule) ? mod : { "default": mod };
48
- };
49
- Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.authUpdate = authUpdate;
51
- const chalk_1 = __importDefault(require("chalk"));
52
- const path = __importStar(require("path"));
53
- const auth_1 = require("../core/auth");
54
- async function authUpdate(options = {}) {
55
- const targetUrl = options.url || process.env.PWGEN_TARGET_URL || 'http://localhost:3000';
56
- const storageStatePath = path.resolve(process.cwd(), '.auth/storage-state.json');
57
- console.log(chalk_1.default.cyan('\n keve auth-update'));
58
- console.log(chalk_1.default.gray(` Target URL: ${targetUrl}`));
59
- console.log(chalk_1.default.gray(` Storage state: ${storageStatePath}`));
60
- console.log(chalk_1.default.yellow('\n Opening browser for manual login...'));
61
- console.log(chalk_1.default.gray(' (Auto-save when you navigate away from login page)\n'));
62
- const success = await (0, auth_1.acquireStorageState)({
63
- targetUrl,
64
- storageStatePath,
65
- logPrefix: ' [keve auth-update]',
66
- onLoginDetected: (url) => console.log(chalk_1.default.green(` Login detected! URL: ${url}`)),
67
- onStateSaved: (cookieCount, originCount) => {
68
- console.log(chalk_1.default.green(`\n ✓ Auth state saved`));
69
- console.log(chalk_1.default.gray(` Cookies: ${cookieCount}, localStorage origins: ${originCount}`));
70
- if (cookieCount === 0) {
71
- console.log(chalk_1.default.yellow(' ⚠ No cookies saved — login may not have completed.'));
72
- console.log(chalk_1.default.yellow(' Try `npx keve auth-update` again.'));
73
- }
74
- else {
75
- console.log(chalk_1.default.green(' ✓ Auth update complete! Run tests with: npx keve run'));
76
- }
77
- },
78
- onError: (msg) => console.error(chalk_1.default.red(` Error: ${msg}`)),
79
- onBrowserFailed: (msg) => {
80
- console.error(chalk_1.default.red(` Cannot launch browser: ${msg}`));
81
- console.error(chalk_1.default.yellow(' Ensure a display is available, or run in a local environment.'));
82
- },
83
- });
84
- if (!success && !options.url) {
85
- console.log(chalk_1.default.yellow('\n Hint: Specify target URL if different from default:'));
86
- console.log(chalk_1.default.yellow(' npx keve auth-update --url http://your-app-url'));
87
- }
88
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Auth utilities — acquire storage state for Playwright tests
3
- *
4
- * 1. Existing valid storage-state.json → skip
5
- * 2. CDP connect to user Chrome → reuse login state
6
- * 3. Manual browser login → fallback
7
- */
8
- export interface AcquireOptions {
9
- targetUrl?: string;
10
- storageStatePath?: string;
11
- logPrefix?: string;
12
- onLoginDetected?: (url: string) => void;
13
- onStateSaved?: (cookieCount: number, originCount: number) => void;
14
- onError?: (msg: string) => void;
15
- onBrowserFailed?: (msg: string) => void;
16
- }
17
- export declare function acquireStorageState(options?: AcquireOptions): Promise<boolean>;
package/dist/core/auth.js DELETED
@@ -1,224 +0,0 @@
1
- "use strict";
2
- /**
3
- * Auth utilities — acquire storage state for Playwright tests
4
- *
5
- * 1. Existing valid storage-state.json → skip
6
- * 2. CDP connect to user Chrome → reuse login state
7
- * 3. Manual browser login → fallback
8
- */
9
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- var desc = Object.getOwnPropertyDescriptor(m, k);
12
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
- desc = { enumerable: true, get: function() { return m[k]; } };
14
- }
15
- Object.defineProperty(o, k2, desc);
16
- }) : (function(o, m, k, k2) {
17
- if (k2 === undefined) k2 = k;
18
- o[k2] = m[k];
19
- }));
20
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
- Object.defineProperty(o, "default", { enumerable: true, value: v });
22
- }) : function(o, v) {
23
- o["default"] = v;
24
- });
25
- var __importStar = (this && this.__importStar) || (function () {
26
- var ownKeys = function(o) {
27
- ownKeys = Object.getOwnPropertyNames || function (o) {
28
- var ar = [];
29
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
- return ar;
31
- };
32
- return ownKeys(o);
33
- };
34
- return function (mod) {
35
- if (mod && mod.__esModule) return mod;
36
- var result = {};
37
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
- __setModuleDefault(result, mod);
39
- return result;
40
- };
41
- })();
42
- Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.acquireStorageState = acquireStorageState;
44
- const fs = __importStar(require("node:fs"));
45
- const os = __importStar(require("node:os"));
46
- const path = __importStar(require("node:path"));
47
- const node_child_process_1 = require("node:child_process"); // getChromeWsUrl 需要用 lsof 检查端口
48
- const node_module_1 = require("node:module");
49
- // ─── Playwright Chromium resolver ───────────────────────────────────────────
50
- async function getChromium() {
51
- const cwdRequire = (0, node_module_1.createRequire)(path.resolve(process.cwd(), 'node_modules'));
52
- const pwAbsolutePath = cwdRequire.resolve('@playwright/test');
53
- const pwModule = await Promise.resolve(`${pwAbsolutePath}`).then(s => __importStar(require(s)));
54
- const chromium = pwModule.chromium || pwModule.default?.chromium;
55
- if (!chromium)
56
- throw new Error('Failed to resolve chromium from @playwright/test');
57
- return chromium;
58
- }
59
- // ─── Chrome CDP helpers ─────────────────────────────────────────────────────
60
- function getChromeUserDataDir() {
61
- const p = os.platform();
62
- const h = os.homedir();
63
- if (p === 'darwin')
64
- return path.join(h, 'Library', 'Application Support', 'Google', 'Chrome');
65
- if (p === 'win32')
66
- return path.join(h, 'AppData', 'Local', 'Google', 'Chrome', 'User Data');
67
- return path.join(h, '.config', 'google-chrome');
68
- }
69
- /** 读取 DevToolsActivePort 获取 CDP WebSocket URL(端口须正在监听) */
70
- function getChromeWsUrl() {
71
- const portFile = path.join(getChromeUserDataDir(), 'DevToolsActivePort');
72
- if (!fs.existsSync(portFile))
73
- return null;
74
- try {
75
- const [portStr, wsPath] = fs.readFileSync(portFile, 'utf8').trim().split('\n').map(l => l.trim()).filter(Boolean);
76
- const port = parseInt(portStr, 10);
77
- if (!port || !wsPath)
78
- return null;
79
- try {
80
- (0, node_child_process_1.execSync)(`lsof -P -i TCP:${port} 2>/dev/null | grep LISTEN`, { encoding: 'utf8' });
81
- }
82
- catch {
83
- return null;
84
- }
85
- return `ws://127.0.0.1:${port}${wsPath}`;
86
- }
87
- catch {
88
- return null;
89
- }
90
- }
91
- /** CDP 连接用户 Chrome → 打开目标页 → 保存 storage-state → 断开
92
- * 仅连接已运行的 Chrome(keve run 已确保 Chrome 启动 + debug 端口就绪)
93
- */
94
- async function authViaCDP(targetUrl, storageStatePath, prefix) {
95
- const wsUrl = getChromeWsUrl();
96
- if (!wsUrl) {
97
- console.log(`${prefix} Chrome CDP not available`);
98
- return false;
99
- }
100
- console.log(`${prefix} Chrome CDP available, connecting...`);
101
- try {
102
- const chromium = await getChromium();
103
- const browser = await chromium.connectOverCDP(wsUrl, { timeout: 10000 });
104
- const context = browser.contexts()[0];
105
- if (!context) {
106
- browser.close();
107
- return false;
108
- }
109
- // 访问目标页面,等登录重定向完成
110
- const page = await context.newPage();
111
- await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 60000 });
112
- await waitForLoginRedirect(page);
113
- const onLoginPage = /login|auth|signin/i.test(page.url());
114
- if (onLoginPage) {
115
- console.log(`${prefix} CDP connected but target requires login`);
116
- await page.close();
117
- browser.close();
118
- return false;
119
- }
120
- // 保存 storage-state
121
- const cookies = await context.cookies();
122
- const state = { cookies, origins: [] };
123
- fs.writeFileSync(storageStatePath, JSON.stringify(state, null, 2));
124
- await page.close();
125
- browser.close();
126
- console.log(`${prefix} CDP auth success (${cookies.length} cookies) → ${storageStatePath}`);
127
- return cookies.length > 0;
128
- }
129
- catch (e) {
130
- console.warn(`${prefix} CDP connect failed: ${e.message}`);
131
- return false;
132
- }
133
- }
134
- function isStorageStateValid(statePath, targetDomain) {
135
- if (!fs.existsSync(statePath))
136
- return false;
137
- try {
138
- const s = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
139
- if (!s.cookies?.length)
140
- return false;
141
- if (targetDomain && !s.cookies.some(c => c.domain?.includes(targetDomain)))
142
- return false;
143
- const now = Date.now() / 1000;
144
- return s.cookies.some(c => !c.expires || c.expires === -1 || c.expires > now);
145
- }
146
- catch {
147
- return false;
148
- }
149
- }
150
- function extractDomain(url) {
151
- try {
152
- return new URL(url).hostname;
153
- }
154
- catch {
155
- return undefined;
156
- }
157
- }
158
- async function waitForLoginRedirect(page, maxMs = 30000) {
159
- for (let i = 0; i < maxMs / 500; i++) {
160
- if (!/login|auth|signin/i.test(page.url()))
161
- break;
162
- await page.waitForTimeout(500);
163
- }
164
- await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => { });
165
- return !/login|auth|signin|sign-in/i.test(page.url());
166
- }
167
- /** 手动登录 → 保存 storage-state */
168
- async function authViaManualLogin(targetUrl, storageStatePath, prefix) {
169
- const loginUrl = targetUrl.replace(/\/?$/, '/login');
170
- console.log(`${prefix} Opening browser for manual login: ${loginUrl}`);
171
- console.log(`${prefix} Please login. Waiting up to 2 minutes...`);
172
- try {
173
- const browser = await (await getChromium()).launch({ headless: false });
174
- const context = await browser.newContext();
175
- const page = await context.newPage();
176
- await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 15000 })
177
- .catch(() => page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 15000 }));
178
- let loginDetected = false;
179
- for (let i = 0; i < 120; i++) {
180
- if (!/login|auth|signin|sign-in/i.test(page.url())) {
181
- loginDetected = true;
182
- break;
183
- }
184
- await page.waitForTimeout(1000);
185
- }
186
- if (!loginDetected)
187
- console.log(`${prefix} Timeout (2min), saving current state...`);
188
- await waitForLoginRedirect(page);
189
- await context.storageState({ path: storageStatePath });
190
- const c = JSON.parse(fs.readFileSync(storageStatePath, 'utf-8')).cookies?.length || 0;
191
- console.log(`${prefix} Auth saved (${c} cookies) → ${storageStatePath}`);
192
- await browser.close();
193
- return c > 0;
194
- }
195
- catch (e) {
196
- console.error(`${prefix} Manual login error: ${e.message}`);
197
- return false;
198
- }
199
- }
200
- async function acquireStorageState(options = {}) {
201
- const targetUrl = options.targetUrl || process.env.PWGEN_TARGET_URL || 'http://localhost:3000';
202
- const storageStatePath = options.storageStatePath || path.resolve(process.cwd(), '.auth/storage-state.json');
203
- const prefix = options.logPrefix || '[keve]';
204
- const targetDomain = extractDomain(targetUrl);
205
- const authDir = path.dirname(storageStatePath);
206
- if (!fs.existsSync(authDir))
207
- fs.mkdirSync(authDir, { recursive: true });
208
- // Skip mode
209
- if (process.env.PWGEN_SKIP_AUTH === '1' || process.env.PWGEN_SKIP_AUTH === 'true') {
210
- fs.writeFileSync(storageStatePath, JSON.stringify({ cookies: [], origins: [] }, null, 2));
211
- console.log(`${prefix} PWGEN_SKIP_AUTH=1 → empty storage state saved`);
212
- return true;
213
- }
214
- // Step 1: 已有有效 storage-state
215
- if (isStorageStateValid(storageStatePath, targetDomain)) {
216
- console.log(`${prefix} Storage state valid → ${storageStatePath}`);
217
- return true;
218
- }
219
- // Step 2: CDP 连接用户 Chrome
220
- if (await authViaCDP(targetUrl, storageStatePath, prefix))
221
- return true;
222
- // Step 3: 手动登录
223
- return await authViaManualLogin(targetUrl, storageStatePath, prefix);
224
- }
@@ -1,5 +0,0 @@
1
- export interface CdpResolverOptions {
2
- endpoint?: string;
3
- wsEndpoint?: string;
4
- }
5
- export declare function resolveCdpWsUrl(options: CdpResolverOptions): Promise<string | null>;