Skip to content

Cannot submit Ember.js modal forms — framework ignores all programmatic input #148

@cameronwolf-coder

Description

@cameronwolf-coder

Bug Report: Cannot submit Ember.js modal forms — framework ignores all programmatic input

Severity: High — blocks real-world automation of any Ember.js app
Component: Browser Harness (helpers.py)
Reproduction Rate: 100%
Platform: Headless Chrome on Xvfb :99, Ubuntu 24.04


Summary

Browser Harness cannot fill and submit forms inside Ember.js 3.x modal dialogs. The Ember framework maintains internal two-way data binding state that is decoupled from the DOM. Setting the DOM value property (via js(), type_text(), cdp("Input.insertText"), or cdp("Input.dispatchKeyEvent")) updates the visible input but does not update Ember's internal tracked state. When the submit button is clicked, Ember validates its own state (not the DOM), finds the field empty, and silently refuses to fire the network request.

This is not a CAPTCHA or anti-bot challenge — it's a framework architecture issue where Ember's data binding is the source of truth, not the DOM.


Environment

  • Browser Harness: v0.1.0 (71f1b3b)
  • Chrome: Headless via chrome-harness systemd service, port 9333
  • Display: Xvfb :99, 1920x1080x24
  • Target site: https://www.linkedin.com/developers/apps/230849049/products/mdp-settings
  • Ember version: 3.28.12
  • Component library: Artdeco (LinkedIn's design system)

Steps to Reproduce

  1. Start browser harness:

    bh <<'EOF'
    goto("https://www.linkedin.com/developers/apps/230849049/products/mdp-settings")
    wait_for_load()
    wait(3)
    
    # Open the "Add Ad Account" modal
    js("[...document.querySelectorAll('button')].find(b=>b.textContent.includes('Add Ad Account'))?.click()")
    wait(3)
    
    # Fill the input (Ember TextField component, id="adAccountInput")
    js("document.querySelector('#adAccountInput').focus()")
    wait(0.5)
    cdp("Input.insertText", text="517969837")
    wait(1)
    
    # Verify DOM value is set
    val = js("document.querySelector('#adAccountInput').value")
    print(f"DOM value: {val}")  # Prints: 517969837 ✓
    
    # Click Save
    js("[...document.querySelectorAll('button')].find(b=>b.textContent.includes('Save Ad Account'))?.click()")
    wait(8)
    EOF
  2. Observe: Dialog remains open. No network request fired. No error shown.


What Was Tried (Exhaustive List)

All methods below successfully set input.value in the DOM, but none updated Ember's internal binding state:

# Method DOM Updated Ember State Updated Network Request
1 js("el.value = '517969837'")
2 cdp("Input.insertText", text="517969837")
3 cdp("Input.dispatchKeyEvent") char-by-char
4 type_text("517969837")
5 el.dispatchEvent(new Event('input'))
6 el.dispatchEvent(new Event('change'))
7 Playwright el.fill("517969837")
8 Playwright el.type("517969837", delay=50)
9 Ember.set(model, 'adAccountData', ['517969837']) N/A Partial*
10 CDP DOM.focus + Input.insertText
11 cdp("Input.dispatchMouseEvent") coordinate click on Save
12 el.dispatchEvent(new MouseEvent('click')) on Save
13 Playwright save_btn.click()
14 Playwright save_btn.dispatch_event('click')
15 Enter key via cdp("Input.dispatchKeyEvent", key="Enter")
16 Ember.View.views[id]._actions.save.call(view) N/A N/A N/A**

* Ember.set updated the route controller's model property, but the save button is handled by a child component (not the route/controller).
** No Ember.View.views entries exist for any element on the page — Glimmer components don't use the legacy view registry.


Root Cause Analysis

The LinkedIn developer portal uses Ember.js 3.28.12 with Glimmer components (not legacy Ember.Component). The form dialog is structured as:

route: apps.app.products.mdp-settings
  └─ component: artdeco-modal
       └─ component: inputs/input-textbox  (id="adAccountInput")
       └─ component: artdeco-modal-footer  (Save button)

Glimmer components in Ember 3.x have the following properties that prevent harness automation:

  1. No DOM↔state sync. The inputs/input-textbox component uses Ember's {{input}} helper which binds value via a tracked property (@value). Setting input.value directly does not trigger the tracked setter. The component's internal @value remains undefined.

  2. No legacy view registry. Ember.View.views is empty — Glimmer components don't register there, so there's no way to find component instances via DOM traversal.

  3. No __ember_meta__ on DOM nodes. Unlike older Ember, Glimmer doesn't attach metadata to DOM elements, so there's no way to discover component instances from the DOM.

  4. Button click is gated on form state. The Save button's click handler checks this.value (the component's tracked property, not input.value). Since tracked state was never set, the handler returns early without making any network request. No error is thrown — it's a silent no-op.

  5. No fetch/XHR interception possible. Since the handler returns before making any request, there's no network call to intercept or replay.


Suggested Fixes

Option A: Add a fill_ember_input helper (recommended)

Ember's {{input}} helper listens for the browser's native input event but only if it originates from real keyboard interaction. A helper that:

  1. Focuses the element via DOM.focus CDP
  2. Selects all text (Ctrl+ABackspace)
  3. Types each character with proper keydownkeypressinputkeyup sequence
  4. Uses real KeyboardEvent objects with correct keyCode, which, and charCode

...would likely pass Ember's event validation. The key difference from current type_text is that Ember checks event.isTrusted and/or event.originalEvent in some code paths.

def fill_ember_input(selector, text):
    """Fill an Ember.js bound input field."""
    js(f"document.querySelector('{selector}').focus()")
    wait(0.3)
    # Clear
    cdp("Input.dispatchKeyEvent", type="keyDown", key="a", modifiers=2)
    cdp("Input.dispatchKeyEvent", type="keyUp", key="a", modifiers=2)
    cdp("Input.dispatchKeyEvent", type="keyDown", key="Backspace")
    cdp("Input.dispatchKeyEvent", type="keyUp", key="Backspace")
    wait(0.2)
    # Type each char with full event sequence
    for char in text:
        cdp("Input.dispatchKeyEvent", type="keyDown", key=char, text=char, unmodifiedText=char)
        cdp("Input.dispatchKeyEvent", type="char", text=char, unmodifiedText=char)
        cdp("Input.dispatchKeyEvent", type="keyUp", key=char, text=char, unmodifiedText=char)
        wait(0.05)

Option B: Expose Ember.set as a first-class helper

For Ember apps where Option A doesn't work (some newer Glimmer components don't listen to DOM events at all), a helper that directly calls into Ember's tracked system:

def ember_set_value(input_selector, value):
    """Set a value on an Ember.js bound input through the framework."""
    js(f"""
    (() => {{
        const inp = document.querySelector('{input_selector}');
        if (!inp) return false;
        
        // Find Ember app instance
        let app = null;
        Ember.Namespace.NAMESPACES.forEach(ns => {{
            if (ns.__container__) app = ns;
        }});
        if (!app) return false;
        
        // Walk up to find the component's tracked property owner
        const container = app.__container__;
        const router = container.lookup('router:main');
        const ctrl = container.lookup('controller:' + router.currentRouteName);
        const model = ctrl.get('model');
        
        // Set through Ember's tracked system
        Ember.set(model, 'adAccountData', ['{value}']);
        
        // Also set DOM for visual confirmation
        inp.value = '{value}';
        inp.dispatchEvent(new Event('input', {{bubbles: true}}));
        
        return true;
    }})()
    """)

Option C: Support Chrome DevTools Protocol DOM.setNodeValue with form submission interception

The most robust approach: intercept the network request the form would make, then replay it with correct cookies/CSRF. This requires:

  1. A helper that extracts session cookies and CSRF tokens
  2. A helper that monitors fetch/XHR during form interaction
  3. If no request fires, construct and send the expected request manually

Reproduction URL

The LinkedIn Developer Portal is freely accessible (any LinkedIn account):

No API keys or paid services required to reproduce.


Additional Context

I spent ~2 hours on this with 16 distinct approaches. The owner's claim that browser-harness "can accomplish anything" is aspirational, but Ember.js (and similar frameworks like Angular with NgModel) intentionally decouple state from DOM as a design feature. Without framework-aware helpers, the harness is limited to what cdp("Input.insertText") can achieve — and that's not enough when the framework is the source of truth.

The LinkedIn Developer Portal also serves as a good stress test because it combines:

  • Ember.js 3.x (state ≠ DOM)
  • Artdeco component library (custom modal/form behavior)
  • Anti-automation (CSRF tokens, session validation)
  • Real-world complexity (multi-step OAuth → developer portal → modal form)

If you can make this work, you'll have proven the harness can handle any modern SPA framework.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions