# ADR-0003: Runtime.callFunctionOn `this` Binding

## Status

Accepted (v0.2.6 bug fix)

## Context

`Runtime.callFunctionOn` is used to execute JavaScript in the context of a DOM node. The CDP documentation states that objects can be passed as arguments, but there's a critical detail:

> "The object is passed as `this` to the function."

Many implementations (including our early versions) incorrectly pass the element as an argument:

```python
# WRONG: Element passed as argument
def click(self, ref: str):
    self._send_cdp("Runtime.callFunctionOn", {
        "functionDeclaration": "(element) => element.click()",
        "objectId": object_id,
        "arguments": [{"objectId": object_id}],  # WRONG
        "returnByValue": True,
    })
```

## Decision

The target object is bound to `this`, NOT passed as argument:

```python
# CORRECT: Element bound to `this`
def click(self, ref: str):
    self._send_cdp("Runtime.callFunctionOn", {
        "functionDeclaration": "function() { this.click(); }",
        "objectId": object_id,
        "returnByValue": True,
    })

# Arrow function alternative
# functionDeclaration: "() => { this.click(); }"
```

## Consequences

### Positive

- **Correct behavior** — Follows CDP specification
- **Simpler arguments** — No need to pass element as argument
- **Works for all methods** — click(), focus(), scrollIntoView()

### Negative

- **Confusing API** — `this` binding is non-obvious to JavaScript developers
- **Arrow function caveat** — Must use regular functions or be careful with `this`

## Examples

### Before (v0.2.5 — Wrong)

```python
# BROKEN in v0.2.5
def click(self, ref: str):
    return self._send_cdp("Runtime.callFunctionOn", {
        "functionDeclaration": "(element) => element.click()",
        "objectId": self.refs[ref],
        "arguments": [{"objectId": self.refs[ref]}],  # Passes null at runtime
        "returnByValue": True,
    })
# Result: element is null, click fails silently
```

### After (v0.2.6 — Correct)

```python
# FIXED in v0.2.6
def click(self, ref: str):
    return self._send_cdp("Runtime.callFunctionOn", {
        "functionDeclaration": "function() { this.click(); }",
        "objectId": self.refs[ref],
        "returnByValue": True,
    })
# Result: this == element, click() works
```

### Affected Methods

| Method | Before | After |
|--------|--------|-------|
| `click()` | ❌ `(element) => element.click()` | ✅ `function() { this.click(); }` |
| `type_text()` | ❌ `(el, text) => el.value = text` | ✅ `function(text) { this.value = text; }` |
| `hover()` | ❌ `(element) =>...` | ✅ `function() { this.dispatchEvent(...); }` |
| `focus()` | ❌ `(element) =>...` | ✅ `function() { this.focus(); }` |

### Arrow Functions (Caveat)

Arrow functions have lexical `this`:

```python
# WRONG: `this` is lexical (window), not bound
"functionDeclaration": "() => { this.click(); }"  # this != element

# CORRECT: Use regular function
"functionDeclaration": "function() { this.click(); }"  # this == element
```

Alternatively, use `call()` to explicitly bind:

```python
"functionDeclaration": "() => { this.click(); }"  # Works because CDP binds `this`
```

Actually, CDP documentation states that `executionContextId` or `objectId` determines `this`, so arrow functions DO work:

```javascript
// CDP binds `this` regardless of arrow function
() => { this.click(); }  // `this` is bound by CDP
```

## References

- [CDP Runtime.callFunctionOn](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-callFunctionOn)
- [Changelog — v0.2.6](../changelog.md)