@frontera-sdk/cli 1.52.39 → 1.52.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +48 -4
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +331 -31
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +875 -88
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +320 -13
- package/src/commands/local/sync.ts +102 -5
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +52 -8
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +112 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +211 -21
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +180 -7
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +346 -35
- package/src/local/local-dev-sync.ts +136 -7
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +205 -7
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +141 -16
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +38 -8
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +1 -1
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +11 -0
- package/src/local/local-sync-push.ts +10 -7
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +343 -32
- package/src/local/local-sync-snapshot.ts +15 -10
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +24 -21
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { AxNode, FrameContent } from './ax-snapshot'
|
|
2
|
+
import { selectorVisible, textPresent } from './page-scripts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The frames of one page, for the coding agent's browser: an App embedded in
|
|
6
|
+
* Studio is an <iframe> on another origin (`*.apps.localhost`), and Chrome
|
|
7
|
+
* renders a frame from another site in a process of its own, reached through
|
|
8
|
+
* a DevTools session of its own. A snapshot reads every frame's accessibility
|
|
9
|
+
* tree into the page's, and a ref names an element in any of them, so the
|
|
10
|
+
* agent clicks, types and waits inside an embedded App as it does on the page.
|
|
11
|
+
*
|
|
12
|
+
* Shared by both browsers the coding agent drives: Forge's (Electron, whose
|
|
13
|
+
* debugger attaches to out-of-process frames itself) and the headless one
|
|
14
|
+
* `frontera mcp` runs without Forge (Playwright, a session per such frame).
|
|
15
|
+
* Each says how it reaches a frame's session (`FrameSessions`); the rest is here.
|
|
16
|
+
*
|
|
17
|
+
* Refs stay what they were on the page itself: an element of the main
|
|
18
|
+
* document is named by its DOM node (`backendNodeId`). An element in a frame
|
|
19
|
+
* is named by a handle (a negative number) that maps to its frame and node,
|
|
20
|
+
* since node ids are only unique within one renderer process.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type Cdp = <T = Record<string, unknown>>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
24
|
+
|
|
25
|
+
export interface FrameSessions {
|
|
26
|
+
/** The page's own session. */
|
|
27
|
+
readonly root: Cdp
|
|
28
|
+
/** The session of a frame Chrome renders in a process of its own (its target id is its frame id), or null when it renders in its parent's. */
|
|
29
|
+
outOfProcess(frameId: string): Promise<Cdp | null>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PageFrame {
|
|
33
|
+
/** The DevTools frame id; null for the page's main frame until it is read. */
|
|
34
|
+
id: string | null
|
|
35
|
+
/** The session the frame's DOM is reached through. */
|
|
36
|
+
cdp: Cdp
|
|
37
|
+
/** The frame is its session's top (the main frame, or one in a process of its own): its coordinates start at its own corner. */
|
|
38
|
+
sessionTop: boolean
|
|
39
|
+
parent: PageFrame | null
|
|
40
|
+
/** The <iframe> element in the parent's document. */
|
|
41
|
+
owner: number | null
|
|
42
|
+
/** The frame's address, as its document says (`location.href`). */
|
|
43
|
+
href: string
|
|
44
|
+
world: number | null
|
|
45
|
+
depth: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** How deep frames are read, and how many at most: a page of ads must not flood the snapshot. */
|
|
49
|
+
export const MAX_FRAME_DEPTH = 4
|
|
50
|
+
export const MAX_FRAMES = 24
|
|
51
|
+
|
|
52
|
+
const GROUP = 'frontera-frames'
|
|
53
|
+
|
|
54
|
+
/** An address without its query or fragment, which can carry a sign-in token: what a snapshot shows of a frame. */
|
|
55
|
+
export function shownAddress(href: string): string {
|
|
56
|
+
try {
|
|
57
|
+
const u = new URL(href)
|
|
58
|
+
if (u.protocol === 'about:' || u.protocol === 'data:' || u.protocol === 'blob:') return u.protocol === 'about:' ? href.split('?')[0]! : `${u.protocol}…`
|
|
59
|
+
return `${u.origin}${u.pathname}`
|
|
60
|
+
} catch {
|
|
61
|
+
return href.split(/[?#]/)[0] ?? href
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class PageFrames {
|
|
66
|
+
readonly main: PageFrame
|
|
67
|
+
private byId = new Map<string, PageFrame>()
|
|
68
|
+
private handles = new Map<number, { frame: PageFrame; node: number }>()
|
|
69
|
+
private handleKeys = new Map<string, number>()
|
|
70
|
+
private nextHandle = 0
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly sessions: FrameSessions,
|
|
74
|
+
private readonly worldName: string,
|
|
75
|
+
) {
|
|
76
|
+
this.main = { id: null, cdp: sessions.root, sessionTop: true, parent: null, owner: null, href: '', world: null, depth: 0 }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The page went to another document: its frames, their nodes and worlds are gone. */
|
|
80
|
+
reset(): void {
|
|
81
|
+
this.byId.clear()
|
|
82
|
+
this.handles.clear()
|
|
83
|
+
this.handleKeys.clear()
|
|
84
|
+
this.main.world = null
|
|
85
|
+
this.main.id = null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A frame went to another document: its world is gone (its nodes go with it, and their refs say so when used). */
|
|
89
|
+
frameNavigated(frameId: string): void {
|
|
90
|
+
const f = this.byId.get(frameId)
|
|
91
|
+
if (f) f.world = null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The number that names an element: its DOM node on the main document, a handle in a frame. */
|
|
95
|
+
handleFor(frame: PageFrame, node: number): number {
|
|
96
|
+
if (frame === this.main) return node
|
|
97
|
+
const key = `${frame.id}:${node}`
|
|
98
|
+
const known = this.handleKeys.get(key)
|
|
99
|
+
if (known !== undefined) return known
|
|
100
|
+
const handle = --this.nextHandle
|
|
101
|
+
this.handleKeys.set(key, handle)
|
|
102
|
+
this.handles.set(handle, { frame, node })
|
|
103
|
+
return handle
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The frame and DOM node a handle names. */
|
|
107
|
+
locate(handle: number): { frame: PageFrame; node: number } {
|
|
108
|
+
if (handle >= 0) return { frame: this.main, node: handle }
|
|
109
|
+
const at = this.handles.get(handle)
|
|
110
|
+
if (!at) throw new Error('the element is gone with its frame: take a new browser_snapshot')
|
|
111
|
+
return at
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Whether the element is in a frame rather than on the page itself. */
|
|
115
|
+
inFrame(handle: number): boolean {
|
|
116
|
+
return handle < 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The address of the frame the element is in. */
|
|
120
|
+
addressOf(handle: number): string | null {
|
|
121
|
+
if (handle >= 0) return null
|
|
122
|
+
return this.handles.get(handle)?.frame.href ?? null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The frame an <iframe> element shows, its session found; null when it shows none. */
|
|
126
|
+
async childFrame(parent: PageFrame, owner: number): Promise<PageFrame | null> {
|
|
127
|
+
const { node } = await parent.cdp<{ node: { frameId?: string; contentDocument?: unknown } }>('DOM.describeNode', { backendNodeId: owner })
|
|
128
|
+
const frameId = node.frameId
|
|
129
|
+
if (!frameId) return null
|
|
130
|
+
const own = await this.sessions.outOfProcess(frameId)
|
|
131
|
+
const cdp = own ?? parent.cdp
|
|
132
|
+
const known = this.byId.get(frameId)
|
|
133
|
+
if (known && known.cdp === cdp && known.parent === parent) {
|
|
134
|
+
known.owner = owner
|
|
135
|
+
return known
|
|
136
|
+
}
|
|
137
|
+
const frame: PageFrame = { id: frameId, cdp, sessionTop: !!own, parent, owner, href: '', world: null, depth: parent.depth + 1 }
|
|
138
|
+
this.byId.set(frameId, frame)
|
|
139
|
+
return frame
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async frameIdOf(frame: PageFrame): Promise<string> {
|
|
143
|
+
if (frame.id) return frame.id
|
|
144
|
+
const tree = await frame.cdp<{ frameTree: { frame: { id: string } } }>('Page.getFrameTree')
|
|
145
|
+
frame.id = tree.frameTree.frame.id
|
|
146
|
+
return frame.id
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The frame's isolated world, where the browser's own scripts run unseen by the page's. */
|
|
150
|
+
async world(frame: PageFrame): Promise<number> {
|
|
151
|
+
if (frame.world !== null) return frame.world
|
|
152
|
+
const frameId = await this.frameIdOf(frame)
|
|
153
|
+
const world = await frame.cdp<{ executionContextId: number }>('Page.createIsolatedWorld', { frameId, worldName: this.worldName, grantUniveralAccess: false })
|
|
154
|
+
frame.world = world.executionContextId
|
|
155
|
+
return frame.world
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The browser's own expression in a frame's isolated world; null when the frame cannot answer. */
|
|
159
|
+
async evaluate<T>(frame: PageFrame, expression: string): Promise<T | null> {
|
|
160
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
161
|
+
try {
|
|
162
|
+
const res = await frame.cdp<{ result: { value?: T } }>('Runtime.evaluate', { expression, contextId: await this.world(frame), returnByValue: true, awaitPromise: true })
|
|
163
|
+
return res.result.value ?? null
|
|
164
|
+
} catch {
|
|
165
|
+
// A frame that navigated took its world with it: make another, once.
|
|
166
|
+
frame.world = null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Call the browser's function on an element, in its frame's isolated world (or the page's own world). */
|
|
173
|
+
async callOn<T>(handle: number, fn: string, args: unknown[] = [], opts: { mainWorld?: boolean } = {}): Promise<T | null> {
|
|
174
|
+
const { frame, node } = this.locate(handle)
|
|
175
|
+
const resolve = async () => frame.cdp<{ object: { objectId?: string } }>('DOM.resolveNode', opts.mainWorld ? { backendNodeId: node } : { backendNodeId: node, executionContextId: await this.world(frame) })
|
|
176
|
+
let resolved: { object: { objectId?: string } }
|
|
177
|
+
try {
|
|
178
|
+
resolved = await resolve()
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (opts.mainWorld) throw err
|
|
181
|
+
frame.world = null
|
|
182
|
+
resolved = await resolve()
|
|
183
|
+
}
|
|
184
|
+
if (!resolved.object.objectId) return null
|
|
185
|
+
const res = await frame.cdp<{ result: { value?: T } }>('Runtime.callFunctionOn', { objectId: resolved.object.objectId, functionDeclaration: fn, arguments: args.map((value) => ({ value })), returnByValue: true })
|
|
186
|
+
return res.result.value ?? null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Where a frame's own coordinates start, on the page. */
|
|
190
|
+
private async offset(frame: PageFrame): Promise<{ x: number; y: number }> {
|
|
191
|
+
if (!frame.parent) return { x: 0, y: 0 }
|
|
192
|
+
const above = await this.offset(frame.parent)
|
|
193
|
+
// A frame in its parent's process reports page coordinates already.
|
|
194
|
+
if (!frame.sessionTop) return above
|
|
195
|
+
const { model } = await frame.parent.cdp<{ model: { content: number[] } }>('DOM.getBoxModel', { backendNodeId: frame.owner })
|
|
196
|
+
return { x: above.x + model.content[0]!, y: above.y + model.content[1]! }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The middle of an element on the page, scrolled into view (and its frames with it). */
|
|
200
|
+
async center(handle: number): Promise<{ x: number; y: number }> {
|
|
201
|
+
const { frame, node } = this.locate(handle)
|
|
202
|
+
const chain: PageFrame[] = []
|
|
203
|
+
for (let f: PageFrame | null = frame; f?.parent; f = f.parent) chain.unshift(f)
|
|
204
|
+
for (const f of chain) await f.parent!.cdp('DOM.scrollIntoViewIfNeeded', { backendNodeId: f.owner }).catch(() => {})
|
|
205
|
+
await frame.cdp('DOM.scrollIntoViewIfNeeded', { backendNodeId: node }).catch(() => {})
|
|
206
|
+
const { quads } = await frame.cdp<{ quads: number[][] }>('DOM.getContentQuads', { backendNodeId: node })
|
|
207
|
+
const q = quads[0]
|
|
208
|
+
if (!q) throw new Error('the element is not on screen (hidden, or zero size)')
|
|
209
|
+
const at = await this.offset(frame)
|
|
210
|
+
return { x: at.x + (q[0]! + q[2]! + q[4]! + q[6]!) / 4, y: at.y + (q[1]! + q[3]! + q[5]! + q[7]!) / 4 }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Focus an element; in a frame through DevTools, which moves the page's focus into that frame (a script's focus() cannot). */
|
|
214
|
+
async focus(handle: number, script: string): Promise<void> {
|
|
215
|
+
const { frame, node } = this.locate(handle)
|
|
216
|
+
if (frame !== this.main) await frame.cdp('DOM.focus', { backendNodeId: node }).catch(() => {})
|
|
217
|
+
await this.callOn(handle, script)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async readHref(frame: PageFrame): Promise<void> {
|
|
221
|
+
const href = await this.evaluate<string>(frame, 'location.href')
|
|
222
|
+
if (typeof href === 'string') frame.href = href
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The page's accessibility tree with every frame's read into it, for the snapshot. */
|
|
226
|
+
async snapshotTree(): Promise<{ nodes: AxNode[]; frames: Map<string, FrameContent | { error: string }> }> {
|
|
227
|
+
const budget = { frames: MAX_FRAMES }
|
|
228
|
+
const { nodes } = await this.main.cdp<{ nodes: AxNode[] }>('Accessibility.getFullAXTree')
|
|
229
|
+
return { nodes, frames: await this.childTrees(this.main, nodes, budget) }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async childTrees(parent: PageFrame, nodes: AxNode[], budget: { frames: number }): Promise<Map<string, FrameContent | { error: string }>> {
|
|
233
|
+
const out = new Map<string, FrameContent | { error: string }>()
|
|
234
|
+
if (parent.depth >= MAX_FRAME_DEPTH) return out
|
|
235
|
+
for (const n of nodes) {
|
|
236
|
+
if (n.ignored || n.backendDOMNodeId === undefined || String(n.role?.value ?? '') !== 'Iframe') continue
|
|
237
|
+
if (budget.frames <= 0) {
|
|
238
|
+
out.set(n.nodeId, { error: `the snapshot reads at most ${MAX_FRAMES} frames` })
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
budget.frames--
|
|
242
|
+
try {
|
|
243
|
+
const frame = await this.childFrame(parent, n.backendDOMNodeId)
|
|
244
|
+
if (!frame) continue
|
|
245
|
+
await this.readHref(frame)
|
|
246
|
+
const tree = await frame.cdp<{ nodes: AxNode[] }>('Accessibility.getFullAXTree', frame.sessionTop ? {} : { frameId: frame.id })
|
|
247
|
+
out.set(n.nodeId, {
|
|
248
|
+
nodes: tree.nodes,
|
|
249
|
+
url: shownAddress(frame.href),
|
|
250
|
+
handleOf: (node) => this.handleFor(frame, node),
|
|
251
|
+
frames: await this.childTrees(frame, tree.nodes, budget),
|
|
252
|
+
})
|
|
253
|
+
} catch (err) {
|
|
254
|
+
out.set(n.nodeId, { error: ((err as Error).message || 'it did not answer').slice(0, 200) })
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return out
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The <iframe> and <frame> elements in a frame's document. */
|
|
261
|
+
private async ownersIn(frame: PageFrame): Promise<number[]> {
|
|
262
|
+
const contextId = await this.world(frame)
|
|
263
|
+
const list = await frame.cdp<{ result: { objectId?: string } }>('Runtime.evaluate', { expression: "[...document.querySelectorAll('iframe,frame')]", contextId, objectGroup: GROUP, returnByValue: false })
|
|
264
|
+
if (!list.result.objectId) return []
|
|
265
|
+
try {
|
|
266
|
+
const props = await frame.cdp<{ result: Array<{ name: string; value?: { objectId?: string } }> }>('Runtime.getProperties', { objectId: list.result.objectId, ownProperties: true })
|
|
267
|
+
const owners: number[] = []
|
|
268
|
+
for (const p of props.result) {
|
|
269
|
+
if (!/^\d+$/.test(p.name) || !p.value?.objectId) continue
|
|
270
|
+
const { node } = await frame.cdp<{ node: { backendNodeId: number } }>('DOM.describeNode', { objectId: p.value.objectId })
|
|
271
|
+
owners.push(node.backendNodeId)
|
|
272
|
+
}
|
|
273
|
+
return owners
|
|
274
|
+
} finally {
|
|
275
|
+
await frame.cdp('Runtime.releaseObjectGroup', { objectGroup: GROUP }).catch(() => {})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Every frame of the page now, the main one first, bounded. */
|
|
280
|
+
async all(): Promise<PageFrame[]> {
|
|
281
|
+
const out: PageFrame[] = [this.main]
|
|
282
|
+
for (let i = 0; i < out.length && out.length < MAX_FRAMES; i++) {
|
|
283
|
+
const f = out[i]!
|
|
284
|
+
if (f.depth >= MAX_FRAME_DEPTH) continue
|
|
285
|
+
const owners = await this.ownersIn(f).catch(() => [] as number[])
|
|
286
|
+
for (const owner of owners) {
|
|
287
|
+
const child = await this.childFrame(f, owner).catch(() => null)
|
|
288
|
+
if (child && !out.includes(child)) out.push(child)
|
|
289
|
+
if (out.length >= MAX_FRAMES) break
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** How often a wait looks for frames again (one that opened after the wait began). */
|
|
297
|
+
export const FRAMES_AGAIN_MS = 2_000
|
|
298
|
+
|
|
299
|
+
/** What the agent can wait for, as both browsers read it. */
|
|
300
|
+
export interface FrameWaitCondition {
|
|
301
|
+
text?: string
|
|
302
|
+
textGone?: string
|
|
303
|
+
selector?: string
|
|
304
|
+
state?: 'visible' | 'hidden'
|
|
305
|
+
ref?: number
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Whether a wait's condition holds now, in any of the page's frames; a
|
|
310
|
+
* string when it can never hold (a selector that is not one). Text shows
|
|
311
|
+
* when any frame shows it and is gone when none does; a selector shows when
|
|
312
|
+
* it matches a drawn element in any frame.
|
|
313
|
+
*/
|
|
314
|
+
export async function conditionHolds(frames: PageFrames, list: PageFrame[], c: FrameWaitCondition, isShown: (handle: number) => Promise<boolean>): Promise<boolean | string> {
|
|
315
|
+
const anyFrame = async (expression: string) => {
|
|
316
|
+
for (const f of list) if ((await frames.evaluate<boolean>(f, expression)) === true) return true
|
|
317
|
+
return false
|
|
318
|
+
}
|
|
319
|
+
if (c.text !== undefined && !(await anyFrame(textPresent(c.text)))) return false
|
|
320
|
+
if (c.textGone !== undefined && (await anyFrame(textPresent(c.textGone)))) return false
|
|
321
|
+
const hidden = c.state === 'hidden'
|
|
322
|
+
if (c.selector !== undefined) {
|
|
323
|
+
let shown = false
|
|
324
|
+
for (const f of list) {
|
|
325
|
+
const v = await frames.evaluate<boolean | { error: string }>(f, selectorVisible(c.selector))
|
|
326
|
+
if (v && typeof v === 'object') return `"${c.selector}" is not a CSS selector: ${v.error}`
|
|
327
|
+
if (v === true) {
|
|
328
|
+
shown = true
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (shown === hidden) return false
|
|
333
|
+
}
|
|
334
|
+
if (c.ref !== undefined && (await isShown(c.ref)) === hidden) return false
|
|
335
|
+
return true
|
|
336
|
+
}
|