dsh-browser-verify 0.1.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/lib/index.js ADDED
@@ -0,0 +1,470 @@
1
+ import { t as BrowserDriver } from "./driver-DxMldTYf.js";
2
+ import { exec } from "node:child_process";
3
+ import { readdir, rm, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { defineTool } from "@deepseek-ai/dsh-tools";
7
+ import { AttachmentError, AttachmentId } from "@deepseek-ai/dsh-attachment";
8
+ //#region src/cleanup.ts
9
+ /**
10
+ * Orphan cleanup for dsh-browser-verify: temp dirs and zombie Chromium
11
+ * processes left by crashes. Parsers are pure; callers do the I/O.
12
+ * @module dsh-browser-verify/cleanup
13
+ */
14
+ /** One line from `ps -Ao pid=,ppid=,command=` (macOS). */
15
+ function parseZombiePids(psText, prefix, selfPid) {
16
+ const pids = [];
17
+ for (const line of psText.split("\n")) {
18
+ const match = /^\s*(\d+)\s+\d+\s+(.+)$/.exec(line);
19
+ if (match === null) continue;
20
+ const pid = Number(match[1]);
21
+ if (pid === selfPid) continue;
22
+ if (match[2].includes(`--user-data-dir=${join(tmpdir(), prefix)}`)) pids.push(pid);
23
+ }
24
+ return pids;
25
+ }
26
+ /** Pick degraded-run temp dirs: name prefixed, old enough, not the current pid dir. */
27
+ function selectOrphanDirs(entries, nowMs, ageMs, prefix) {
28
+ return entries.filter((e) => e.path.includes(`/${prefix}`) && nowMs - e.mtimeMs > ageMs).sort((a, b) => b.mtimeMs - a.mtimeMs);
29
+ }
30
+ //#endregion
31
+ //#region src/attachments.ts
32
+ function imageRefFromValue(image) {
33
+ return {
34
+ attachmentId: AttachmentId(image.attachmentId),
35
+ mediaType: image.mediaType,
36
+ bytes: image.bytes,
37
+ width: image.width,
38
+ height: image.height,
39
+ ...image.name === void 0 ? {} : { name: image.name }
40
+ };
41
+ }
42
+ function renderScreenshotBlocks(value) {
43
+ const dup = value.identicalToPrevious ? "(与上一张截图哈希相同,疑似页面未刷新;请 browser_open 重开场景后重试)" : "";
44
+ return [{
45
+ type: "text",
46
+ text: `<type>screenshot</type>\n<content>\n${value.image.mediaType}, ${value.image.width}x${value.image.height} px, ${value.image.bytes} bytes, sha256 ${value.sha256.slice(0, 12)}${dup}\n</content>`
47
+ }, {
48
+ type: "image",
49
+ attachment: imageRefFromValue(value.image)
50
+ }];
51
+ }
52
+ /** Persist screenshot bytes, mapping store refusals to actionable errors. */
53
+ async function saveScreenshot(ctx, data, name) {
54
+ const attachments = ctx.get("attachments");
55
+ if (attachments === void 0) throw new Error("browser-verify: 附件存储未挂载,无法持久化截图。请检查当前 DSH 组合是否包含 attachment 插件。");
56
+ try {
57
+ return await attachments.saveImage({
58
+ data,
59
+ mediaType: "image/png",
60
+ ...name === void 0 ? {} : { name }
61
+ });
62
+ } catch (error) {
63
+ if (!(error instanceof AttachmentError)) throw error;
64
+ if (error.code === "IMAGE_TOO_LARGE") throw new Error("browser-verify: 截图超过 attachment 存储字节上限。请改用 fullPage:false 或调低 deviceScaleFactor 后重试。", { cause: error });
65
+ if (error.code === "IMAGE_DIMENSION_TOO_LARGE" || error.code === "IMAGE_TOO_MANY_PIXELS") throw new Error("browser-verify: 截图尺寸超过 attachment 存储限制。请改用 fullPage:false 或调低 deviceScaleFactor 后重试。", { cause: error });
66
+ if (error.code === "IMAGE_TYPE_MISMATCH") throw new Error(`browser-verify: 截图格式校验失败:${error.message} 请改用其他附件类型或重试。`, { cause: error });
67
+ throw error;
68
+ }
69
+ }
70
+ /** Gate: the calling route must be able to see image input (mirror of read-image). */
71
+ async function assertImageCapable(ctx, exec) {
72
+ const routed = exec.agent?.session?.requestHeader?.()?.config;
73
+ const provider = routed?.provider ?? exec.agent?.options?.provider;
74
+ const model = routed?.model ?? exec.agent?.options?.model;
75
+ const llm = ctx.get("llm");
76
+ if (provider === void 0 || model === void 0 || llm === void 0) throw new Error("browser-verify: 无法解析当前模型路由(或模型服务未挂载),无法判断图片输入能力。请检查当前会话的模型路由配置后重试。");
77
+ const active = await llm.resolveModelInfo(provider, model);
78
+ if (active.inputModalities === void 0 || !active.inputModalities.includes("image")) throw new Error("browser-verify: 当前模型不支持看图:请改用 browser_assert 做文本断言(更省 token),或切换到图片模型后重试。");
79
+ }
80
+ //#endregion
81
+ //#region src/tools/timeout.ts
82
+ /** Race a promise against a deadline; the loser's work is abandoned, not awaited. */
83
+ function withTimeout(promise, ms, label) {
84
+ return new Promise((resolve, reject) => {
85
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`browser-verify: ${label} 超时(${ms}ms)。请检查页面或调大 DSH_BROWSER_VERIFY_TIMEOUT 后重试。`)), ms);
86
+ promise.then((value) => {
87
+ clearTimeout(timer);
88
+ resolve(value);
89
+ }, (error) => {
90
+ clearTimeout(timer);
91
+ reject(error);
92
+ });
93
+ });
94
+ }
95
+ //#endregion
96
+ //#region src/tools/index.ts
97
+ /** Parse a positive-integer env var; NaN/zero/negative falls back to the default. */
98
+ const numberFromEnv = (name, fallback) => {
99
+ const value = Number(process.env[name] ?? fallback);
100
+ return Number.isFinite(value) && value > 0 ? value : fallback;
101
+ };
102
+ const envTimeoutMs = () => numberFromEnv("DSH_BROWSER_VERIFY_TIMEOUT", 1e4);
103
+ const envIdleMs = () => numberFromEnv("DSH_BROWSER_VERIFY_IDLE_MS", 6e5);
104
+ function registerBrowserTools(ctx) {
105
+ const driver = new BrowserDriver({
106
+ timeoutMs: envTimeoutMs(),
107
+ idleMs: envIdleMs()
108
+ });
109
+ ctx.effect(() => () => {
110
+ driver.dispose();
111
+ });
112
+ ctx.tools.register(defineTool({
113
+ name: "browser_open",
114
+ description: "在无头浏览器中打开一个页面并返回页面状态(标题/状态码/可见文本摘要/console 错误)用于验证前端页面;可选 waitSelector 等待关键元素出现,默认视口 390×844 @2x(移动端形态)。可传 mocks 在打开时拦截接口(用于启动即依赖接口数据的页面)。验证顺序:先 browser_assert 做 DOM 断言,确需看版式再 browser_screenshot。",
115
+ parameters: {
116
+ url: {
117
+ type: "string",
118
+ required: true,
119
+ description: "页面地址,如 http://localhost:5173/hweb/pages/..."
120
+ },
121
+ viewport: {
122
+ type: "object",
123
+ additionalProperties: false,
124
+ properties: {
125
+ width: {
126
+ type: "number",
127
+ required: true,
128
+ description: "视口宽"
129
+ },
130
+ height: {
131
+ type: "number",
132
+ required: true,
133
+ description: "视口高"
134
+ }
135
+ },
136
+ description: "视口尺寸 {width, height},默认 390x844"
137
+ },
138
+ deviceScaleFactor: {
139
+ type: "number",
140
+ description: "缩放比,默认 2"
141
+ },
142
+ mocks: {
143
+ type: "array",
144
+ items: {
145
+ type: "object",
146
+ additionalProperties: false,
147
+ properties: {
148
+ urlPattern: {
149
+ type: "string",
150
+ required: true,
151
+ description: "glob 模式,如 **/api/*.do*"
152
+ },
153
+ json: {
154
+ type: "json",
155
+ required: true,
156
+ description: "拦截响应体(任意 JSON)"
157
+ },
158
+ status: {
159
+ type: "number",
160
+ description: "响应状态码,默认 200"
161
+ }
162
+ }
163
+ },
164
+ description: "可选:页面启动前注册的接口拦截(glob urlPattern + json,如 [{urlPattern: \"**/api/*.do*\", json: {...}}])"
165
+ },
166
+ waitSelector: {
167
+ type: "string",
168
+ description: "可选:等待该选择器出现后再返回(优先于固定等待)"
169
+ },
170
+ timeoutMs: {
171
+ type: "number",
172
+ description: `加载超时,默认 ${envTimeoutMs()}ms`
173
+ }
174
+ },
175
+ output: {
176
+ schema: {
177
+ type: "object",
178
+ additionalProperties: false,
179
+ properties: {
180
+ title: {
181
+ type: "string",
182
+ required: true
183
+ },
184
+ url: {
185
+ type: "string",
186
+ required: true
187
+ },
188
+ status: {
189
+ oneOf: [{ type: "number" }, { type: "null" }],
190
+ required: true
191
+ },
192
+ visible: {
193
+ type: "array",
194
+ items: { type: "string" },
195
+ required: true
196
+ },
197
+ consoleErrors: {
198
+ type: "array",
199
+ items: { type: "string" },
200
+ required: true
201
+ },
202
+ elapsedMs: {
203
+ type: "number",
204
+ required: true
205
+ },
206
+ browserKnown: {
207
+ type: "boolean",
208
+ required: true
209
+ },
210
+ versionHint: {
211
+ oneOf: [{ type: "string" }, { type: "null" }],
212
+ required: true,
213
+ description: "浏览器 revision 认证提示(null=认证通过)"
214
+ }
215
+ }
216
+ },
217
+ render: (_args, value) => [{
218
+ type: "text",
219
+ text: JSON.stringify(value)
220
+ }]
221
+ },
222
+ async execute(args) {
223
+ const timeoutMs = args.timeoutMs ?? envTimeoutMs();
224
+ return withTimeout(driver.startScenario({
225
+ url: args.url,
226
+ waitSelector: args.waitSelector,
227
+ timeoutMs,
228
+ viewport: args.viewport,
229
+ deviceScaleFactor: args.deviceScaleFactor,
230
+ mocks: args.mocks
231
+ }), timeoutMs, "browser_open");
232
+ }
233
+ }));
234
+ ctx.tools.register(defineTool({
235
+ name: "browser_mock",
236
+ description: "为当前验证场景注册接口拦截并自动重新加载页面:urlPattern 用 playwright glob(如 **/api/lifeIndex.do*),拦截后返回指定 json,用于 mock 空态/异常态。与已注册 pattern 完全相同时报错;请先 browser_open。",
237
+ parameters: {
238
+ urlPattern: {
239
+ type: "string",
240
+ required: true,
241
+ description: "glob 模式,如 **/api/lifeIndex.do*"
242
+ },
243
+ json: {
244
+ type: "json",
245
+ description: "拦截响应体(任意 JSON)",
246
+ required: true
247
+ },
248
+ status: {
249
+ type: "number",
250
+ description: "响应状态码,默认 200"
251
+ },
252
+ reload: {
253
+ type: "boolean",
254
+ description: "注册后自动 reload 当前页,默认 true"
255
+ }
256
+ },
257
+ output: {
258
+ schema: {
259
+ type: "object",
260
+ additionalProperties: false,
261
+ properties: { patterns: {
262
+ type: "array",
263
+ items: { type: "string" },
264
+ required: true
265
+ } }
266
+ },
267
+ render: (_args, value) => [{
268
+ type: "text",
269
+ text: JSON.stringify(value)
270
+ }]
271
+ },
272
+ async execute(args) {
273
+ return driver.withScenario(async (scenario) => ({ patterns: await scenario.addMock({
274
+ urlPattern: args.urlPattern,
275
+ json: args.json,
276
+ status: args.status,
277
+ reload: args.reload,
278
+ timeoutMs: envTimeoutMs()
279
+ }) }));
280
+ }
281
+ }));
282
+ ctx.tools.register(defineTool({
283
+ name: "browser_assert",
284
+ description: "对当前页面 DOM 断言:selector 必须存在,可校验匹配数量(count,数字或 {min,max})与文本包含(text)。不满足时返回 pass:false 并附差异、不抛错。这是最省 token 的验证手段,优先于截图。",
285
+ parameters: {
286
+ selector: {
287
+ type: "string",
288
+ required: true,
289
+ description: "CSS 选择器"
290
+ },
291
+ count: {
292
+ oneOf: [{ type: "number" }, {
293
+ type: "object",
294
+ additionalProperties: false,
295
+ properties: {
296
+ min: {
297
+ type: "number",
298
+ required: true
299
+ },
300
+ max: {
301
+ type: "number",
302
+ required: true
303
+ }
304
+ }
305
+ }],
306
+ description: "期望匹配数量:数字=精确,或 {min,max}=范围"
307
+ },
308
+ text: {
309
+ type: "string",
310
+ description: "期望包含于首个匹配元素文本(contains 谓词)"
311
+ },
312
+ timeoutMs: {
313
+ type: "number",
314
+ description: "等待选择器出现的超时,默认 5000ms"
315
+ }
316
+ },
317
+ output: {
318
+ schema: {
319
+ type: "object",
320
+ additionalProperties: false,
321
+ properties: {
322
+ pass: {
323
+ type: "boolean",
324
+ required: true
325
+ },
326
+ count: {
327
+ type: "number",
328
+ required: true
329
+ },
330
+ actualText: {
331
+ oneOf: [{ type: "string" }, { type: "null" }],
332
+ required: true
333
+ },
334
+ elapsedMs: {
335
+ type: "number",
336
+ required: true
337
+ }
338
+ }
339
+ },
340
+ render: (_args, value) => [{
341
+ type: "text",
342
+ text: JSON.stringify(value)
343
+ }]
344
+ },
345
+ async execute(args) {
346
+ return driver.withScenario((scenario) => scenario.assert({
347
+ selector: args.selector,
348
+ count: args.count,
349
+ text: args.text,
350
+ timeoutMs: args.timeoutMs ?? 5e3
351
+ }));
352
+ }
353
+ }));
354
+ ctx.tools.register(defineTool({
355
+ name: "browser_screenshot",
356
+ description: "截图当前页面并自动投影进模型上下文(图片块),返回尺寸与哈希;与上一张完全一致时 identicalToPrevious:true(疑似页面未刷新,请 browser_open 重开)。仅需要检查版式时使用——能断言就别截图。",
357
+ parameters: {
358
+ name: {
359
+ type: "string",
360
+ description: "可选命名(进入附件名)"
361
+ },
362
+ fullPage: {
363
+ type: "boolean",
364
+ description: "是否整页截图,默认 false"
365
+ }
366
+ },
367
+ output: {
368
+ schema: {
369
+ type: "object",
370
+ additionalProperties: false,
371
+ properties: {
372
+ image: {
373
+ type: "object",
374
+ additionalProperties: false,
375
+ required: true,
376
+ properties: {
377
+ attachmentId: {
378
+ type: "string",
379
+ required: true
380
+ },
381
+ mediaType: {
382
+ type: "string",
383
+ enum: ["image/png"],
384
+ required: true
385
+ },
386
+ bytes: {
387
+ type: "integer",
388
+ required: true
389
+ },
390
+ width: {
391
+ type: "integer",
392
+ required: true
393
+ },
394
+ height: {
395
+ type: "integer",
396
+ required: true
397
+ },
398
+ name: { type: "string" }
399
+ }
400
+ },
401
+ sha256: {
402
+ type: "string",
403
+ required: true
404
+ },
405
+ identicalToPrevious: {
406
+ type: "boolean",
407
+ required: true
408
+ }
409
+ }
410
+ },
411
+ render: (_args, value) => renderScreenshotBlocks(value)
412
+ },
413
+ async execute(args, exec) {
414
+ await assertImageCapable(ctx, exec);
415
+ return driver.withScenario(async (scenario) => {
416
+ const shot = await scenario.screenshot({ fullPage: args.fullPage });
417
+ const ref = await saveScreenshot(ctx, shot.data, args.name);
418
+ return {
419
+ image: {
420
+ attachmentId: String(ref.attachmentId),
421
+ mediaType: "image/png",
422
+ bytes: ref.bytes,
423
+ width: ref.width,
424
+ height: ref.height,
425
+ ...ref.name === void 0 ? {} : { name: ref.name }
426
+ },
427
+ sha256: shot.sha256,
428
+ identicalToPrevious: shot.identicalToPrevious
429
+ };
430
+ });
431
+ }
432
+ }));
433
+ }
434
+ //#endregion
435
+ //#region src/index.ts
436
+ const name = "browser-verify";
437
+ const inject = ["tools"];
438
+ const prefix = "dsh-browser-verify-";
439
+ /** One-shot startup sweep: old temp dirs + stray Chromium, limited to our prefix. */
440
+ async function sweepOrphans() {
441
+ const root = tmpdir();
442
+ const entries = [];
443
+ const dirents = await readdir(root, { withFileTypes: true }).catch(() => []);
444
+ for (const dirent of dirents) {
445
+ if (dirent.name === `${prefix}${process.pid}`) continue;
446
+ if (!dirent.name.startsWith(prefix) || !dirent.isDirectory()) continue;
447
+ const full = join(root, dirent.name);
448
+ const info = await stat(full).catch(() => null);
449
+ if (info !== null) entries.push({
450
+ path: full,
451
+ mtimeMs: info.mtimeMs
452
+ });
453
+ }
454
+ for (const orphan of selectOrphanDirs(entries, Date.now(), 36e5, prefix)) await rm(orphan.path, {
455
+ recursive: true,
456
+ force: true
457
+ }).catch(() => void 0);
458
+ exec("ps -Ao pid=,ppid=,command=", (error, stdout) => {
459
+ if (error !== null) return;
460
+ for (const pid of parseZombiePids(String(stdout), prefix, process.pid)) try {
461
+ process.kill(pid, "SIGKILL");
462
+ } catch {}
463
+ });
464
+ }
465
+ function apply(ctx) {
466
+ sweepOrphans().catch(() => {});
467
+ registerBrowserTools(ctx);
468
+ }
469
+ //#endregion
470
+ export { apply, inject, name };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Screenshot persistence + model projection, mirroring the read_image output
3
+ * direction: save into the durable attachment store, render a text envelope
4
+ * beside the image block the harness projects into the next model request.
5
+ * @module dsh-browser-verify/attachments
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
9
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
10
+ export interface ScreenshotImage {
11
+ attachmentId: string;
12
+ mediaType: ImageMediaType;
13
+ bytes: number;
14
+ width: number;
15
+ height: number;
16
+ name?: string;
17
+ }
18
+ export interface ScreenshotValue {
19
+ image: ScreenshotImage;
20
+ sha256: string;
21
+ identicalToPrevious: boolean;
22
+ }
23
+ export declare function imageRefFromValue(image: ScreenshotImage): ImageAttachmentRef;
24
+ export declare function renderScreenshotBlocks(value: ScreenshotValue): ContentBlock[];
25
+ /** Persist screenshot bytes, mapping store refusals to actionable errors. */
26
+ export declare function saveScreenshot(ctx: Context, data: Buffer, name: string | undefined): Promise<ImageAttachmentRef>;
27
+ /** Gate: the calling route must be able to see image input (mirror of read-image). */
28
+ export declare function assertImageCapable(ctx: Context, exec: {
29
+ agent?: {
30
+ session?: {
31
+ requestHeader?: () => {
32
+ config?: {
33
+ provider?: string;
34
+ model?: string;
35
+ };
36
+ } | undefined;
37
+ };
38
+ options?: {
39
+ provider?: string;
40
+ model?: string;
41
+ };
42
+ };
43
+ }): Promise<void>;
44
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Locate a Browser-for-Testing binary in the machine playwright cache. Pure:
3
+ * filesystem probing is injected so every branch is unit-testable.
4
+ * @module dsh-browser-verify/browser/discover
5
+ */
6
+ export type BrowserKind = 'headless-shell' | 'chromium' | 'custom';
7
+ /** Revision numbers verified against the matching playwright-core browsers.json. */
8
+ export declare const KNOWN_REVISIONS: Readonly<Record<number, string>>;
9
+ export interface DiscoveredBrowser {
10
+ executablePath: string;
11
+ kind: BrowserKind;
12
+ revision: number;
13
+ known: boolean;
14
+ versionHint: string | null;
15
+ }
16
+ export interface DiscoverOptions {
17
+ cacheDir?: string;
18
+ overridePath?: string;
19
+ exists?: (path: string) => boolean;
20
+ /** Directory listing of cacheDir; defaults to readdirSync(cacheDir) (throws → treated as missing). */
21
+ entries?: string[];
22
+ }
23
+ /** Default cache location on macOS. */
24
+ export declare function defaultCacheDir(): string;
25
+ /**
26
+ * Find the browser binary: env override wins, then headless shell (highest
27
+ * revision), then full chromium. Throws with an install hint when absent.
28
+ */
29
+ export declare function discoverBrowser(opts?: DiscoverOptions): DiscoveredBrowser;
30
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Browser driving: one lazy launch per process, one active verification
3
+ * scenario, FIFO-serialized tool access, idle reclamation, graceful close +
4
+ * temp dir removal on dispose. launch args are pure for unit tests.
5
+ * @module dsh-browser-verify/browser/driver
6
+ */
7
+ import { type Browser } from 'playwright-core';
8
+ import { discoverBrowser } from './discover.ts';
9
+ import { Scenario, type OpenResult } from './scenario.ts';
10
+ export interface OpenScenarioResult extends OpenResult {
11
+ browserKnown: boolean;
12
+ versionHint: string | null;
13
+ }
14
+ /**
15
+ * Headless launch args. Deviation D8-3: playwright-core >= 1.41 rejects
16
+ * `--user-data-dir` inside `args` (misuse error, both launch and
17
+ * launchPersistentContext); the user data dir must be passed as the
18
+ * launchPersistentContext first parameter. Only the headless-mode flag remains.
19
+ */
20
+ export declare function buildLaunchArgs(headlessShell: boolean): string[];
21
+ export declare class BrowserDriver {
22
+ private readonly opts;
23
+ private browser;
24
+ private discovered;
25
+ private scenario;
26
+ private readonly userDataDir;
27
+ private idleTimer;
28
+ private lockChain;
29
+ private disposed;
30
+ constructor(opts?: {
31
+ discover?: typeof discoverBrowser;
32
+ viewport?: {
33
+ width: number;
34
+ height: number;
35
+ };
36
+ deviceScaleFactor?: number;
37
+ timeoutMs?: number;
38
+ idleMs?: number;
39
+ });
40
+ /** FIFO serialization: every tool op runs alone. */
41
+ private chain;
42
+ /** Reject new op entries once disposed; the engine cannot come back. */
43
+ private ensureNotDisposed;
44
+ /** Normalize errors at the driver boundary: prefix + context + advice. */
45
+ private wrapError;
46
+ withScenario<T>(fn: (scenario: Scenario) => Promise<T>): Promise<T>;
47
+ /** Open a fresh verification scenario; per design, each open = new context+page. */
48
+ startScenario(reset: {
49
+ url: string;
50
+ waitSelector?: string;
51
+ timeoutMs?: number;
52
+ viewport?: {
53
+ width: number;
54
+ height: number;
55
+ };
56
+ deviceScaleFactor?: number;
57
+ mocks?: Array<{
58
+ urlPattern: string;
59
+ json: unknown;
60
+ status?: number;
61
+ }>;
62
+ }): Promise<OpenScenarioResult>;
63
+ private openScenario;
64
+ private resetIdleTimer;
65
+ ensureBrowser(): Promise<Browser>;
66
+ private requireScenario;
67
+ /**
68
+ * Reset on dispose: mark disposed first (idempotent, blocks new ops), then
69
+ * run the teardown serialized on the FIFO chain so it waits out any
70
+ * in-flight op and cannot interleave with a launch or scenario op.
71
+ */
72
+ dispose(): Promise<void>;
73
+ /** Best-effort teardown: close scenario + browser, delete the profile dir. */
74
+ private teardown;
75
+ /**
76
+ * Hard-kill fallback: SIGKILL every process whose command line still
77
+ * carries our user-data-dir (playwright appends `--user-data-dir` itself;
78
+ * our predictable temp dir is the reverse-lookup key, per design §6).
79
+ */
80
+ private hardKillChromium;
81
+ }
82
+ //# sourceMappingURL=driver.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * One verification scenario = one page + its request mocks + assertion/shot
3
+ * state. Pure helpers are exported for unit tests; the IO methods use
4
+ * playwright-core. Polluting nothing outside the page's own requests.
5
+ * @module dsh-browser-verify/browser/scenario
6
+ */
7
+ import type { BrowserContext, Page } from 'playwright-core';
8
+ export interface MockRule {
9
+ json: unknown;
10
+ status: number;
11
+ }
12
+ export interface OpenResult {
13
+ title: string;
14
+ url: string;
15
+ status: number | null;
16
+ visible: string[];
17
+ consoleErrors: string[];
18
+ elapsedMs: number;
19
+ }
20
+ export interface AssertResult {
21
+ pass: boolean;
22
+ count: number;
23
+ actualText: string | null;
24
+ elapsedMs: number;
25
+ }
26
+ export declare function assertNoMockConflict(patterns: string[], next: string): void;
27
+ export declare function normalizeCountSpec(count: number | {
28
+ min: number;
29
+ max: number;
30
+ } | undefined): {
31
+ min: number;
32
+ max: number;
33
+ } | null;
34
+ export declare function summarizeVisibleText(texts: string[]): string[];
35
+ export declare function capConsoleErrors(errors: string[]): string[];
36
+ export declare function sha256Hex(data: Buffer): string;
37
+ export declare function textDiff(actual: string | null, expected: string): string;
38
+ /** Visible-text extraction, evaluated in the page: text of visible elements. */
39
+ export declare const VISIBLE_TEXT_SCRIPT = "\nArray.from(document.querySelectorAll('body *')).map(el => {\n const rect = el.getBoundingClientRect()\n if (rect.width === 0 || rect.height === 0) return ''\n const text = (el.childElementCount === 0 ? el.textContent ?? '' : '').trim()\n return text.length > 0 ? text : ''\n})\n";
40
+ export declare class Scenario {
41
+ readonly page: Page;
42
+ readonly context: BrowserContext;
43
+ readonly mocks: Map<string, MockRule>;
44
+ lastScreenshotHash: string | null;
45
+ constructor(page: Page, context: BrowserContext);
46
+ navigate(opts: {
47
+ url: string;
48
+ waitSelector?: string;
49
+ timeoutMs?: number;
50
+ }): Promise<OpenResult>;
51
+ addMock(rule: {
52
+ urlPattern: string;
53
+ json: unknown;
54
+ status?: number;
55
+ reload?: boolean;
56
+ timeoutMs?: number;
57
+ }): Promise<string[]>;
58
+ assert(opts: {
59
+ selector: string;
60
+ count?: number | {
61
+ min: number;
62
+ max: number;
63
+ };
64
+ text?: string;
65
+ timeoutMs: number;
66
+ }): Promise<AssertResult>;
67
+ screenshot(opts: {
68
+ fullPage?: boolean;
69
+ }): Promise<{
70
+ data: Buffer;
71
+ sha256: string;
72
+ identicalToPrevious: boolean;
73
+ }>;
74
+ close(): Promise<void>;
75
+ }
76
+ //# sourceMappingURL=scenario.d.ts.map