dsh-browser-playwright-codex 0.5.1
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/CHANGELOG.md +486 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +114 -0
- package/cordis.patch.yml +66 -0
- package/lib/cdp-policy.d.ts +69 -0
- package/lib/cdp-policy.js +105 -0
- package/lib/compat.d.ts +37 -0
- package/lib/compat.js +31 -0
- package/lib/contract.d.ts +74 -0
- package/lib/contract.js +117 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.js +21 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +12 -0
- package/lib/injected.d.ts +35 -0
- package/lib/injected.js +350 -0
- package/lib/playwright.d.ts +647 -0
- package/lib/playwright.js +2515 -0
- package/lib/runtime-state.d.ts +45 -0
- package/lib/runtime-state.js +181 -0
- package/lib/service.d.ts +56 -0
- package/lib/service.js +96 -0
- package/lib/snapshot-aria.d.ts +121 -0
- package/lib/snapshot-aria.js +485 -0
- package/lib/snapshot-render.d.ts +28 -0
- package/lib/snapshot-render.js +100 -0
- package/lib/tool.d.ts +77 -0
- package/lib/tool.js +907 -0
- package/lib/types.d.ts +242 -0
- package/lib/types.js +5 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +108 -0
- package/scripts/analysis/hl-compare.mjs +149 -0
- package/scripts/analysis/live-handoff-probe.mjs +97 -0
- package/scripts/analysis/needs-human-probe.mjs +83 -0
- package/scripts/analysis/repro-minimize.mjs +112 -0
- package/scripts/autocommit.ps1 +23 -0
- package/scripts/check-doc-links.py +112 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/dsh-browser-verify.mjs +475 -0
- package/scripts/measure-prompt-cost.mjs +57 -0
- package/scripts/prompt-cost.mjs +73 -0
- package/scripts/shipped-gates.mjs +43 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** How a page is read into a snapshot tree. */
|
|
2
|
+
export type SnapshotEngine = 'legacy' | 'aria';
|
|
3
|
+
/** Current enable state (loads from disk on first call). */
|
|
4
|
+
export declare function getEnabled(): boolean;
|
|
5
|
+
/** Set the enable state. Returns true if the value actually changed. */
|
|
6
|
+
export declare function setEnabled(next: boolean): boolean;
|
|
7
|
+
/** Subscribe to enable-state transitions. Returns an unsubscribe function. */
|
|
8
|
+
export declare function subscribe(fn: (enabled: boolean) => void): () => void;
|
|
9
|
+
/**
|
|
10
|
+
* The page-reading mode the user picked, or undefined when they never picked
|
|
11
|
+
* one — the configured default then applies.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSnapshotEngine(): SnapshotEngine | undefined;
|
|
14
|
+
/** Pick the page-reading mode. Returns true if the value actually changed. */
|
|
15
|
+
export declare function setSnapshotEngine(next: SnapshotEngine): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Report the mode this deployment was configured with, so the settings page can
|
|
18
|
+
* display what is actually in effect instead of guessing. Called by the provider
|
|
19
|
+
* when it is constructed; not persisted (it comes from config).
|
|
20
|
+
* @param engine - the configured default mode.
|
|
21
|
+
*/
|
|
22
|
+
export declare function publishConfiguredEngine(engine: SnapshotEngine): void;
|
|
23
|
+
/**
|
|
24
|
+
* What a capture will use right now: the user's pick, else the configured
|
|
25
|
+
* default, else 'legacy'. This is what the settings page shows.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getEffectiveEngine(): SnapshotEngine;
|
|
28
|
+
/** Whether the current mode is the user's own pick rather than the default. */
|
|
29
|
+
export declare function isEngineUserChosen(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The tool family's effective gates, published at registration. The Settings page
|
|
32
|
+
* needs to show what is actually in effect — including whatever a profile patch
|
|
33
|
+
* or a home-level patch decided — instead of guessing at the package default.
|
|
34
|
+
* Not persisted: it is derived from config every time the plugin is applied.
|
|
35
|
+
* @param gates - whether browser_evaluate / browser_cdp are registered.
|
|
36
|
+
*/
|
|
37
|
+
export declare function publishGates(gates: {
|
|
38
|
+
allowEvaluate: boolean;
|
|
39
|
+
allowCdp: boolean;
|
|
40
|
+
}): void;
|
|
41
|
+
/** The published gates, or undefined while the tool family has not been applied. */
|
|
42
|
+
export declare function getGates(): {
|
|
43
|
+
allowEvaluate: boolean;
|
|
44
|
+
allowCdp: boolean;
|
|
45
|
+
} | undefined;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared user settings for dsh-browser-playwright-codex, used by the three host
|
|
3
|
+
* entries (browser / browser-playwright / browser-tool) and by the settings-page
|
|
4
|
+
* switches in dsh-browser-toggle.
|
|
5
|
+
*
|
|
6
|
+
* Two settings, both user-facing:
|
|
7
|
+
* - `enabled` (default true) — are the browser tools available at all.
|
|
8
|
+
* - `engine` (default: unset) — how a page is read into the snapshot tree.
|
|
9
|
+
* 'legacy' = the plugin's own DOM walk (most tested).
|
|
10
|
+
* 'aria' = the browser's official accessibility tree (what a screen
|
|
11
|
+
* reader sees), usually more faithful on complex pages.
|
|
12
|
+
* Unset means "follow the configured default", which is 'legacy'.
|
|
13
|
+
*
|
|
14
|
+
* State persists to ~/.dsh/dsh-browser-playwright.state.json so both settings
|
|
15
|
+
* survive a DSH restart. The FILE name deliberately keeps the pre-rename
|
|
16
|
+
* spelling: it is persisted user state, and renaming it would silently reset the
|
|
17
|
+
* user's switches.
|
|
18
|
+
*
|
|
19
|
+
* The file is re-read on every call (no read-once caching), so a change made on
|
|
20
|
+
* one side (web or desktop) is visible to the other immediately, and flipping a
|
|
21
|
+
* switch needs no restart and no config edit.
|
|
22
|
+
*
|
|
23
|
+
* `DSH_BROWSER_STATE_FILE` redirects the file; tests use it so they never touch
|
|
24
|
+
* real user state.
|
|
25
|
+
* @module dsh-browser-playwright-codex/runtime-state
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
// Shared layer: the web and desktop sides read the same switch file, so one
|
|
31
|
+
// agent can never see two different states depending on which side is open.
|
|
32
|
+
const STATE_DIR = path.join(os.homedir(), '.dsh');
|
|
33
|
+
const DEFAULT_STATE_FILE = path.join(STATE_DIR, 'dsh-browser-playwright.state.json');
|
|
34
|
+
/** The state file in use, resolved per call so tests can redirect it. */
|
|
35
|
+
function stateFile() {
|
|
36
|
+
const override = process.env.DSH_BROWSER_STATE_FILE;
|
|
37
|
+
return override !== undefined && override !== '' ? override : DEFAULT_STATE_FILE;
|
|
38
|
+
}
|
|
39
|
+
let _enabled = true;
|
|
40
|
+
let _engine = undefined;
|
|
41
|
+
/** Default pushed in by the provider that is actually loaded (display only). */
|
|
42
|
+
let _configuredEngine = undefined;
|
|
43
|
+
/** Effective tool gates, pushed in by the tool family when it is applied (display only). */
|
|
44
|
+
let _gates = undefined;
|
|
45
|
+
const _subscribers = new Set();
|
|
46
|
+
function load() {
|
|
47
|
+
// Re-read the file on every call: whichever side flips the switch, the
|
|
48
|
+
// other side follows immediately (no read-once caching).
|
|
49
|
+
const previous = _enabled;
|
|
50
|
+
try {
|
|
51
|
+
const txt = fs.readFileSync(stateFile(), 'utf8');
|
|
52
|
+
const obj = JSON.parse(txt);
|
|
53
|
+
if (obj !== null && typeof obj === 'object') {
|
|
54
|
+
if (typeof obj.enabled === 'boolean')
|
|
55
|
+
_enabled = obj.enabled;
|
|
56
|
+
// The file is authoritative for the page-reading mode: an absent or
|
|
57
|
+
// unknown value means "follow the configured default" (undefined here).
|
|
58
|
+
// Files written before this setting existed therefore stay valid.
|
|
59
|
+
_engine = obj.engine === 'legacy' || obj.engine === 'aria' ? obj.engine : undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Missing file / parse error / unreadable: keep the current values.
|
|
64
|
+
}
|
|
65
|
+
if (_enabled !== previous) {
|
|
66
|
+
for (const fn of [..._subscribers]) {
|
|
67
|
+
try {
|
|
68
|
+
fn(_enabled);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// One subscriber failing must not affect the others.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function persist() {
|
|
77
|
+
try {
|
|
78
|
+
const target = stateFile();
|
|
79
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
80
|
+
// Atomic write: rename(2) is atomic on the same filesystem, so a crash
|
|
81
|
+
// mid-write leaves either the old file or the new file, never half of
|
|
82
|
+
// both.
|
|
83
|
+
const tmp = target + '.tmp';
|
|
84
|
+
// Keep the historical shape until the user actually picks a page-reading
|
|
85
|
+
// mode: an absent `engine` means "use the configured default".
|
|
86
|
+
const payload = _engine === undefined ? { enabled: _enabled } : { enabled: _enabled, engine: _engine };
|
|
87
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
88
|
+
fs.renameSync(tmp, target);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Persist failure must not break the in-memory switches; the next set
|
|
92
|
+
// will retry.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Current enable state (loads from disk on first call). */
|
|
96
|
+
export function getEnabled() {
|
|
97
|
+
load();
|
|
98
|
+
return _enabled;
|
|
99
|
+
}
|
|
100
|
+
/** Set the enable state. Returns true if the value actually changed. */
|
|
101
|
+
export function setEnabled(next) {
|
|
102
|
+
load();
|
|
103
|
+
if (typeof next !== 'boolean')
|
|
104
|
+
return false;
|
|
105
|
+
if (_enabled === next)
|
|
106
|
+
return false;
|
|
107
|
+
_enabled = next;
|
|
108
|
+
persist();
|
|
109
|
+
for (const fn of [..._subscribers]) {
|
|
110
|
+
try {
|
|
111
|
+
fn(next);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// One subscriber failing must not prevent others from reacting.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/** Subscribe to enable-state transitions. Returns an unsubscribe function. */
|
|
120
|
+
export function subscribe(fn) {
|
|
121
|
+
_subscribers.add(fn);
|
|
122
|
+
return () => {
|
|
123
|
+
_subscribers.delete(fn);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The page-reading mode the user picked, or undefined when they never picked
|
|
128
|
+
* one — the configured default then applies.
|
|
129
|
+
*/
|
|
130
|
+
export function getSnapshotEngine() {
|
|
131
|
+
load();
|
|
132
|
+
return _engine;
|
|
133
|
+
}
|
|
134
|
+
/** Pick the page-reading mode. Returns true if the value actually changed. */
|
|
135
|
+
export function setSnapshotEngine(next) {
|
|
136
|
+
load();
|
|
137
|
+
if (next !== 'legacy' && next !== 'aria')
|
|
138
|
+
return false;
|
|
139
|
+
if (_engine === next)
|
|
140
|
+
return false;
|
|
141
|
+
_engine = next;
|
|
142
|
+
persist();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Report the mode this deployment was configured with, so the settings page can
|
|
147
|
+
* display what is actually in effect instead of guessing. Called by the provider
|
|
148
|
+
* when it is constructed; not persisted (it comes from config).
|
|
149
|
+
* @param engine - the configured default mode.
|
|
150
|
+
*/
|
|
151
|
+
export function publishConfiguredEngine(engine) {
|
|
152
|
+
if (engine === 'legacy' || engine === 'aria')
|
|
153
|
+
_configuredEngine = engine;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* What a capture will use right now: the user's pick, else the configured
|
|
157
|
+
* default, else 'legacy'. This is what the settings page shows.
|
|
158
|
+
*/
|
|
159
|
+
export function getEffectiveEngine() {
|
|
160
|
+
load();
|
|
161
|
+
return _engine ?? _configuredEngine ?? 'legacy';
|
|
162
|
+
}
|
|
163
|
+
/** Whether the current mode is the user's own pick rather than the default. */
|
|
164
|
+
export function isEngineUserChosen() {
|
|
165
|
+
load();
|
|
166
|
+
return _engine !== undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The tool family's effective gates, published at registration. The Settings page
|
|
170
|
+
* needs to show what is actually in effect — including whatever a profile patch
|
|
171
|
+
* or a home-level patch decided — instead of guessing at the package default.
|
|
172
|
+
* Not persisted: it is derived from config every time the plugin is applied.
|
|
173
|
+
* @param gates - whether browser_evaluate / browser_cdp are registered.
|
|
174
|
+
*/
|
|
175
|
+
export function publishGates(gates) {
|
|
176
|
+
_gates = { allowEvaluate: gates.allowEvaluate === true, allowCdp: gates.allowCdp === true };
|
|
177
|
+
}
|
|
178
|
+
/** The published gates, or undefined while the tool family has not been applied. */
|
|
179
|
+
export function getGates() {
|
|
180
|
+
return _gates === undefined ? undefined : { ..._gates };
|
|
181
|
+
}
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser capability seam: the provider registry and selection service.
|
|
3
|
+
* Providers implement browser backends; consumers acquire sessions through
|
|
4
|
+
* ctx.browser without importing a concrete implementation.
|
|
5
|
+
* @module dsh-browser-playwright-codex/service
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import type { BrowserProvider, BrowserSession } from './types.ts';
|
|
10
|
+
declare module '@deepseek-ai/cordis' {
|
|
11
|
+
interface Context {
|
|
12
|
+
browser: BrowserRuntime;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Selection config for the browser seam. */
|
|
16
|
+
export interface BrowserRuntimeConfig {
|
|
17
|
+
/** Explicit provider id. Omitted = auto-select when exactly one is registered. */
|
|
18
|
+
readonly provider?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Schemastery validation for {@link BrowserRuntimeConfig}. */
|
|
21
|
+
export declare const Config: z<BrowserRuntimeConfig>;
|
|
22
|
+
/**
|
|
23
|
+
* The browser service, registered as ctx.browser (one instance per context).
|
|
24
|
+
* Selection semantics (resolved at acquire time, never order-dependent):
|
|
25
|
+
* - A configured id that is registered → that provider.
|
|
26
|
+
* - A configured id not registered → CONFIGURED_PROVIDER_MISSING.
|
|
27
|
+
* - No id configured, exactly one registered provider → that provider.
|
|
28
|
+
* - No id configured, multiple registered providers → AMBIGUOUS_PROVIDER.
|
|
29
|
+
* - No registered provider → NO_PROVIDER.
|
|
30
|
+
*/
|
|
31
|
+
export default class BrowserRuntime extends Service {
|
|
32
|
+
static Config: z<BrowserRuntimeConfig>;
|
|
33
|
+
private readonly providers;
|
|
34
|
+
private readonly configuredId;
|
|
35
|
+
constructor(ctx: Context, config?: BrowserRuntimeConfig);
|
|
36
|
+
/**
|
|
37
|
+
* Register a browser provider. Duplicate ids fail the registering plugin.
|
|
38
|
+
* @param provider - the provider; its id is the registry key.
|
|
39
|
+
* @returns the disposer that unregisters and disposes the provider.
|
|
40
|
+
*/
|
|
41
|
+
registerProvider(provider: BrowserProvider): () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Acquire the session owned by the owner key through the selected provider.
|
|
44
|
+
* @param owner - opaque caller-owned session key (a harness session id).
|
|
45
|
+
* @param signal - optional cancellation forwarded to the provider.
|
|
46
|
+
* @returns the live browser session.
|
|
47
|
+
*/
|
|
48
|
+
acquire(owner: string, signal?: AbortSignal): Promise<BrowserSession>;
|
|
49
|
+
/**
|
|
50
|
+
* Release the session owned by the owner key on every registered provider;
|
|
51
|
+
* providers ignore unknown owners.
|
|
52
|
+
* @param owner - the owner key whose session should be released.
|
|
53
|
+
*/
|
|
54
|
+
disposeOwner(owner: string): Promise<void>;
|
|
55
|
+
private resolveProvider;
|
|
56
|
+
}
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser capability seam: the provider registry and selection service.
|
|
3
|
+
* Providers implement browser backends; consumers acquire sessions through
|
|
4
|
+
* ctx.browser without importing a concrete implementation.
|
|
5
|
+
* @module dsh-browser-playwright-codex/service
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import { BrowserError } from "./errors.js";
|
|
10
|
+
/** Schemastery validation for {@link BrowserRuntimeConfig}. */
|
|
11
|
+
export const Config = z.object({
|
|
12
|
+
provider: z.string(),
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* The browser service, registered as ctx.browser (one instance per context).
|
|
16
|
+
* Selection semantics (resolved at acquire time, never order-dependent):
|
|
17
|
+
* - A configured id that is registered → that provider.
|
|
18
|
+
* - A configured id not registered → CONFIGURED_PROVIDER_MISSING.
|
|
19
|
+
* - No id configured, exactly one registered provider → that provider.
|
|
20
|
+
* - No id configured, multiple registered providers → AMBIGUOUS_PROVIDER.
|
|
21
|
+
* - No registered provider → NO_PROVIDER.
|
|
22
|
+
*/
|
|
23
|
+
export default class BrowserRuntime extends Service {
|
|
24
|
+
static Config = Config;
|
|
25
|
+
providers = new Map();
|
|
26
|
+
configuredId;
|
|
27
|
+
constructor(ctx, config = {}) {
|
|
28
|
+
super(ctx, 'browser');
|
|
29
|
+
this.configuredId = config.provider;
|
|
30
|
+
// Unloading this plugin disposes every registered provider's resources.
|
|
31
|
+
this.ctx.effect(() => () => {
|
|
32
|
+
for (const provider of this.providers.values())
|
|
33
|
+
void provider.dispose();
|
|
34
|
+
this.providers.clear();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Register a browser provider. Duplicate ids fail the registering plugin.
|
|
39
|
+
* @param provider - the provider; its id is the registry key.
|
|
40
|
+
* @returns the disposer that unregisters and disposes the provider.
|
|
41
|
+
*/
|
|
42
|
+
registerProvider(provider) {
|
|
43
|
+
if (this.providers.has(provider.id)) {
|
|
44
|
+
throw new BrowserError('AMBIGUOUS_PROVIDER', 'a browser provider with id "' + provider.id + '" is already registered');
|
|
45
|
+
}
|
|
46
|
+
const runtime = this;
|
|
47
|
+
const dispose = this.ctx.effect(function* () {
|
|
48
|
+
runtime.providers.set(provider.id, provider);
|
|
49
|
+
yield () => {
|
|
50
|
+
runtime.providers.delete(provider.id);
|
|
51
|
+
void provider.dispose();
|
|
52
|
+
};
|
|
53
|
+
}, 'browser.registerProvider()');
|
|
54
|
+
return () => void dispose();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Acquire the session owned by the owner key through the selected provider.
|
|
58
|
+
* @param owner - opaque caller-owned session key (a harness session id).
|
|
59
|
+
* @param signal - optional cancellation forwarded to the provider.
|
|
60
|
+
* @returns the live browser session.
|
|
61
|
+
*/
|
|
62
|
+
async acquire(owner, signal) {
|
|
63
|
+
return this.resolveProvider().acquire(owner, signal);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Release the session owned by the owner key on every registered provider;
|
|
67
|
+
* providers ignore unknown owners.
|
|
68
|
+
* @param owner - the owner key whose session should be released.
|
|
69
|
+
*/
|
|
70
|
+
async disposeOwner(owner) {
|
|
71
|
+
for (const provider of this.providers.values()) {
|
|
72
|
+
await provider.disposeOwner(owner);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
resolveProvider() {
|
|
76
|
+
const { configuredId, providers } = this;
|
|
77
|
+
if (configuredId !== undefined) {
|
|
78
|
+
const provider = providers.get(configuredId);
|
|
79
|
+
if (provider === undefined) {
|
|
80
|
+
throw new BrowserError('CONFIGURED_PROVIDER_MISSING', 'configured browser provider "' + configuredId + '" is not registered');
|
|
81
|
+
}
|
|
82
|
+
return provider;
|
|
83
|
+
}
|
|
84
|
+
const registered = [...providers.values()];
|
|
85
|
+
const [single] = registered;
|
|
86
|
+
if (single === undefined) {
|
|
87
|
+
throw new BrowserError('NO_PROVIDER', 'no browser provider is registered; mount the playwright entry of this package');
|
|
88
|
+
}
|
|
89
|
+
if (registered.length > 1) {
|
|
90
|
+
throw new BrowserError('AMBIGUOUS_PROVIDER', 'multiple browser providers are registered (' +
|
|
91
|
+
registered.map((p) => p.id).join(', ') +
|
|
92
|
+
'); configure one explicitly');
|
|
93
|
+
}
|
|
94
|
+
return single;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aria semantic snapshot engine: uses Playwright's official
|
|
3
|
+
* locator.ariaSnapshot({ mode: 'ai' }) as the semantic tree source, then
|
|
4
|
+
* re-aligns refs onto the project's stable data-dsh-ref scheme.
|
|
5
|
+
*
|
|
6
|
+
* Why a ref-alignment layer: ariaSnapshot's `[ref=eN]` are Playwright-internal
|
|
7
|
+
* per-invocation handles — they are NOT locator-addressable and their numbering
|
|
8
|
+
* does not survive the project's cross-snapshot stability contract. This module
|
|
9
|
+
* therefore (1) walks the DOM to mint/keep the same e<docNonce><seq> refs the
|
|
10
|
+
* legacy engine uses, (2) parses the official YAML into a semantic tree, and
|
|
11
|
+
* (3) assigns the DOM-minted refs to actionable nodes in document order, so
|
|
12
|
+
* refLocator('[data-dsh-ref=...]') and REF_PATTERN keep working unchanged.
|
|
13
|
+
*
|
|
14
|
+
* The actionable set is intentionally role-based (the same role set the
|
|
15
|
+
* injected engine uses) so DOM-side ref assignment and tree-side ref alignment
|
|
16
|
+
* agree on ordering; elements that only tabindex/contenteditable would make
|
|
17
|
+
* actionable in the legacy engine are out of scope for this engine.
|
|
18
|
+
* @module dsh-browser-playwright-codex/snapshot-aria
|
|
19
|
+
*/
|
|
20
|
+
import type { Page } from 'playwright-core';
|
|
21
|
+
import type { BrowserNode } from './types.ts';
|
|
22
|
+
/**
|
|
23
|
+
* Page-context script: walks the DOM in document order and mints/keeps
|
|
24
|
+
* data-dsh-ref on every actionable element (same rule as the legacy engine,
|
|
25
|
+
* minus the tabindex/contenteditable escape hatches). Returns the refs in
|
|
26
|
+
* document order plus the per-document nonce, so the alignment layer can pair
|
|
27
|
+
* them with the aria tree's actionable nodes depth-first.
|
|
28
|
+
*/
|
|
29
|
+
export declare const ARIA_REF_SCRIPT: string;
|
|
30
|
+
/** One semantic node parsed from the official ariaSnapshot YAML. */
|
|
31
|
+
export interface AriaNode {
|
|
32
|
+
readonly role: string;
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/** Playwright-internal ref from the YAML ([ref=eN]); used for alignment only. */
|
|
35
|
+
readonly ariaRef?: string;
|
|
36
|
+
readonly level?: number;
|
|
37
|
+
readonly checked?: boolean;
|
|
38
|
+
readonly selected?: boolean;
|
|
39
|
+
readonly disabled?: boolean;
|
|
40
|
+
readonly href?: string;
|
|
41
|
+
/** `/placeholder:` attribute line: names an input the same way the legacy walker does. */
|
|
42
|
+
readonly placeholder?: string;
|
|
43
|
+
readonly children: readonly AriaNode[];
|
|
44
|
+
}
|
|
45
|
+
/** Parsed tree plus the lines the parser could not read (format drift). */
|
|
46
|
+
export interface AriaParseResult {
|
|
47
|
+
readonly roots: AriaNode[];
|
|
48
|
+
/** Content lines that could not be parsed — non-empty means format drift. */
|
|
49
|
+
readonly unparsed: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse Playwright's official ariaSnapshot YAML into a semantic node tree.
|
|
53
|
+
*
|
|
54
|
+
* The real format (verified against playwright-core 1.62, 2026-09-30) is:
|
|
55
|
+
*
|
|
56
|
+
* - generic [active] [ref=e1]: ← trailing ':' = the node has children
|
|
57
|
+
* - link "One" [ref=e6] [cursor=pointer]:
|
|
58
|
+
* - /url: /one ← href rides a deeper child line
|
|
59
|
+
* - text: Name ← text content uses 'text: <value>'
|
|
60
|
+
*
|
|
61
|
+
* The first version of this parser assumed a hand-rolled dialect
|
|
62
|
+
* (`- role "name" [flags] -> href`, nothing after the flags) that real output
|
|
63
|
+
* never produces. Every container line ends in ':', so that regex matched none
|
|
64
|
+
* of them: the whole hierarchy was dropped, links and their hrefs vanished, and
|
|
65
|
+
* `captureAriaSnapshot` still reported success. Unreadable lines are now
|
|
66
|
+
* collected in `unparsed` so the capture layer can refuse the tree and let the
|
|
67
|
+
* provider fall back to the legacy engine.
|
|
68
|
+
* @param yaml - the ariaSnapshot text.
|
|
69
|
+
* @returns parsed roots plus any unreadable content lines.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseAriaSnapshotWithStats(yaml: string): AriaParseResult;
|
|
72
|
+
/**
|
|
73
|
+
* Parse Playwright's ariaSnapshot YAML into a semantic node tree.
|
|
74
|
+
* @param yaml - the ariaSnapshot text.
|
|
75
|
+
* @returns the parsed root nodes (see {@link parseAriaSnapshotWithStats} for
|
|
76
|
+
* the unreadable-line report this is a thin wrapper over).
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseAriaSnapshot(yaml: string): AriaNode[];
|
|
79
|
+
/** Whether an aria role carries a ref (must match the DOM-side assignment). */
|
|
80
|
+
export declare function isActionableRole(role: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Build BrowserNode[] from the parsed aria tree, aligning DOM-minted refs onto
|
|
83
|
+
* actionable nodes in depth-first order, with node/name caps and truncation.
|
|
84
|
+
* @param nodes - parsed aria tree roots.
|
|
85
|
+
* @param opts - bounding options.
|
|
86
|
+
* @param refs - data-dsh-refs minted by the page-side walk, in document order.
|
|
87
|
+
* @returns the converted tree plus the truncation flag.
|
|
88
|
+
*/
|
|
89
|
+
export declare function ariaTreeToBrowserNodes(nodes: readonly AriaNode[], opts: {
|
|
90
|
+
maxNodes: number;
|
|
91
|
+
maxNameLength: number;
|
|
92
|
+
maxTextLength: number;
|
|
93
|
+
interactiveOnly?: boolean;
|
|
94
|
+
}, refs: readonly string[], roles?: readonly string[]): {
|
|
95
|
+
nodes: BrowserNode[];
|
|
96
|
+
truncated: boolean;
|
|
97
|
+
totalRefs: number;
|
|
98
|
+
misaligned: boolean;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Capture one aria snapshot from a live page: official ariaSnapshot(mode:'ai')
|
|
102
|
+
* for the semantic tree, DOM-side ref minting for stable refs, then alignment.
|
|
103
|
+
* Returns the same shape the legacy capture produces, so playwright.ts can
|
|
104
|
+
* dispatch transparently.
|
|
105
|
+
*
|
|
106
|
+
* `missing: true` is the honest-failure contract: the official call threw, the
|
|
107
|
+
* YAML was empty, or the text carried lines this parser cannot read. The caller
|
|
108
|
+
* must treat it as "engine unavailable" (the provider falls back to the legacy
|
|
109
|
+
* DOM walker) and must never forward the empty node list to the model.
|
|
110
|
+
*/
|
|
111
|
+
export declare function captureAriaSnapshot(page: Page, opts: {
|
|
112
|
+
interactiveOnly?: boolean;
|
|
113
|
+
maxNodes: number;
|
|
114
|
+
maxNameLength: number;
|
|
115
|
+
maxTextLength: number;
|
|
116
|
+
} | undefined): Promise<{
|
|
117
|
+
nodes: BrowserNode[];
|
|
118
|
+
truncated: boolean;
|
|
119
|
+
totalRefs: number;
|
|
120
|
+
missing?: boolean;
|
|
121
|
+
}>;
|