@kkeve/suite 0.0.15 → 0.0.18

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 (115) 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 +12 -11
  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 +49 -15
  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 +204 -200
  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 +82 -7
  82. package/dist/page-agent/agent.js +252 -88
  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/tools.d.ts +1 -1
  94. package/dist/page-agent/tools.js +144 -8
  95. package/package.json +24 -11
  96. package/scripts/gen-system-prompt.mjs +36 -0
  97. package/scripts/run-dsl-task.mjs +652 -0
  98. package/scripts/runtime/cypress-preprocessor.mjs +44 -0
  99. package/scripts/runtime/cypress.config.mjs +35 -0
  100. package/scripts/runtime/playwright.config.mjs +41 -0
  101. package/scripts/verify-shot-diff.mjs +243 -0
  102. package/dist/commands/auth-update.d.ts +0 -16
  103. package/dist/commands/auth-update.js +0 -88
  104. package/dist/core/auth.d.ts +0 -17
  105. package/dist/core/auth.js +0 -224
  106. package/dist/core/cdp-resolver.d.ts +0 -5
  107. package/dist/core/cdp-resolver.js +0 -93
  108. package/dist/core/global-setup.d.ts +0 -6
  109. package/dist/core/global-setup.js +0 -123
  110. package/dist/core/keve-decorators.d.ts +0 -51
  111. package/dist/core/keve-decorators.js +0 -95
  112. package/dist/core/keve-report.js +0 -376
  113. /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
  114. /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
  115. /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cypress TypeScript 预处理器(静态模板)
3
+ *
4
+ * esbuild 由 eve-backend 声明并安装,通过 KEVE_ESBUILD_MODULE 指向其入口,
5
+ * 避免任务目录再生成配置文件或软链依赖。
6
+ */
7
+ import { createRequire } from 'node:module';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const esbuildEntry = process.env.KEVE_ESBUILD_MODULE;
11
+ if (!esbuildEntry) {
12
+ throw new Error('缺少 KEVE_ESBUILD_MODULE,无法定位 esbuild 入口');
13
+ }
14
+ const esbuild = require(esbuildEntry);
15
+
16
+ export async function preprocess(file) {
17
+ await esbuild.build({
18
+ entryPoints: [file.filePath],
19
+ outfile: file.outputPath,
20
+ bundle: true,
21
+ format: 'iife',
22
+ platform: 'browser',
23
+ target: 'chrome100',
24
+ sourcemap: 'inline',
25
+ logLevel: 'warning',
26
+ // 任务目录位于 eve-backend/ 下,esbuild 会自动继承后端的 tsconfig.json,
27
+ // 而那里开了 experimentalDecorators: true —— 会把 keveScene/keveModel 降级成
28
+ // 旧式 (target, key, descriptor) 调用,但装饰器实现走的是 TC39 标准签名
29
+ // (value, context),运行期直接报 context.addInitializer is not a function。
30
+ // 这里显式覆盖,保证与 suite 自身 tsconfig 的装饰器语义一致。
31
+ tsconfigRaw: {
32
+ compilerOptions: {
33
+ experimentalDecorators: false,
34
+ useDefineForClassFields: false,
35
+ },
36
+ },
37
+ define: {
38
+ 'process.env.KEVE_ENV': JSON.stringify(process.env.KEVE_ENV || 'test'),
39
+ 'process.env.KEVE_CY_TEST_TIMEOUT': JSON.stringify(process.env.KEVE_CY_TEST_TIMEOUT || ''),
40
+ 'process.env.KEVE_TARGET_URL': JSON.stringify(process.env.KEVE_TARGET_URL || ''),
41
+ },
42
+ });
43
+ return file.outputPath;
44
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cypress 任务级配置(静态模板)
3
+ *
4
+ * 以 `--project <taskDir> --config-file <本文件>` 方式加载:
5
+ * 项目根仍是任务目录(specPattern 相对它解析),
6
+ * 轮次产物目录则通过 KEVE_RESULT_DIR 注入。
7
+ */
8
+ import * as path from 'node:path';
9
+ import * as url from 'node:url';
10
+ import { cypressSetup } from '../../dist/engine-cypress/setup.js';
11
+ import { preprocess } from './cypress-preprocessor.mjs';
12
+
13
+ const runtimeDir = path.dirname(url.fileURLToPath(import.meta.url));
14
+ const suiteRoot = path.resolve(runtimeDir, '..', '..');
15
+ const taskDir = process.env.KEVE_TASK_DIR || path.resolve(suiteRoot, '.keve', 'default');
16
+ const resultDir = process.env.KEVE_RESULT_DIR || path.join(taskDir, 'reports', 'round-latest');
17
+ const targetUrl = process.env.KEVE_TARGET_URL || '';
18
+
19
+ export default {
20
+ e2e: {
21
+ ...(targetUrl ? { baseUrl: targetUrl } : {}),
22
+ specPattern: 'specs/**/*.spec.ts',
23
+ supportFile: false,
24
+ video: true,
25
+ videosFolder: path.join(resultDir, 'videos'),
26
+ screenshotsFolder: path.join(resultDir, 'screenshots'),
27
+ downloadsFolder: path.join(resultDir, 'downloads'),
28
+ screenshotOnRunFailure: false,
29
+ defaultCommandTimeout: 10000,
30
+ setupNodeEvents(on, config) {
31
+ on('file:preprocessor', preprocess);
32
+ return cypressSetup(on, config);
33
+ },
34
+ },
35
+ };
@@ -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>;