项目文件夹

文件
alex e1eabc4584 fix: move shopify-admin skills to agent-workspace/domain-skills/
PR #247 added these under domain-skills/ at the repo root, but the
canonical location for domain skills is agent-workspace/domain-skills/
(referenced in README.md, SKILL.md, and helpers.py).
2026-04-29 23:39:22 -07:00

5.3 KiB

Polaris React inputs require CDP-native keystrokes

Shopify admin uses Polaris (their design system). Until January 2026 it was React-based. Polaris React text inputs and textareas are controlled components that reject the standard "React-friendly" synthetic value setter pattern.

The trap

This pattern looks like it works — the field's value shows the right text:

const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
setter.call(inputEl, "my text");
inputEl.dispatchEvent(new Event('input', { bubbles: true }));

But the Save / Submit button stays disabled. Polaris's onChange handler reads from React's internal state, which the synthetic event chain doesn't fully update.

What works

CDP-native keystrokes via Input.insertText:

from helpers import js, type_text

# 1. Focus the input via JS — this works fine
js("""
(() => {
  const input = Array.from(document.querySelectorAll('input[type="text"], input:not([type])'))
    .find(x => { const r = x.getBoundingClientRect(); return r.width > 100 && r.height > 0; });
  if (input) input.focus();
})()
""", target_id=tid)

# 2. Type via CDP — fires Input.insertText which is the lowest-level
#    text-entry signal. React's controlled-input subscriber catches this.
type_text("My question text")

For textareas, same pattern with document.querySelectorAll('textarea').

Full add-FAQ pattern (Knowledge Base App)

import time
from helpers import iframe_target, js, type_text, page_info, screenshot

def add_faq(question: str, answer: str) -> tuple[bool, str]:
    tid = iframe_target("qa-pairs-app")

    # 1. Make sure the form is rendered
    for _ in range(15):
        ready = js("""
        (() => {
          const i = Array.from(document.querySelectorAll('input[type="text"], input:not([type])'))
            .find(x => { const r = x.getBoundingClientRect(); return r.width > 100; });
          const t = Array.from(document.querySelectorAll('textarea'))
            .find(x => { const r = x.getBoundingClientRect(); return r.width > 100; });
          if (i && t) { i.focus(); return true; }
          return false;
        })()
        """, target_id=tid)
        if ready: break
        time.sleep(0.3)

    # 2. Type question (input has focus from step 1)
    type_text(question)
    time.sleep(0.2)

    # 3. Focus textarea, type answer
    js("""
    (() => {
      const t = Array.from(document.querySelectorAll('textarea'))
        .find(x => { const r = x.getBoundingClientRect(); return r.width > 100; });
      if (t) t.focus();
    })()
    """, target_id=tid)
    time.sleep(0.2)
    type_text(answer)
    time.sleep(0.4)

    # 4. Click Save (now enabled because Polaris saw real keystrokes)
    saved = js("""
    (() => {
      const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Save');
      if (!btn || btn.disabled) return {clicked: false, disabled: btn?.disabled};
      btn.click();
      return {clicked: true};
    })()
    """, target_id=tid)
    if not saved.get("clicked"):
        return False, "save_button_disabled"

    # 5. Poll URL for save success — Shopify redirects to /pairs/<id>
    for _ in range(20):
        time.sleep(0.3)
        url = page_info().get("url", "")
        if "/pairs/" in url and "/new" not in url:
            return True, url.split("/pairs/")[-1]
    return False, "save_timeout"

Why this works

Polaris React components subscribe to native inputType events (e.g., insertText from IME / accessibility tools / paste). The synthetic React-friendly setter fires input events but skips the lower-level inputType signal that Polaris validates against to enable Save buttons.

CDP Input.insertText (which the harness's type_text() calls) emits the full native event chain, including inputType: 'insertText', which React catches via its synthetic event system.

Polaris Web Components (post January 2026)

The polaris-react repo was archived January 6, 2026. New Polaris is web-component-based. For new admin surfaces (Catalog Mapping, parts of Settings), the pattern shifts:

// Web components expose value setter on the element itself
const wc = document.querySelector('s-text-field');
wc.value = 'my text';
wc.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: 'my text' } }));

But until Shopify completes the migration (probably late 2026), always test the React pattern first — most legacy surfaces still use it.

How to know which pattern to use

Screenshot the form first. Then JS-introspect:

// Check if React-based (Polaris-* class names) or web-component-based (s-* tags)
const hasReact = document.querySelector('[class*="Polaris-"]');
const hasWC = document.querySelector('s-text-field, s-button, s-textarea');
return { hasReact: !!hasReact, hasWC: !!hasWC };

If both, lean web component (the surface is mid-migration and the WC will be authoritative).

Avoid

  • Coordinate-based typing via Input.dispatchKeyEvent keypress-by-keypress — slower, more brittle, no real benefit over Input.insertText
  • el.value = 'x' without the setter prototype trick — won't even fill the visible field on Polaris React
  • dispatchEvent(new Event('change', ...)) only — Polaris listens for input, not change, on text fields