Repository Public Research preview
1# How it works, and why23Everything below was observed on Safari 27.0 (macOS 27.0) while building this skill's test suite.45## Two channels67- **AppleScript** talks to Safari itself: windows, tabs, URLs, titles.8 `tell application "Safari" to get URL of tab 2 of window id 1234`.9- **`do JavaScript … in tab`** runs code inside the page, in the page's own world, with its10 cookies. It needs Safari's "Allow JavaScript from Apple Events" setting.1112Both need the macOS **Automation** permission (the calling app controlling Safari). Error `-1743`13means it was refused: System Settings → Privacy & Security → Automation.1415## Windows have ids; use them1617`id of window` is a stable integer for the life of the window. `window 1` is "frontmost", which18changes the moment the user clicks elsewhere — a script addressing `window 1` will eventually act19on the wrong page. Tabs have no id, only an `index` inside their window, which shifts when tabs20before them close. Hence: one dedicated window per automation, addressed by id.2122A closed window can linger as an invisible object with zero tabs. When listing windows, keep those23that are `visible` and have at least one tab.2425The window id is also the system's window number, so `screencapture -x -o -l <id> out.png`26captures exactly that window, even when it is behind others (not when minimised).2728## Knowing when a page is ready2930This is the part most scripts get wrong.3132When a navigation starts, Safari immediately sets the tab's `URL` property to the **destination**.33The old document keeps answering `do JavaScript` until the new one commits. During that interval:3435| | value |36|---|---|37| `URL of tab` (AppleScript) | the new address |38| `location.href` (JavaScript) | the old address |39| `document.readyState` | `"complete"` — of the old page |4041So "wait until `readyState` is complete" returns instantly and you act on the page you were42leaving. A newly created tab behaves the same way: it answers as `about:blank` / `complete` first.4344The reliable test: the document's URL equals the tab's URL **and** `readyState` is `complete`,45twice in a row. That is what `wait-for-load.applescript` and the tool do. A navigation that never46commits (a download, an unreachable server) ends in a timeout, reported as such.4748`readyState` says nothing about content loaded by scripts afterwards. For that, wait for the thing49you need: a selector, or a piece of text.5051## Getting values in and out5253- `do JavaScript` returns strings, numbers and booleans. An object, `undefined`, or a promise54 yields no value at all (an AppleScript "variable not defined" error if you use it). Return55 `JSON.stringify(...)`.56- It cannot wait for a promise. For asynchronous work, start it, store the outcome on a `window`57 property, and poll with further calls. For a simple authenticated GET, a synchronous58 `XMLHttpRequest` avoids the polling (see `fetch-base64.applescript`).59- Never build JavaScript by concatenating user text into source. Pass the code as an argument60 (`on run argv`) and pass values percent-encoded, decoding them inside the page. The recipes do61 this; the test suite feeds them quotes, backslashes and `');` sequences.62- Inside a `tell application "Safari"` block the word `tab` is a browser tab, not the tab63 character. Use `character id 9`.64- Results arrive on standard output, so large values (a megabyte of base64) are fine; arguments are65 limited to roughly 1 MB, so large inputs must be sent in chunks.6667## Forms6869- Frameworks replace the `value` property of inputs to track changes. Assigning `el.value` goes70 through their replacement and is ignored on the next render. Call the native setter71 (`Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set`) and dispatch72 `input` and `change` with `bubbles: true`.73- Checkboxes and radio buttons: `click()` them when their state differs from what you want.74- `<select>`: set the value through the native setter, then dispatch `change`.75- Submit with `form.requestSubmit()`, which runs validation and submit handlers; `form.submit()`76 skips both.77- A file input cannot be given a path. Build a `File` from bytes, put it in a `DataTransfer`, and78 assign `input.files = dt.files`, then dispatch `change`.7980## Reaching into components8182`document.querySelector` does not see inside shadow roots or iframes. Walk them: for each element83with an open `shadowRoot`, search it too; for each `iframe`, try `contentDocument` (it throws or84is null across origins). Closed shadow roots and cross-origin frames stay out of reach.8586## Dialogs8788A JavaScript `alert`, `confirm` or `prompt` halts the page's scripts — including yours — until a89person dismisses it. Replace the three functions in the page before triggering anything that may90raise one, recording the message and returning a fixed answer. The tool does this each time it91touches a page.9293## What Apple events cannot do9495Private windows, Reader mode, a PDF displayed in a tab, browser settings, extensions, and the96address bar are not scriptable this way. UI scripting through System Events can reach some of97them, but it needs Accessibility permission, depends on the interface language and layout, and98takes over the keyboard and pointer; it is not used here.99