chromeflow 0.3.0 → 0.5.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/CLAUDE.md +42 -5
- package/dist/tools/flow.js +19 -4
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -126,6 +126,17 @@ use `take_and_copy_screenshot()` — it saves a PNG to ~/Downloads and copies it
|
|
|
126
126
|
- `set_file_input` accepts CSS selectors as the hint (e.g. `#import-problem-file`,
|
|
127
127
|
`.upload-input`) in addition to label text. Use selectors when file inputs are hidden
|
|
128
128
|
behind custom UIs and have no visible label.
|
|
129
|
+
- **Replacing an already-uploaded file**: after `set_file_input` succeeds, the input
|
|
130
|
+
becomes invisible and a "Remove" span/button typically appears near the upload area.
|
|
131
|
+
To replace the file: `click_element("Remove", nth=N)` (the right `nth` if there are
|
|
132
|
+
multiple), then call `set_file_input(hint, newPath)` again — the same hidden input is
|
|
133
|
+
recycled and accepts the new file. Verify with `get_form_fields()` between the two
|
|
134
|
+
steps so you're sure the input has reappeared.
|
|
135
|
+
- **Forcing auto-save on idempotent text edits** (e.g. keep-alive loop on an
|
|
136
|
+
auto-saving DataAnnotation form): some auto-save logic diffs against the last-saved
|
|
137
|
+
value and skips no-op writes. To force a real save on each tick without changing
|
|
138
|
+
visible content, toggle a trailing space — add when absent, remove when present.
|
|
139
|
+
`fill_input` value comparison handles both directions transparently.
|
|
129
140
|
- After any radio/checkbox click that reveals new fields, call `get_form_fields()` again —
|
|
130
141
|
the inventory will include the new fields and warn if more hidden ones still exist.
|
|
131
142
|
- If a form has collapsible sections, expand them all before calling `get_form_fields()` so
|
|
@@ -144,6 +155,12 @@ use `take_and_copy_screenshot()` — it saves a PNG to ~/Downloads and copies it
|
|
|
144
155
|
- `switch_to_tab("1")` switches by tab number; `switch_to_tab("form")` matches by URL or title substring.
|
|
145
156
|
- Before navigating away from a partially-filled form, call `save_page_state()` so the form
|
|
146
157
|
can be restored if the tab reloads or the page loses its state on return.
|
|
158
|
+
- **In long-lived self-rescheduling loops**, the active tab can silently drift mid-session
|
|
159
|
+
(the user navigates manually while AFK, or another tab steals focus). At the start of
|
|
160
|
+
every loop iteration, call `list_tabs` and verify the active tab's URL matches your
|
|
161
|
+
expected target — if not, `switch_to_tab(<URL or title substring>)` before running
|
|
162
|
+
`execute_script` or any other tab-scoped tool. Without this guard, scripts run on the
|
|
163
|
+
wrong tab and fail with confusing "undefined" errors that look like page bugs.
|
|
147
164
|
|
|
148
165
|
## Error handling
|
|
149
166
|
|
|
@@ -194,7 +211,12 @@ set_file_input("Photos", "/path/2.jpg", verify_selector=".photo-thumbnail:nth-of
|
|
|
194
211
|
```
|
|
195
212
|
The page-level file count is reported in the response — use it to spot uploaders that consume-and-reset the input vs uploaders that keep the file there.
|
|
196
213
|
|
|
197
|
-
**Waiting for async results** (build, save, deploy): `wait_for_selector(selector, timeout)` — never poll with screenshots.
|
|
214
|
+
**Waiting for async results** (build, save, deploy): `wait_for_selector(selector, timeout)` — never poll with screenshots. `wait_for_selector` pierces open shadow roots, so a selector inside a web component (Outlier task UI, Lit/Stencil widget) matches without ceremony.
|
|
215
|
+
|
|
216
|
+
**Waiting for a shadow host's tree to attach** (e.g. SPA route flips where `<my-host>` appears 10s before its shadow content hydrates, and `wait_for_selector("my-host")` resolves while `host.shadowRoot` is still null): pass `shadow_root=true`. The wait then requires the matched element's `.shadowRoot` to be non-null, not just for the host element to exist.
|
|
217
|
+
```
|
|
218
|
+
wait_for_selector("iframe", shadow_root=true) — wait until the iframe both exists AND has an attached shadowRoot
|
|
219
|
+
```
|
|
198
220
|
|
|
199
221
|
**Waiting for an existing region to update** (e.g. click Save, then get the confirmation toast; send a chat message, then get the reply): `wait_for_change(selector)` uses a MutationObserver on the element's subtree and returns its new text content as soon as the mutation settles. Prefer this over `wait_for_selector` + `get_page_text` when the element already exists and you just need its next state — one call instead of two, no polling.
|
|
200
222
|
|
|
@@ -265,10 +287,25 @@ document.body.style.zoom = '1';
|
|
|
265
287
|
1. Retry the exact same `execute_script` call
|
|
266
288
|
2. If still failing, use `find_and_highlight` to show the user a download button to click manually
|
|
267
289
|
|
|
268
|
-
**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
290
|
+
**React-controlled native radios/checkboxes that don't update `checked`**: `click_element`
|
|
291
|
+
auto-handles this for native `<input type=radio>` and `<input type=checkbox>` inputs
|
|
292
|
+
(including labels that wrap or `for=` reference them). The flow:
|
|
293
|
+
- If a radio is already `checked=true`, `click_element` skips the click — re-clicking can
|
|
294
|
+
toggle it OFF on React forms whose `onChange` interprets the click as a deselect. The
|
|
295
|
+
response says `"X — radio already checked, click skipped"`.
|
|
296
|
+
- If the standard click fires but the input's `checked` state didn't change as expected
|
|
297
|
+
(radio still unchecked, or checkbox didn't toggle), `click_element` automatically
|
|
298
|
+
dispatches the full pointer-event chain (`pointerdown → mousedown → pointerup → mouseup
|
|
299
|
+
→ click`) on the input. The response says `"now checked (after pointer-chain fallback)"`.
|
|
300
|
+
|
|
301
|
+
You only need to drop into `execute_script` for the no-native-input case below.
|
|
302
|
+
|
|
303
|
+
**Shadow DOM `[role=radio]` / role-only custom radios silently no-op**: On sites like
|
|
304
|
+
Outlier where the radio is a `[role=radio]` div with no underlying `<input>`,
|
|
305
|
+
`click_element`'s native-input fallback can't help — the click target has no `.checked`
|
|
306
|
+
property to verify. Two things must be true: (a) the element must be scrolled into view
|
|
307
|
+
FIRST (`scrollIntoView({block:'center'})`), and (b) the full pointer-event chain must
|
|
308
|
+
fire — not just `click()`:
|
|
272
309
|
```js
|
|
273
310
|
['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t =>
|
|
274
311
|
el.dispatchEvent(new MouseEvent(t, {bubbles: true, cancelable: true}))
|
package/dist/tools/flow.js
CHANGED
|
@@ -92,7 +92,15 @@ If the click causes page navigation, this resolves when the new page finishes lo
|
|
|
92
92
|
Examples: wait for a build to finish, a success/error message to appear, a modal to open.
|
|
93
93
|
After it resolves, use get_page_text to read the result rather than taking a screenshot.
|
|
94
94
|
For long-running server-side processes (e.g. a query job that may take minutes), set poll_interval
|
|
95
|
-
to 15 seconds so the page is checked gently rather than hammered every 500ms
|
|
95
|
+
to 15 seconds so the page is checked gently rather than hammered every 500ms.
|
|
96
|
+
|
|
97
|
+
Pierces open shadow roots automatically \u2014 selectors for elements inside web components
|
|
98
|
+
(Outlier task UI, Lit/Stencil widgets) match without needing a shadow-DOM-aware caller.
|
|
99
|
+
|
|
100
|
+
Pass \`shadow_root: true\` when the matched element is itself a shadow host whose tree
|
|
101
|
+
hasn't attached yet \u2014 common after SPA route transitions where the host element appears
|
|
102
|
+
seconds before its shadow content hydrates. Without this, wait_for_selector("the-host")
|
|
103
|
+
resolves on the empty host and the next execute_script(host.shadowRoot) returns null.`,
|
|
96
104
|
{
|
|
97
105
|
selector: z.string().describe(
|
|
98
106
|
`CSS selector to wait for (e.g. '.deploy-ready', '[data-status="error"]', '.toast-error')`
|
|
@@ -100,14 +108,21 @@ to 15 seconds so the page is checked gently rather than hammered every 500ms.`,
|
|
|
100
108
|
timeout: z.number().optional().describe("Max seconds to wait (default 30)"),
|
|
101
109
|
poll_interval: z.number().optional().describe(
|
|
102
110
|
"How often to check for the selector, in seconds (default 0.5). Set to 15 when waiting for a slow server-side process."
|
|
111
|
+
),
|
|
112
|
+
shadow_root: z.boolean().optional().describe(
|
|
113
|
+
"If true, also require the matched element to have an attached shadowRoot (not null). Use after SPA navigations where the shadow host appears before its tree hydrates. Default false."
|
|
103
114
|
)
|
|
104
115
|
},
|
|
105
|
-
async ({ selector, timeout = 30, poll_interval }) => {
|
|
116
|
+
async ({ selector, timeout = 30, poll_interval, shadow_root }) => {
|
|
106
117
|
const timeoutMs = timeout * 1e3;
|
|
107
118
|
const pollMs = poll_interval ? poll_interval * 1e3 : void 0;
|
|
108
|
-
await bridge.request(
|
|
119
|
+
await bridge.request(
|
|
120
|
+
{ type: "wait_for_selector", selector, timeout: timeoutMs, refresh: pollMs, shadow_root },
|
|
121
|
+
timeoutMs + 5e3
|
|
122
|
+
);
|
|
123
|
+
const suffix = shadow_root ? " (with attached shadowRoot)" : "";
|
|
109
124
|
return {
|
|
110
|
-
content: [{ type: "text", text: `Selector "${selector}" found on page.` }]
|
|
125
|
+
content: [{ type: "text", text: `Selector "${selector}" found on page${suffix}.` }]
|
|
111
126
|
};
|
|
112
127
|
}
|
|
113
128
|
);
|
package/package.json
CHANGED