惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
小众软件
小众软件
Blog — PlanetScale
Blog — PlanetScale
T
The Blog of Author Tim Ferriss
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
博客园 - Franky
F
Fortinet All Blogs
D
Docker
博客园 - 司徒正美
腾讯CDC
Recent Announcements
Recent Announcements
The Cloudflare Blog
B
Blog RSS Feed
GbyAI
GbyAI
T
Tailwind CSS Blog
雷峰网
雷峰网
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Why element.click() Isn't a Click
אחיה כהן · 2026-05-17 · via DEV Community

My AI agent had a checkbox to tick. A multi-step form: tick two boxes, hit Next. It ticked the boxes. It hit Next. The form rejected it and snapped back to step one — every time.

The boxes were visibly checked. The DOM said checked = true. The agent had done everything right. The form still didn't believe it.

Over the next day I shipped four patch releases of safari-mcp — v2.10.6 through v2.10.9 — and still didn't completely win. Here's what each layer of the stack taught me about why a programmatic click isn't a click.

The tell: isTrusted

When software clicks, it calls element.click() or dispatches a MouseEvent. The handler runs — but the event carries isTrusted: false. That flag is the browser stating, on the record, that no human did this.

Most code never checks. But the modern stack has at least four layers that do, and each rejects a forged click in its own way. I met all four.

Layer 1 — the component library

The form used react-select. My tool opened the dropdown by clicking the chevron, then clicked the option. Fine — for the first few rows. Past row four, clicking the chevron did nothing. No menu. The element still had a live React fiber; its pointer handler had simply, silently, stopped responding.

So I stopped driving the UI. The v2.10.6 fix walks the React fiber up from the target node, finds the Select component, and calls its onChange directly — with the same { action: 'select-option', option, name } payload react-select dispatches internally. No menu, no chevron, no click.

Lesson: when a component's visible affordance gets flaky, its internal API usually isn't. Reach for the fiber.

Layer 2 — the framework

Next layer: Vue 3. My tool clicked the checkbox. The DOM .checked flipped to true. Vue's reactive v-model proxy did not.

So the box looked checked, but Vue's internal state still held the old value — and the next form submission read Vue's state, not the DOM. That was the snap-back.

The v2.10.7 fix is belt-and-suspenders: after the click, redispatch input and change with composed: true so they cross Shadow DOM and Vue Teleport portals, and reset React's _valueTracker for the shared React-checkbox case. Now the reactive layer hears the change — not just the DOM.

Lesson: flipping a DOM property is not the same as telling the framework you flipped it.

Layer 3 — the browser's own geometry

Safari MCP can also do real clicks — actual OS-level CGEvent mouse events at screen coordinates — for cases synthetic clicks can't reach.

To turn a page coordinate into a screen coordinate, you need the height of everything above the web content: title bar, toolbar, tab bar. I had hardcoded it at 74 px.

On modern Safari the chrome above the content is closer to 90 px. Every native click landed ~16 px high. Often that's still inside the target row, so it sort of worked — but for a button near whitespace, 16 px is a hit versus a silent miss.

The v2.10.8 fix: stop guessing. Compute outerHeight - innerHeight in JavaScript at click time, with a sanity range and a fallback. The browser already knows how tall its own chrome is. Ask it.

Lesson: never hardcode a number the platform will hand you for free.

Layer 4 — the operating system

Those OS-level clicks need macOS Accessibility permission. macOS stores that grant in its TCC database, keyed to the code-signing identifier of the binary asking.

My helper binary was ad-hoc signed with a hash-based identifier — a new string on every rebuild. So every npm install produced a binary macOS had never seen. The Accessibility grant from yesterday was bound to yesterday's identifier; today's binary inherited nothing.

The symptom was maddening: the helper reported success, no clicks reached the page, and System Settings showed the permission as granted — for the stale identifier.

The v2.10.9 fix: postinstall re-signs the helper with a stable identifier so the grant survives upgrades.

Lesson: if a permission keeps "randomly" resetting, check whether the thing requesting it has a stable identity.

Layer 5 — the one I haven't beaten

Four releases, four layers, one day. And then, on macOS 26, a click still didn't land.

With everything above fixed — right coordinates, stable permission, valid target — CGEvent.postToPid reports a successful injection and the page receives nothing. No isTrusted event at all. The private window-targeting fields the call needs are still present in the macOS 26 SDK; the event simply never crosses into Safari's sandboxed WebContent process.

I can't yet prove it's an OS change rather than something I'm still missing — so it's tracked in the open as issue #29, with the full repro and everything ruled out. If you've automated macOS UI and have a theory, that thread's the place.

"Just click the button"

A click looks atomic. It isn't. Before a real finger reaches a checkbox, an event has to satisfy a component library, a reactive framework, the browser's coordinate math, and the OS permission model — all in one motion — and on a new OS release, the OS can quietly change the rules underneath all of it.

element.click() skips the finger and asks four contracts to take its word for it. Some of them won't. If you're building automation for AI agents, budget for every layer — and keep your release numbers cheap. Some days you'll spend four.


safari-mcp is open source — native Safari automation for AI agents on macOS, no Chrome, no headless. github.com/achiya-automation/safari-mcp