17 KiB
Raw Blame History

name description version tags
browser-app-testing Test and debug Bricks or SPA apps with browser tools. 1.0.0
browser
testing
spa
bricks
debugging
cdp

Browser App Testing

Test and debug web applications through Hermes browser tools. Covers SPA routing verification, framework-specific interaction workarounds, console log capture, and DOM querying when accessibility snapshots are insufficient.

Quick Diagnostic

browser_navigate(url='...')     # Load the app
browser_console()               # Check for JS errors and app logs
browser_snapshot()              # See rendered elements

Interaction Workarounds by Framework

Standard SPAs (Vue, React, etc.)

browser_click works for standard DOM events. Tested with Vue.js docs — full SPA client-side routing confirmed.

Bricks Framework

browser_click does NOT work with Bricks widgets. Bricks uses an internal event system that doesn't respond to CDP-level click events.

Workaround: dispatchEvent via browser_console (verified on Pipeline 产线平台):

// Click a specific Bricks menu item
const el = document.querySelector('#sidebar_menu .vcontainer').children[0];
el.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));

Compatibility matrix:

Action browser_click .click() via console dispatchEvent
Bricks Menu item
Bricks toggle button (native DOM)
Bricks Form submit
Standard SPAs
Console log capture

Log Capture

browser_console() without arguments returns all accumulated console messages and uncaught JS errors:

browser_console()
→ console_messages: [{type, text, source}, ...]
→ js_errors: [{error_message, url, line}, ...]
→ total_messages, total_errors

Bricks log markers:

  • idset= <widget> id= <id> — widget instantiation
  • regen_menuitem_event() — menu click with module/url
  • 401 unauthorized, opening login — auth redirect

To run JS and capture result simultaneously, pass expression:

browser_console(expression="document.querySelector('#sidebar').innerText")

To clear accumulated logs, use clear=true:

browser_console(clear=true)

DOM Querying When Snapshot Is Insufficient

browser_snapshot may show Bricks widgets as "generic" without text. Query the actual DOM:

// Get widget text content
document.querySelector('#sidebar_menu').innerText

// Get current URL (verify SPA routing)
window.location.href

// Check for specific elements
document.querySelectorAll('iframe').length

// Inspect element class/state
document.querySelector('#sidebar_menu').className

SPA Routing Verification

  1. Take snapshot, record URL via browser_console: window.location.href
  2. Click a nav link
  3. Take new snapshot, check URL again
  4. Verify: URL changed WITHOUT full page reload = SPA routing works
  5. Check browser_console() for route-change logs and errors

When apps use HttpOnly cookies (like AIOHTTP_SESSION), document.cookie can't see them. Use CDP's Storage domain instead:

browser_cdp(method='Storage.getCookies', params={})
→ lists ALL cookies across ALL domains — find cross-domain session issues

browser_cdp(method='Storage.clearCookies', params={})
→ clears all cookies (works)

browser_cdp(method='Storage.setCookies', params={'cookies': [{...}]})
→ inject a cookie (e.g. session from curl login)

Note: Network.deleteCookies and Network.clearBrowserCookies return -32601 (not found) in headless Chrome — use Storage.* methods instead.

Bricks iframe Interaction

When a Bricks page is loaded in an iframe (e.g. login overlay), two patterns work:

Pattern A: Direct CDP into iframe context

# From browser_snapshot, find frame_id in frame_tree.children[]
browser_cdp(method='Runtime.evaluate',
            frame_id='542403D3A13B041DE4F59F9635D5307E',
            params={'expression': 'typeof bricks'})
# → 'object' if Bricks loaded, 'undefined' if not

Pattern B: Parent-page DOM query

// Query iframe content from parent page (same-origin only)
const iframe = document.getElementById('login-iframe');
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.querySelectorAll('input');  // find form fields

Bricks PopupWindow form interaction

Bricks form fields in PopupWindows are NOT native <input> elements.

Critical: PopupWindow runs in an isolated Bricks app context. When a PopupWindow opens (e.g. login), bricks.apps queried from the parent page is empty — the PopupWindow's widgets are NOT accessible via bricks.getWidgetById() from the parent page context. The PopupWindow creates its own Bricks app instance.

To interact with PopupWindow form fields, you must execute code inside the PopupWindow's context. Approach:

  1. Check browser_snapshot for the login form's textbox refs (e.g. @e19, @e20)
  2. Use browser_type on those refs to fill fields (this works for Bricks textboxes)
  3. For submit: browser_click, dispatchEvent, and .click() all fail on Bricks Submit/Reset/Cancel buttons. Use the curl login + CDP cookie injection approach (see Pitfall #8) as fallback, but note Pitfall #10 below.

Pitfalls

  1. browser_click silent failure on Bricks: Always verify with browser_console() after clicking — if no new logs appear, the click didn't register. Switch to dispatchEvent.
  2. Bricks widget text missing in snapshot: The accessibility tree shows "generic" for Bricks custom elements. Always use browser_console with DOM queries for actual text content.
  3. iframe content: Interactive examples often live in sandboxed iframes. Use browser_console(expression='...', frame_id='...') with the frame_id from browser_snapshot.frame_tree.
  4. 401 unauthenticated: Without login session, Bricks apps return 401 and show login popup. Test workflow: first login via browser_navigate + browser_type + browser_click on login form, then navigate to target pages.
  5. CDN resources blocked in China: See references/browser-setup-cn.md for Playwright Chromium setup via npmmirror.
  6. Bricks login page JS redirect: Some RBAC login pages (/rbac/user/login.ui) redirect via JS before the login form renders — even with no cookies. The server HTML is clean; the redirect is client-side in bricks.js. Workaround: use curl to call the login DSPY directly, capture the Set-Cookie, inject via CDP Storage.setCookies.
  7. HttpOnly cookies invisible to JS: document.cookie won't show HttpOnly session cookies. Use browser_cdp(method='Storage.getCookies') to see all cookies including HttpOnly ones.
  8. Bricks Form Submit requires Bricks API: browser_click, dispatchEvent, and .click() all fail on Bricks Form Submit/Reset/Cancel buttons. These are NOT native <button> elements and don't respond to DOM events. Workaround: use curl to POST directly to the login DSPY (/rbac/user/up_login.dspy), capture the Set-Cookie: AIOHTTP_SESSION header, then inject via Storage.setCookies. Then navigate to authenticated pages.
  9. "401 unauthorized, opening login" loop: A flood of this console message means the Bricks app is stuck — a page resource (js/css/menu) lacks any RBAC permission. The app redirects to login, but since it's already ON the login page, it loops infinitely. Fix: find the 401 resource via curl and add its path to roleid='any' in rolepermission. After DB change, clear Redis cache: redis-cli KEYS 'rbac*' | xargs redis-cli DEL.
  10. CDP cookie injection unreliable for Bricks PopupWindow login: Injecting AIOHTTP_SESSION via Storage.setCookies and then navigating to the Bricks app may still trigger 401 + login popup. The PopupWindow may create its own isolated Bricks app context with separate session validation. When this happens, don't keep retrying cookie injection — switch to filling the login form via browser_type on the snapshot refs and finding a working submit path.
  11. curl ≠ browser for Bricks testing: Bricks widgets have their own event system, PopupWindow contexts, and app lifecycle. curl can verify HTTP endpoints but CANNOT validate Bricks UI behavior (menu clicks, form submissions, widget rendering, PopupWindow state). When the user asks to test a Bricks app, you MUST use browser tools — do not substitute curl checks for browser interaction tests.
  12. fetch API login with _webbricks_=1: The simplest way to login in-browser for Bricks apps is via browser_console fetch API. Always append ?_webbricks_=1 to the login DSPY URL so the server returns JSON (not full Bricks widget), otherwise the response may be silently swallowed:
    var fd = new FormData();
    fd.append('username','admin');
    fd.append('password','admin123');
    var resp = await fetch('/rbac/user/up_login.dspy?_webbricks_=1',
        {method:'POST', body:fd, credentials:'include'});
    var t = await resp.json();
    // t.status === 'ok' → session cookie set, ready to navigate
    
  13. "Authorization Error" from get_userorgid(): When a Bricks CRUD DSPY returns {"widgettype":"Error","title":"Authorization Error"} but the user is logged in, it's likely the DSPY code checks get_userorgid() at the top and the user's orgid is NULL. This is NOT an RBAC permissions issue — fix by setting a valid orgid on the user record in the DB, then re-login.
  14. All DSPY endpoints return 500 — check PYTHONPATH/module installation: When every DSPY data endpoint returns 500 Internal Server Error and the log shows str(request.url)=... invalid path, the business modules (pcpool, pcc, etc.) are likely not installed as Python packages. Sage modules use setup.json — but the standard build.sh pip install loop only checks for setup.py/setup.cfg/pyproject.toml, silently skipping them. Generate pyproject.toml from setup.json for each module, pip install, then restart. See references/pccs-deployment-pitfalls.md for the full recipe.
  15. Bricks Tree widget in PopupWindow: The Bricks Tree widget renders SVG-based nodes. These do NOT appear in browser_snapshot (accessibility tree shows them as image/generic with no text). The tree IS rendering — verify via browser_console: look for state_changed events with labels like 📁 apps, 📁 deliverables. Use browser_console with DOM queries to confirm: document.querySelectorAll('.popup .flexbox') and check textContent. Don't waste time trying to fix rendering that's working visually but invisible to the snapshot.
  16. PopupWindow DOM structure: A Bricks PopupWindow's content_w (Layout widget, class flexbox) is inside content_box (child 0 of the popup). The resizebox element (child 1) is a separate 30×30px control area — NOT the content container. When debugging "empty popup" issues, check pw.dom_element.children[0] (content_box) for your content, not the resizebox. The flexbox inside content_box holds the actual widget tree.

Wterm/xterm 终端测试vi 编辑)

  • xterm 用 canvas rendererrendererType:"canvas":光标画在 canvas 上,不是 DOM 元素。.xterm-cursor 元素为空、xterm-cursor-layer 内层 empty 都是正常的,不代表光标没渲染。不要按 DOM 元素找光标。
  • Wterm 不在 bricks.Body.children 树里:从 bricks.Bodybricks.app 递归 findW 都找不到 Wterm它在 PopupWindow 的 content_w/Layout 里,不是标准 children 树)。验证终端状态靠 console 日志:ws msg= {type:1, data}WebSocket 数据流)+ VIM 初始化控制序列 key= \u001b[2;2R(光标定位)/\u001b[>0;276;0c(设备属性)/\u001b]10;rgb:...(颜色查询)。
  • headless 清缓存不可用Network.clearBrowserCacheNetwork.setCacheDisabled 都返回 -32601 method not found。改静态资源bricks.css/js后验证新代码用 cache-buster URL?t=vi5)重新导航,或运行时注入 document.head.appendChild(style) 临时验证修复有效性(再让用户 Ctrl+F5 加载真文件)。
  • resize 拖拽验证用 CDP Input.dispatchMouseEventbrowser_click/dispatchEvent 不触发 Bricks 的 resize_start_pos。用 browser_cdp(method='Input.dispatchMouseEvent', params={type:'mousePressed'/'mouseMoved'/'mouseReleased', x, y}, target_id=<tabId>) 模拟完整拖拽,然后读 .popupgetBoundingClientRect() 对比尺寸变化。
  • resizebox 被 Wterm 覆盖的根因诊断document.elementFromPoint(右下角坐标) 返回 xterm-cursor-layer(不是 resizebox→ resizebox 的 z-index 是 auto 被 xterm DOM 覆盖。修复 .resizebox { z-index: 9999 },验证 elementFromPoint 返回 SVGresizebox即可。

Bricks Feature Development: Declarative Patterns, Avoid Custom JS

When adding a Bricks feature (Popups, Trees, forms), prefer declarative actiontype over actiontype: "script". There is no popupwindow action type. The correct pattern is actiontype: "urlwidget" where the DSPY returns a PopupWindow widget definition.

Loading a PopupWindow from DSPY (urlwidget action)

Button config — no custom JavaScript:

{
  "widgettype": "Button",
  "id": "workspace_btn",
  "options": {"label": "工作空间", "css": "small"},
  "binds": [{
    "wid": "self",
    "event": "click",
    "actiontype": "urlwidget",
    "target": "self",
    "options": {
      "url": "/module/api/my_popup.dspy"
    }
  }]
}

The DSPY returns a PopupWindow widget — Bricks detects its type at runtime and creates it as a standalone floating window (not embedded into the target). Example DSPY response structure:

{
    "widgettype": "PopupWindow",
    "options": {"title": "标题", "cwidth": 80, "cheight": 36, "auto_open": true},
    "subwidgets": [{ "widgettype": "HBox", ... }]
}

Workspace / File Browser Pattern (HBox + Tree + VScrollPanel)

For a split-pane file browser (tree on left, files on right):

{
    "widgettype": "PopupWindow",
    "options": {"title": "... - 工作空间", "cwidth": 80, "cheight": 36, "auto_open": true},
    "subwidgets": [{
        "widgettype": "HBox",
        "options": {"height": "100%"},
        "subwidgets": [
            {
                "widgettype": "VBox",
                "options": {"width": "30%"},
                "subwidgets": [{
                    "widgettype": "Tree",
                    "id": "ws_tree",
                    "options": {
                        "dataurl": entire_url("/module/api/workspace_tree.dspy"),
                        "textField": "label", "idField": "id", "cfontsize": 1.0,
                        "css": "filler", "padding": "4px", "cheight": "100%"
                        },
                        "binds": [{
                        "wid": "self", "event": "selected",
                        "actiontype": "script", "target": "self",
                        "script": "var tree=bricks.getWidgetById('ws_tree',bricks.app);var nid=tree.selected_node.user_data.id;var fp=bricks.getWidgetById('ws_files',bricks.app);if(fp)fetch(entire_url('/module/api/workspace_files.dspy')+'?id='+encodeURIComponent(nid)).then(function(r){return r.json()}).then(function(d){fp.dom_element.innerHTML='';bricks.widgetBuild(d,fp);});"
                        }]
                }]
            },
            {
                "widgettype": "VScrollPanel",
                "id": "ws_files",
                "options": {"css": "filler", "padding": "8px", "gap": "4px"}
            }
        ]
    }]
}

Key rules:

  • Use HBox (not Splitter) with "height": "100%" for the layout container
  • Wrap the Tree in a VBox with "width": "30%" (or desired split)
  • ALWAYS use entire_url() for dataurl and bind url options in DSPY files — this is a recurring mistake
  • auto_open: true is required for PopupWindow to display

DSPY Returns Widget JSON

A DSPY that returns a widget is return json.dumps(widget_dict, ensure_ascii=False). The widget dict can be any valid Bricks widget tree — the framework handles construction, DOM attachment, and lifecycle.

A DSPY that returns a widget is just return json.dumps(widget_dict, ensure_ascii=False). The widget dict can be any valid Bricks widget tree — the framework handles construction, DOM attachment, and lifecycle.

Bricks Tree Rendering and Snapshot Visibility

The Bricks Tree widget renders SVG-based nodes that do NOT show text in browser_snapshot (accessibility tree shows them as image/generic). To verify Tree content:

// Check console for state_changed events (proof Tree loaded data)
browser_console()
// Look for: state_changed ... label: "📁 apps", "📁 deliverables"

// Query DOM directly
browser_console(expression="document.querySelector('.flexbox').textContent")

Do NOT waste time "fixing" Tree rendering that is working visually but invisible to the text snapshot.

References

  • references/browser-setup-cn.md — Browser tool setup in China (npm, agent-browser, Playwright Chromium via npmmirror, system deps, CDP launch)
  • references/pccs-deployment-pitfalls.md — PCCS deployment: setup.json→pyproject.toml bridge, nginx Host header, admin orgid, RBAC permissions
  • references/pyproject-toml-from-setup-json.md — Sage module pyproject.toml generation from setup.json
  • references/ahserver-i18n-setup.md — ahserver MiniI18N setup: file layout, ProgramPath fix, endpoint, RBAC
  • references/pccs-testing-notes.md — PCCS testing: auth, deployment pitfalls, i18n, compliance audit checklist