@geometra/mcp 1.19.18 → 1.19.20
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 +26 -16
- package/dist/__tests__/proxy-session-recovery.test.js +55 -0
- package/dist/__tests__/server-batch-results.test.js +240 -0
- package/dist/server.js +418 -171
- package/dist/session.js +160 -82
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,13 +14,19 @@ Native Geometra: WebSocket → JSON geometry (no browser on the agent path)
|
|
|
14
14
|
Geometra proxy: Chromium → DOM geometry → same WebSocket as native → MCP tools unchanged (often started via `pageUrl`, no manual CLI)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## When to choose this
|
|
18
|
+
|
|
19
|
+
Use Geometra MCP when an LLM needs to explore, interpret, and operate a real UI with compact semantic state instead of repeatedly consuming large browser snapshots. Keep Playwright-style tooling for deterministic scripts, DOM-oriented test automation, and compatibility fallback paths while Geometra closes remaining live-site gaps.
|
|
20
|
+
|
|
21
|
+
Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a small warm pool so compatible headed and headless workflows do not immediately evict each other.
|
|
22
|
+
|
|
17
23
|
## Tools
|
|
18
24
|
|
|
19
25
|
| Tool | Description |
|
|
20
26
|
|---|---|
|
|
21
|
-
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; `
|
|
27
|
+
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; can inline `formSchema` and/or `pageModel` for lower-turn starts |
|
|
22
28
|
| `geometra_query` | Find elements by stable id, role, name, text content, ancestor/prompt context, current value, or semantic state such as `invalid`, `required`, or `busy` |
|
|
23
|
-
| `geometra_wait_for` | Wait for a semantic condition instead of guessing sleeps (`busy`, `disabled`, alerts, values, etc.) |
|
|
29
|
+
| `geometra_wait_for` | Wait for a semantic condition instead of guessing sleeps (`busy`, `disabled`, alerts, values, etc.). **Strict parameters** — use `text` plus `present: false` to wait until a substring disappears (e.g. “Parsing your resume”); there is no `textGone` field |
|
|
24
30
|
| `geometra_form_schema` | Compact, fill-oriented form schema with stable field ids and collapsed radio/button groups; can auto-connect from `pageUrl` / `url` |
|
|
25
31
|
| `geometra_fill_form` | Fill a form from `valuesById` / `valuesByLabel` in one MCP call; can auto-connect from `pageUrl` / `url` for the lowest-token known-form path |
|
|
26
32
|
| `geometra_fill_fields` | Fill labeled text/choice/toggle/file fields in one MCP call; can return final-only status for the smallest responses |
|
|
@@ -28,7 +34,7 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
|
|
|
28
34
|
| `geometra_page_model` | Summary-first webpage model: archetypes, stable section ids, counts, top-level sections, primary actions |
|
|
29
35
|
| `geometra_expand_section` | Expand one form/dialog/list/landmark from `geometra_page_model` on demand, with paging/filtering for long sections |
|
|
30
36
|
| `geometra_reveal` | Scroll until a matching node is visible instead of guessing wheel deltas |
|
|
31
|
-
| `geometra_click` | Click
|
|
37
|
+
| `geometra_click` | Click by coordinates or semantic target, optionally waiting for a post-click semantic condition |
|
|
32
38
|
| `geometra_type` | Type text into the focused element |
|
|
33
39
|
| `geometra_key` | Send special keys (Enter, Tab, Escape, arrows) |
|
|
34
40
|
| `geometra_upload_files` | Attach files: labeled field / auto / hidden input / native chooser / synthetic drop (`@geometra/proxy` only) |
|
|
@@ -40,6 +46,10 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
|
|
|
40
46
|
| `geometra_layout` | Raw computed geometry for every node |
|
|
41
47
|
| `geometra_disconnect` | Close the connection |
|
|
42
48
|
|
|
49
|
+
### `geometra_wait_for` and loading banners
|
|
50
|
+
|
|
51
|
+
The tool input is **strict**: unknown keys are rejected (so mistaken names like `textGone` fail fast instead of being ignored). To wait until copy such as “Parsing…” or “Parsing your resume” is **gone**, pass a substring that matches the banner in **`text`** and set **`present`** to **`false`**. Example: `{ "text": "Parsing", "present": false }` (adjust the substring per site).
|
|
52
|
+
|
|
43
53
|
## Setup
|
|
44
54
|
|
|
45
55
|
<details>
|
|
@@ -251,19 +261,17 @@ Then in Claude Code (either backend):
|
|
|
251
261
|
Agent: geometra_connect({ url: "ws://localhost:3100" })
|
|
252
262
|
→ Connected. UI: button "Sign Up", textbox "Email", textbox "Password"
|
|
253
263
|
|
|
254
|
-
Agent:
|
|
255
|
-
→
|
|
256
|
-
|
|
257
|
-
Agent: geometra_click({ x: 250, y: 220 })
|
|
258
|
-
→ Clicked. Email input focused.
|
|
264
|
+
Agent: geometra_click({ role: "textbox", name: "Email" })
|
|
265
|
+
→ Clicked textbox "Email" (n:0) at (250, 220). Email input focused.
|
|
259
266
|
|
|
260
267
|
Agent: geometra_type({ text: "test@example.com" })
|
|
261
268
|
→ Typed. Email field updated.
|
|
262
269
|
|
|
263
|
-
Agent:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
270
|
+
Agent: geometra_click({
|
|
271
|
+
role: "button",
|
|
272
|
+
name: "Sign Up",
|
|
273
|
+
waitFor: { role: "dialog", name: "Success" }
|
|
274
|
+
})
|
|
267
275
|
→ Clicked. Success message visible.
|
|
268
276
|
```
|
|
269
277
|
|
|
@@ -312,18 +320,20 @@ For long application flows, prefer one of these patterns:
|
|
|
312
320
|
2. otherwise `geometra_form_schema`
|
|
313
321
|
3. then `geometra_fill_form`
|
|
314
322
|
3. `geometra_reveal` for far-below-fold targets such as submit buttons
|
|
315
|
-
4. `
|
|
316
|
-
5. `
|
|
323
|
+
4. `geometra_click({ ..., waitFor: ... })` when one action should also wait for the next semantic state
|
|
324
|
+
5. `geometra_run_actions` when you need mixed navigation + waits + field entry
|
|
325
|
+
6. `geometra_connect({ pageUrl, returnPageModel: true })` when you want connect + summary-first exploration in one turn
|
|
326
|
+
7. `geometra_page_model` + `geometra_expand_section` when you are still exploring the page rather than filling it
|
|
317
327
|
|
|
318
328
|
Typical batch:
|
|
319
329
|
|
|
320
330
|
```json
|
|
321
331
|
{
|
|
322
332
|
"actions": [
|
|
323
|
-
{ "type": "click", "
|
|
333
|
+
{ "type": "click", "role": "textbox", "name": "Full name" },
|
|
324
334
|
{ "type": "type", "text": "Taylor Applicant" },
|
|
325
335
|
{ "type": "upload_files", "paths": ["/Users/you/resume.pdf"], "fieldLabel": "Resume" },
|
|
326
|
-
{ "type": "
|
|
336
|
+
{ "type": "click", "role": "button", "name": "Submit", "waitFor": { "text": "Application submitted", "timeoutMs": 10000 } }
|
|
327
337
|
]
|
|
328
338
|
}
|
|
329
339
|
```
|
|
@@ -115,4 +115,59 @@ describe('connectThroughProxy recovery', () => {
|
|
|
115
115
|
await closePeer(freshPeer.wss);
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
|
+
it('keeps separate warm proxies for compatible headed and headless reuse', async () => {
|
|
119
|
+
const headedPeer = await createProxyPeer({
|
|
120
|
+
pageUrl: 'https://jobs.example.com/headed',
|
|
121
|
+
});
|
|
122
|
+
const headlessPeer = await createProxyPeer({
|
|
123
|
+
pageUrl: 'https://jobs.example.com/headless',
|
|
124
|
+
});
|
|
125
|
+
const headedRuntime = {
|
|
126
|
+
wsUrl: headedPeer.wsUrl,
|
|
127
|
+
closed: false,
|
|
128
|
+
close: vi.fn(async () => {
|
|
129
|
+
headedRuntime.closed = true;
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
const headlessRuntime = {
|
|
133
|
+
wsUrl: headlessPeer.wsUrl,
|
|
134
|
+
closed: false,
|
|
135
|
+
close: vi.fn(async () => {
|
|
136
|
+
headlessRuntime.closed = true;
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
mockState.startEmbeddedGeometraProxy
|
|
140
|
+
.mockResolvedValueOnce({ runtime: headedRuntime, wsUrl: headedPeer.wsUrl })
|
|
141
|
+
.mockResolvedValueOnce({ runtime: headlessRuntime, wsUrl: headlessPeer.wsUrl });
|
|
142
|
+
mockState.spawnGeometraProxy.mockRejectedValue(new Error('spawn fallback should not be used'));
|
|
143
|
+
try {
|
|
144
|
+
const headedSession = await connectThroughProxy({
|
|
145
|
+
pageUrl: 'https://jobs.example.com/headed',
|
|
146
|
+
headless: false,
|
|
147
|
+
});
|
|
148
|
+
expect(headedSession.proxyRuntime).toBe(headedRuntime);
|
|
149
|
+
disconnect();
|
|
150
|
+
const headlessSession = await connectThroughProxy({
|
|
151
|
+
pageUrl: 'https://jobs.example.com/headless',
|
|
152
|
+
headless: true,
|
|
153
|
+
});
|
|
154
|
+
expect(headlessSession.proxyRuntime).toBe(headlessRuntime);
|
|
155
|
+
disconnect();
|
|
156
|
+
const reusedHeadedSession = await connectThroughProxy({
|
|
157
|
+
pageUrl: 'https://jobs.example.com/headed',
|
|
158
|
+
headless: false,
|
|
159
|
+
});
|
|
160
|
+
expect(reusedHeadedSession.proxyRuntime).toBe(headedRuntime);
|
|
161
|
+
expect(mockState.startEmbeddedGeometraProxy).toHaveBeenCalledTimes(2);
|
|
162
|
+
expect(headedRuntime.close).not.toHaveBeenCalled();
|
|
163
|
+
expect(headlessRuntime.close).not.toHaveBeenCalled();
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
disconnect({ closeProxy: true });
|
|
167
|
+
expect(headedRuntime.close).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(headlessRuntime.close).toHaveBeenCalledTimes(1);
|
|
169
|
+
await closePeer(headedPeer.wss);
|
|
170
|
+
await closePeer(headlessPeer.wss);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
118
173
|
});
|
|
@@ -294,6 +294,28 @@ describe('batch MCP result shaping', () => {
|
|
|
294
294
|
},
|
|
295
295
|
});
|
|
296
296
|
});
|
|
297
|
+
it('can inline the page model into connect for the low-turn exploration path', async () => {
|
|
298
|
+
const handler = getToolHandler('geometra_connect');
|
|
299
|
+
const result = await handler({
|
|
300
|
+
pageUrl: 'https://jobs.example.com/application',
|
|
301
|
+
headless: true,
|
|
302
|
+
returnPageModel: true,
|
|
303
|
+
maxPrimaryActions: 4,
|
|
304
|
+
maxSectionsPerKind: 3,
|
|
305
|
+
});
|
|
306
|
+
const payload = JSON.parse(result.content[0].text);
|
|
307
|
+
expect(payload).toMatchObject({
|
|
308
|
+
connected: true,
|
|
309
|
+
transport: 'proxy',
|
|
310
|
+
pageUrl: 'https://jobs.example.com/application',
|
|
311
|
+
pageModel: {
|
|
312
|
+
viewport: { width: 1280, height: 800 },
|
|
313
|
+
archetypes: ['form'],
|
|
314
|
+
summary: { formCount: 1 },
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
expect(payload).not.toHaveProperty('formSchema');
|
|
318
|
+
});
|
|
297
319
|
it('returns compact form schemas without requiring section expansion', async () => {
|
|
298
320
|
const handler = getToolHandler('geometra_form_schema');
|
|
299
321
|
mockState.formSchemas = [
|
|
@@ -741,4 +763,222 @@ describe('query and reveal tools', () => {
|
|
|
741
763
|
},
|
|
742
764
|
});
|
|
743
765
|
});
|
|
766
|
+
it('clicks an offscreen semantic target by revealing it first', async () => {
|
|
767
|
+
const handler = getToolHandler('geometra_click');
|
|
768
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
769
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
770
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
771
|
+
children: [
|
|
772
|
+
node('form', 'Application', {
|
|
773
|
+
bounds: { x: 20, y: -200, width: 760, height: 1900 },
|
|
774
|
+
path: [0],
|
|
775
|
+
children: [
|
|
776
|
+
node('button', 'Submit application', {
|
|
777
|
+
bounds: { x: 60, y: 1540, width: 180, height: 40 },
|
|
778
|
+
path: [0, 0],
|
|
779
|
+
}),
|
|
780
|
+
],
|
|
781
|
+
}),
|
|
782
|
+
],
|
|
783
|
+
});
|
|
784
|
+
mockState.sendWheel.mockImplementationOnce(async () => {
|
|
785
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
786
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
787
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 1220 },
|
|
788
|
+
children: [
|
|
789
|
+
node('form', 'Application', {
|
|
790
|
+
bounds: { x: 20, y: -1420, width: 760, height: 1900 },
|
|
791
|
+
path: [0],
|
|
792
|
+
children: [
|
|
793
|
+
node('button', 'Submit application', {
|
|
794
|
+
bounds: { x: 60, y: 320, width: 180, height: 40 },
|
|
795
|
+
path: [0, 0],
|
|
796
|
+
}),
|
|
797
|
+
],
|
|
798
|
+
}),
|
|
799
|
+
],
|
|
800
|
+
});
|
|
801
|
+
bumpMockUiRevision();
|
|
802
|
+
return { status: 'updated', timeoutMs: 2500 };
|
|
803
|
+
});
|
|
804
|
+
const result = await handler({
|
|
805
|
+
id: 'n:0.0',
|
|
806
|
+
maxRevealSteps: 3,
|
|
807
|
+
revealTimeoutMs: 2500,
|
|
808
|
+
detail: 'minimal',
|
|
809
|
+
});
|
|
810
|
+
expect(mockState.sendWheel).toHaveBeenCalledTimes(1);
|
|
811
|
+
expect(mockState.sendClick).toHaveBeenCalledWith(mockState.session, 150, 340, undefined);
|
|
812
|
+
expect(result.content[0].text).toContain('Clicked button "Submit application" (n:0.0) at (150, 340) after 1 reveal step.');
|
|
813
|
+
});
|
|
814
|
+
it('can wait for a semantic post-click condition in the same click call', async () => {
|
|
815
|
+
const handler = getToolHandler('geometra_click');
|
|
816
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
817
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
818
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
819
|
+
children: [
|
|
820
|
+
node('button', 'Submit application', {
|
|
821
|
+
bounds: { x: 60, y: 320, width: 180, height: 40 },
|
|
822
|
+
path: [0],
|
|
823
|
+
}),
|
|
824
|
+
],
|
|
825
|
+
});
|
|
826
|
+
mockState.sendClick.mockImplementationOnce(async () => {
|
|
827
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
828
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
829
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
830
|
+
children: [
|
|
831
|
+
node('button', 'Submit application', {
|
|
832
|
+
bounds: { x: 60, y: 320, width: 180, height: 40 },
|
|
833
|
+
path: [0],
|
|
834
|
+
}),
|
|
835
|
+
node('dialog', 'Application submitted', {
|
|
836
|
+
bounds: { x: 240, y: 140, width: 420, height: 260 },
|
|
837
|
+
path: [1],
|
|
838
|
+
}),
|
|
839
|
+
],
|
|
840
|
+
});
|
|
841
|
+
bumpMockUiRevision();
|
|
842
|
+
return { status: 'updated', timeoutMs: 2000 };
|
|
843
|
+
});
|
|
844
|
+
const result = await handler({
|
|
845
|
+
role: 'button',
|
|
846
|
+
name: 'Submit application',
|
|
847
|
+
waitFor: {
|
|
848
|
+
role: 'dialog',
|
|
849
|
+
name: 'Application submitted',
|
|
850
|
+
timeoutMs: 5000,
|
|
851
|
+
},
|
|
852
|
+
detail: 'minimal',
|
|
853
|
+
});
|
|
854
|
+
expect(mockState.waitForUiCondition).toHaveBeenCalledWith(mockState.session, expect.any(Function), 5000);
|
|
855
|
+
expect(result.content[0].text).toContain('Post-click condition satisfied after');
|
|
856
|
+
expect(result.content[0].text).toContain('1 matching node(s).');
|
|
857
|
+
});
|
|
858
|
+
it('lets run_actions click a semantic target without manual coordinates', async () => {
|
|
859
|
+
const handler = getToolHandler('geometra_run_actions');
|
|
860
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
861
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
862
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
863
|
+
children: [
|
|
864
|
+
node('form', 'Application', {
|
|
865
|
+
bounds: { x: 20, y: -200, width: 760, height: 1900 },
|
|
866
|
+
path: [0],
|
|
867
|
+
children: [
|
|
868
|
+
node('button', 'Submit application', {
|
|
869
|
+
bounds: { x: 60, y: 1540, width: 180, height: 40 },
|
|
870
|
+
path: [0, 0],
|
|
871
|
+
}),
|
|
872
|
+
],
|
|
873
|
+
}),
|
|
874
|
+
],
|
|
875
|
+
});
|
|
876
|
+
mockState.sendWheel.mockImplementationOnce(async () => {
|
|
877
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
878
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
879
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 1220 },
|
|
880
|
+
children: [
|
|
881
|
+
node('form', 'Application', {
|
|
882
|
+
bounds: { x: 20, y: -1420, width: 760, height: 1900 },
|
|
883
|
+
path: [0],
|
|
884
|
+
children: [
|
|
885
|
+
node('button', 'Submit application', {
|
|
886
|
+
bounds: { x: 60, y: 320, width: 180, height: 40 },
|
|
887
|
+
path: [0, 0],
|
|
888
|
+
}),
|
|
889
|
+
],
|
|
890
|
+
}),
|
|
891
|
+
],
|
|
892
|
+
});
|
|
893
|
+
bumpMockUiRevision();
|
|
894
|
+
return { status: 'updated', timeoutMs: 2500 };
|
|
895
|
+
});
|
|
896
|
+
const result = await handler({
|
|
897
|
+
actions: [
|
|
898
|
+
{
|
|
899
|
+
type: 'click',
|
|
900
|
+
role: 'button',
|
|
901
|
+
name: 'Submit application',
|
|
902
|
+
maxRevealSteps: 3,
|
|
903
|
+
revealTimeoutMs: 2500,
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
stopOnError: true,
|
|
907
|
+
includeSteps: true,
|
|
908
|
+
detail: 'minimal',
|
|
909
|
+
});
|
|
910
|
+
const payload = JSON.parse(result.content[0].text);
|
|
911
|
+
const steps = payload.steps;
|
|
912
|
+
expect(mockState.sendWheel).toHaveBeenCalledTimes(1);
|
|
913
|
+
expect(mockState.sendClick).toHaveBeenCalledWith(mockState.session, 150, 340, undefined);
|
|
914
|
+
expect(steps[0]).toMatchObject({
|
|
915
|
+
index: 0,
|
|
916
|
+
type: 'click',
|
|
917
|
+
ok: true,
|
|
918
|
+
at: { x: 150, y: 340 },
|
|
919
|
+
revealSteps: 1,
|
|
920
|
+
target: { id: 'n:0.0', role: 'button', name: 'Submit application' },
|
|
921
|
+
wait: 'updated',
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
it('lets run_actions click and wait for a semantic post-condition in one step', async () => {
|
|
925
|
+
const handler = getToolHandler('geometra_run_actions');
|
|
926
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
927
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
928
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
929
|
+
children: [
|
|
930
|
+
node('button', 'Submit application', {
|
|
931
|
+
bounds: { x: 60, y: 320, width: 180, height: 40 },
|
|
932
|
+
path: [0],
|
|
933
|
+
}),
|
|
934
|
+
],
|
|
935
|
+
});
|
|
936
|
+
mockState.sendClick.mockImplementationOnce(async () => {
|
|
937
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
938
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
939
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
940
|
+
children: [
|
|
941
|
+
node('button', 'Submit application', {
|
|
942
|
+
bounds: { x: 60, y: 320, width: 180, height: 40 },
|
|
943
|
+
path: [0],
|
|
944
|
+
}),
|
|
945
|
+
node('dialog', 'Application submitted', {
|
|
946
|
+
bounds: { x: 240, y: 140, width: 420, height: 260 },
|
|
947
|
+
path: [1],
|
|
948
|
+
}),
|
|
949
|
+
],
|
|
950
|
+
});
|
|
951
|
+
bumpMockUiRevision();
|
|
952
|
+
return { status: 'updated', timeoutMs: 2000 };
|
|
953
|
+
});
|
|
954
|
+
const result = await handler({
|
|
955
|
+
actions: [
|
|
956
|
+
{
|
|
957
|
+
type: 'click',
|
|
958
|
+
role: 'button',
|
|
959
|
+
name: 'Submit application',
|
|
960
|
+
waitFor: {
|
|
961
|
+
role: 'dialog',
|
|
962
|
+
name: 'Application submitted',
|
|
963
|
+
timeoutMs: 5000,
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
stopOnError: true,
|
|
968
|
+
includeSteps: true,
|
|
969
|
+
detail: 'minimal',
|
|
970
|
+
});
|
|
971
|
+
const payload = JSON.parse(result.content[0].text);
|
|
972
|
+
const steps = payload.steps;
|
|
973
|
+
expect(steps[0]).toMatchObject({
|
|
974
|
+
index: 0,
|
|
975
|
+
type: 'click',
|
|
976
|
+
ok: true,
|
|
977
|
+
postWait: {
|
|
978
|
+
present: true,
|
|
979
|
+
matchCount: 1,
|
|
980
|
+
filter: { role: 'dialog', name: 'Application submitted' },
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
});
|
|
744
984
|
});
|