Add file upload: setfiles (known input) + upload (file chooser)#2
Add file upload: setfiles (known input) + upload (file chooser)#2motin wants to merge 4 commits into
setfiles (known input) + upload (file chooser)#2Conversation
Two Playwright-equivalent entry points for getting local files into a page, both ending in DOM.setFileInputFiles (the only reliable way to populate an <input type=file> — its .files is read-only to page JS, and a value-setter won't route through a React onChange): - `setfiles <ref> <path…>` — resolve a snapshot ref to a RemoteObject and set files on a known, static <input type=file>. Fires input/change. Errors helpfully (pointing at `upload`) if the ref isn't a file input. - `upload --click <ref> <path…>` — arm Page.setInterceptFileChooserDialog, click the trigger, wait for Page.fileChooserOpened, and set files on the backendNodeId Chrome reports. Handles the transient/custom-button case where a button creates+clicks its own throwaway input (App Store Connect "Attach File", many React dropzones) — setting the static input there is useless. Paths resolve from cwd and are validated to exist; multiple paths upload as a multi-file selection. New core in src/core/upload.ts; `findRefExpr` added to dom.ts to resolve a ref to an object handle. README + SKILL.md updated. Also syncs the stale package-lock version to match package.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DcStnfmjz5VPtqm9Tmh57B
…cess
Field report from real App Store Connect: `upload --click` reported "uploaded 1
file" but nothing staged — input.files stayed 0, no chip. Root cause: ASC's
"Attach File" creates a *transient* <input type=file>, .click()s it, and never
appends it to the document, reading it through a DELEGATED change handler
(React-style). DOM.setFileInputFiles sets .files and fires `change`, but on a
detached node that event has no document ancestors to bubble through, so the
delegated handler never runs. A node-bound listener fires detached; a delegated
one does not — which is why the original synthetic test (direct listener) passed
and ASC didn't.
Fix in uploadViaChooser, after setFileInputFiles:
- Resolve the backendNodeId to a live handle and VERIFY (isConnected, files.length).
- If the input is detached, re-attach it (hidden) to the document and re-dispatch
bubbling input+change so delegation / React onChange actually fires.
- Return {count, connected, reconnected}. The command now FAILS (exit 1) when
count===0 instead of claiming success, notes when it had to reconnect, and
reminds to re-snapshot to confirm the chip appeared.
Validated against a faithful repro covering four handler patterns (direct vs
document-delegated × detached vs appended-then-removed vs appended-stays): the
detached+delegated case (the ASC shape) went from silent no-op to staged; the
others still stage. Real-ASC confirmation still pending a live run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcStnfmjz5VPtqm9Tmh57B
|
Follow-up from a real App Store Connect run (commit e371f5b).
Hardening, after
Validated against a repro covering direct-vs-delegated × detached/removed/stays. Caveat noted in the thread: if a page delegates at a deep container that isn't an ancestor of |
…files Live App Store Connect debugging showed the previous count===0 check was a false NEGATIVE: ASC (like many uploaders) resets the input to "" the instant its change handler consumes the file, so "0 files remain" actually means *delivered*, not *failed* — the hardened command was reporting a real success as an error. Fix: install a one-shot `change` listener on the resolved node BEFORE setFileInputFiles, recording the file count at dispatch time (survives the app's reset). uploadViaChooser now returns `delivered` alongside `count`: - delivered === 0 -> the change fired with no file (input torn down first): real failure, exit 1. - delivered >= 1, count 0 -> delivered; app consumed + reset the input (normal). Report success but flag that this proves delivery, not staging — verify the UI. - delivered >= 1, count >= 1 -> set on a plain input that retains the file. Validated against synthetic patterns (direct/delegated × detached/appended, incl. an ASC-style reset-after-consume) and against real ASC: the 3.9 MB mp4 is delivered to ASC's handler (confirmed separately: ASC reads the blob bytes and the video loads), though ASC's own client-side staging doesn't render a chip for a programmatically-set file — an ASC-internal limitation beyond the input layer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DcStnfmjz5VPtqm9Tmh57B
…e attachments) Hard-won from live App Store Connect use: `upload`/`setfiles` stage successfully even when the app immediately resets input.files to 0 (so judge by the `change` event / "delivered", not residual files), the staged file often appears as a row in an attachment LIST rather than a single chip (verify via snapshot/screenshot), and each successful run ADDS another attachment — blind retries silently create duplicates. Documents the behavior; no code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DcStnfmjz5VPtqm9Tmh57B
Adds Playwright-style file upload — the one capability missing for forms that take attachments.
Why
App Store Connect's "Reply to App Review" has an "Attach File" button that opens a transient file input via a native chooser. We couldn't upload because (a) the CLI had no file-set command, (b) setting the static
input[type=file]via JS/DOM.setFileInputFilesdoes not route through the React handler — the button creates+clicks its own input, and (c) the file the button uses only exists while the chooser is open. The robust fix is to intercept the chooser, exactly like Playwright'sfileChooser/setInputFiles.What
Two entry points, mirroring Playwright's two paths. Both end in
DOM.setFileInputFiles(a file input's.filesis read-only to page JS, soeval/value-setters can't populate it; CDP can, and fires trustedinput/change):setfiles -s <session> <ref> <path…>— set files on a known, snapshot-able<input type=file>. Resolves the ref to a RemoteObject (objectId), validates it's a file input (else errors pointing atupload), and sets files.upload -s <session> --click <ref> <path…>— armPage.setInterceptFileChooserDialog, click the trigger, wait forPage.fileChooserOpened, thenDOM.setFileInputFiles({ backendNodeId, files }), then disable interception. Handles the transient-input / custom-button case (ASC "Attach File", React dropzones).Paths resolve from cwd and are validated to exist; multiple paths upload as a multi-file selection.
Implementation
src/core/upload.ts(setFilesByRef,uploadViaChooser);findRefExpradded todom.tsto resolve a ref to an object handle. Commands insrc/commands/{setfiles,upload}.ts, registered inindex.ts. Reuses the existing per-target CDP core (no hand-rolled socket — that's what avoids theOrigin403 from Chrome).package-lock.jsonversion to matchpackage.json.Validation
npm install && npm run typecheck && npm run buildclean. Ran the build (node dist/index.js …) against a scratch tab with custom HTML containing both a static<input type=file>and a button that creates+clicks a transient input:setfiles e1 <file>→ static input'schangehandler fired (filename+size observed).upload --click e2 <file>→ transient input'schangehandler fired (filename+size observed).--clickall behave correctly. No live tabs touched.🤖 Generated with Claude Code