17 KiB
| name | description | version | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|
| browser-app-testing | Test and debug Bricks or SPA apps with browser tools. | 1.0.0 |
|
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 instantiationregen_menuitem_event()— menu click with module/url401 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
- Take snapshot, record URL via
browser_console:window.location.href - Click a nav link
- Take new snapshot, check URL again
- Verify: URL changed WITHOUT full page reload = SPA routing works
- Check
browser_console()for route-change logs and errors
CDP Cookie & Session Debugging
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:
- Check
browser_snapshotfor the login form's textbox refs (e.g. @e19, @e20) - Use
browser_typeon those refs to fill fields (this works for Bricks textboxes) - For submit:
browser_click,dispatchEvent, and.click()all fail on Bricks Submit/Reset/Cancel buttons. Use thecurllogin + CDP cookie injection approach (see Pitfall #8) as fallback, but note Pitfall #10 below.
Pitfalls
- browser_click silent failure on Bricks: Always verify with
browser_console()after clicking — if no new logs appear, the click didn't register. Switch todispatchEvent. - Bricks widget text missing in snapshot: The accessibility tree shows "generic" for Bricks custom elements. Always use
browser_consolewith DOM queries for actual text content. - iframe content: Interactive examples often live in sandboxed iframes. Use
browser_console(expression='...', frame_id='...')with the frame_id frombrowser_snapshot.frame_tree. - 401 unauthenticated: Without login session, Bricks apps return 401 and show login popup. Test workflow: first login via
browser_navigate+browser_type+browser_clickon login form, then navigate to target pages. - CDN resources blocked in China: See
references/browser-setup-cn.mdfor Playwright Chromium setup via npmmirror. - 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: usecurlto call the login DSPY directly, capture the Set-Cookie, inject via CDPStorage.setCookies. - HttpOnly cookies invisible to JS:
document.cookiewon't show HttpOnly session cookies. Usebrowser_cdp(method='Storage.getCookies')to see all cookies including HttpOnly ones. - 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: usecurlto POST directly to the login DSPY (/rbac/user/up_login.dspy), capture theSet-Cookie: AIOHTTP_SESSIONheader, then inject viaStorage.setCookies. Then navigate to authenticated pages. - "401 unauthorized, opening login" loop: A flood of this console message means the Bricks app is stuck — a page resource (js/css/menu) lacks
anyRBAC permission. The app redirects to login, but since it's already ON the login page, it loops infinitely. Fix: find the 401 resource viacurland add its path toroleid='any'inrolepermission. After DB change, clear Redis cache:redis-cli KEYS 'rbac*' | xargs redis-cli DEL. - CDP cookie injection unreliable for Bricks PopupWindow login: Injecting
AIOHTTP_SESSIONviaStorage.setCookiesand 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 viabrowser_typeon the snapshot refs and finding a working submit path. - curl ≠ browser for Bricks testing: Bricks widgets have their own event system, PopupWindow contexts, and app lifecycle.
curlcan 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 substitutecurlchecks for browser interaction tests. - fetch API login with
_webbricks_=1: The simplest way to login in-browser for Bricks apps is viabrowser_consolefetch API. Always append?_webbricks_=1to 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 - "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 checksget_userorgid()at the top and the user'sorgidis NULL. This is NOT an RBAC permissions issue — fix by setting a valid orgid on the user record in the DB, then re-login. - All DSPY endpoints return 500 — check PYTHONPATH/module installation: When every DSPY data endpoint returns
500 Internal Server Errorand the log showsstr(request.url)=... invalid path, the business modules (pcpool, pcc, etc.) are likely not installed as Python packages. Sage modules usesetup.json— but the standard build.shpip installloop only checks forsetup.py/setup.cfg/pyproject.toml, silently skipping them. Generatepyproject.tomlfromsetup.jsonfor each module, pip install, then restart. Seereferences/pccs-deployment-pitfalls.mdfor the full recipe. - Bricks Tree widget in PopupWindow: The Bricks Tree widget renders SVG-based nodes. These do NOT appear in
browser_snapshot(accessibility tree shows them asimage/genericwith no text). The tree IS rendering — verify viabrowser_console: look forstate_changedevents with labels like📁 apps,📁 deliverables. Usebrowser_consolewith DOM queries to confirm:document.querySelectorAll('.popup .flexbox')and checktextContent. Don't waste time trying to fix rendering that's working visually but invisible to the snapshot. - PopupWindow DOM structure: A Bricks PopupWindow's
content_w(Layout widget, classflexbox) is insidecontent_box(child 0 of the popup). Theresizeboxelement (child 1) is a separate 30×30px control area — NOT the content container. When debugging "empty popup" issues, checkpw.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 renderer(
rendererType:"canvas"):光标画在 canvas 上,不是 DOM 元素。.xterm-cursor元素为空、xterm-cursor-layer内层 empty 都是正常的,不代表光标没渲染。不要按 DOM 元素找光标。 - Wterm 不在
bricks.Body.children树里:从bricks.Body或bricks.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.clearBrowserCache、Network.setCacheDisabled都返回 -32601method not found。改静态资源(bricks.css/js)后验证新代码,用 cache-buster URL(?t=vi5)重新导航,或运行时注入document.head.appendChild(style)临时验证修复有效性(再让用户 Ctrl+F5 加载真文件)。 - resize 拖拽验证用 CDP
Input.dispatchMouseEvent:browser_click/dispatchEvent不触发 Bricks 的 resize_start_pos。用browser_cdp(method='Input.dispatchMouseEvent', params={type:'mousePressed'/'mouseMoved'/'mouseReleased', x, y}, target_id=<tabId>)模拟完整拖拽,然后读.popup的getBoundingClientRect()对比尺寸变化。 - resizebox 被 Wterm 覆盖的根因诊断:
document.elementFromPoint(右下角坐标)返回xterm-cursor-layer(不是 resizebox)→ resizebox 的 z-index 是 auto 被 xterm DOM 覆盖。修复.resizebox { z-index: 9999 },验证elementFromPoint返回 SVG(resizebox)即可。
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(notSplitter) with"height": "100%"for the layout container - Wrap the
Treein aVBoxwith"width": "30%"(or desired split) - ALWAYS use
entire_url()fordataurland bindurloptions in DSPY files — this is a recurring mistake auto_open: trueis 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.
Related: DSPY Returns Widget JSON
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 permissionsreferences/pyproject-toml-from-setup-json.md— Sage module pyproject.toml generation from setup.jsonreferences/ahserver-i18n-setup.md— ahserver MiniI18N setup: file layout, ProgramPath fix, endpoint, RBACreferences/pccs-testing-notes.md— PCCS testing: auth, deployment pitfalls, i18n, compliance audit checklist