11 KiB
| name | description | version | created | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| bricks-ui-testing | Test bricks UIs: click, console, DOM, login, pitfalls. | 1 | 2026-08-07 |
|
Bricks UI Testing
Test bricks-framework web applications via Hermes browser tools.
Event System
bricks uses standard DOM events. Source: widget.js:351-353:
bind(eventname, handler){
this.dom_element.addEventListener(eventname, handler);
}
No isTrusted check anywhere in bricks source. browser_click (CDP Input.dispatchMouseEvent) triggers addEventListener callbacks normally.
Click Compatibility
| Widget type | browser_click | Notes |
|---|---|---|
| Menu items | ✅ | Triggers regen_menuitem_event() |
| toggle buttons | ✅ | Triggers idset= re-render |
| Text/Icon/HBox | ✅ | Standard DOM events |
| Form Submit button | ❌ | Known bug — see pitfalls |
Console & Logs
Use browser_console (no expression) to grab all console output. bricks emits verbose idset= and regen_menuitem_event() logs.
browser_console(clear=True) # flush buffer
browser_click(ref='@e3') # perform action
browser_console() # read all logs
Snapshot Limitations
bricks widget text often shows as "generic" in browser_snapshot. Use browser_console to read actual DOM:
browser_console(expression="""
document.querySelector('#sidebar_menu').innerText
""")
Pitfalls
侧边栏导航:用 Menu,不要用 Tree
Sage/Bricks 应用侧边栏导航必须用 Menu 控件(widgettype: "Menu"),格式参照 references/sage-menu-pattern.md。不要用 Tree:
- Tree 默认
textField="text",数据常用"label"→ 菜单文字不显示 - Tree 是层级数据展示控件,Menu 才是导航控件
- Menu 格式:
{name, label, icon, url, target},子菜单嵌套"items"(非"children") - target 必须用
"app.xxx"格式。bricks.getWidgetById内部用 DOMel.closest()向上查找 +el.querySelector()向下查找。Menu 和主内容区是兄弟节点,DOM 遍历不到。app.前缀从 bodyquerySelector搜索
Menu target DOM 解析限制
bricks.getWidgetById(target, this) 从 Menu 调用时:
el.closest('#target')— 只向上查祖先,Menu 和主内容区是兄弟,找不到el.querySelector('#target')— 只向下查子节点,也找不到
唯一解法:target 用 "app.目标ID" 格式,getWidgetById 解析 app → bricks.app(body),然后 body.querySelector('#目标ID')
"target": "app.main_content" // ✅ 从 body querySelector
"target": "main_content" // ❌ closest/querySelector 找不到兄弟
Dashboard 统计 API 返回格式
urlwidget 渲染的 DSPY 必须返回 Bricks Widget 格式 {widgettype, options},不能返回裸数据 {status: "ok", data: {...}}:
# ❌ 裸数据 — urlwidget 无法渲染(控制台:widgettype is null)
return {'status': 'ok', 'data': {...}}
# ✅ Widget 格式
return {'widgettype': 'Text', 'options': {
'otext': '统计: 3 个集群', 'cfontsize': 0.9, 'color': '#1e293b'
}}
DSPY 调用模块函数的限制
DSPY 中调用模块级 async 函数(如 await pool_stats(request, params_kw))需要函数已通过 load_xxx() → ServerEnv 注册到 DSPY 上下文。如果 pccs.py 的 init() 未调用 load_pcpool() 等,会导致 NameError → 500。
最可靠方案:DSPY 内联 SQL 查询,不依赖模块函数注册。
Form Submit button does not work via browser_click
bricks Form generates Submit/Reset/Cancel as internal divs. Clicking via CDP opens another window instead of submitting the form.
Workaround: Use browser_console to call the fetch API directly:
browser_console(expression="""
(async function(){
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();
if(t.status==='ok') window.location.href='/target-page';
})()
""")
HttpOnly session cookie
Pipeline/Sage uses AIOHTTP_SESSION cookie with HttpOnly flag. JavaScript document.cookie returns empty. Use Storage.getCookies via CDP to inspect, or curl + SOCKS5 proxy for API-level verification.
Page load timing
Wait 2-3s after browser_navigate before clicking. Bricks pages load external CDN resources asynchronously.
🔴 Cache-busting is MANDATORY after rebuilding dist/bricks.js
Bricks serves the bundled dist/bricks.js at /bricks/bricks.js. The browser caches it aggressively. After you add a widget to build.sh and rerun ./build.sh, the page keeps running the OLD bundle until you force a fresh fetch. Two ways:
- Navigate with a cache-busting query:
browser_navigate('https://host/?t=<timestamp>')— the new URL bypasses the cached HTML→bricks.js chain. - Verify the bundle actually changed on the server first:
curl -s http://host/bricks/bricks.js | grep -c YourNewWidget(and confirmwc -cgrew).
Symptom of stale bundle: your new widget class is undefined (typeof bricks.ResourceBrowser === 'undefined'), or the page silently runs old behavior with zero errors. Never conclude "widget doesn't work" before confirming the served bundle actually contains it.
🔴 Session expiry mid-test → silent redirect to PCCS
The AIOHTTP_SESSION cookie expires during long browser sessions. When it does, the pipeline/Sage app redirects to pccs.opencomputing.cn (the SSO/user-info host) instead of showing a login dialog. Symptom: browser_snapshot suddenly shows PCCS widgets ("Compute Pool", "集群管理"), or document.getElementById('project_id') returns null with a "no cockpit" log. The pipeline app itself looks like it "vanished".
Fix: re-login via curl, re-inject the cookie, and re-navigate:
S=$(curl -s -D- -X POST 'https://host/rbac/user/up_login.dspy' -H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin123' | grep -oP 'AIOHTTP_SESSION=\K[^;]+')
# then browser_cdp Storage.clearCookies + Storage.setCookies (domain, httpOnly, secure, value=$S)
Fresh cookies are cheap — get a new one whenever the page looks wrong.
🔴 Prefer sequential browser_click/browser_snapshot over nested setTimeout chains
Long nested setTimeout(...) async chains inside a single browser_console(expression=...) call break whenever the page refreshes mid-chain (session re-auth, redirect). The scheduled callbacks are lost, and you get stale mixed console output plus dead-end logs ("no cockpit", "no tree") that look like real failures. This frustrated the user — "browser-use有能力去操作各种网站,你该好好学习".
Correct workflow for a Bricks flow (login → menu → popup → assert):
browser_navigate→browser_snapshot(confirm login/menu present)browser_click(ref=...)one elementbrowser_snapshotto observe the result- Repeat click→snapshot one step at a time. Use
browser_console(expression=...)only for a single synchronous DOM probe, not to orchestrate multi-second sequences.
Clicking a Bricks Tree node via DOM (for testing)
Tree node DOM is TWO levels deep — do NOT confuse them (this cost several wasted iterations):
-
node.dom_element(TreeNode VBox,div.vcontainer) children:[0]=node_widgetHBox row,[1]= child-nodes container (non-leaf only). -
node.node_widget.dom_element(the HBox row) children:[0]= expand/collapse triple (StatedSvg),[1]= folder/type icon,[2]= label text. -
EXPAND/COLLAPSE → click the triple:
node.dom_element.children[0].children[0](i.e. the HBox row's first child). Firesstate_changed(open/close) →toggleExpandCollapse→ lazy-load withparams={id:...}. -
SELECT → click the HBox row itself:
node.dom_element.children[0](node.node_widget.dom_element). Firesnode_selected; node id is atnode.selected_node.user_data.id.
From raw DOM (no widget ref handy): locate the label <div> whose textContent equals the node label, then label.parentElement is the HBox row and label.parentElement.children[0] is the triple.
Find the tree widget with bricks.getWidgetById('ws_tree', bricks.app) (NOT document.getElementById('ws_tree').__bricks_widget__, which is undefined — Bricks registers ids via idset= not __bricks_widget__).
Button script(binds)点击测试:async 时序陷阱
bricks 的 Button 通过 binds 绑定 click → script。bricks.buildScriptHandler 把 script 包装成 AsyncFunction,universal_handler 是 async 函数(bricks.js:220-230)。点击按钮后 script 里的 fetch 是异步执行的。
测试陷阱:用 browser_console(expression=...) 检查按钮点击结果时,如果同步返回(不等待),fetch 还没完成,会误判"script 没执行"。本次会话就因此误判"按钮 script 失效",实际只是时序问题。
// ❌ 立即检查 —— fetch 未完成,返回空,误判 script 没跑
btn.click();
return JSON.stringify(window._fetchLog); // "[]"
// ✅ 等待 async 完成
(async () => {
btn.click();
await new Promise(r => setTimeout(r, 500));
return JSON.stringify(window._fetchLog); // ["/api/..."] —— script 正常
})()
验证 bind 是否生效(区分"bind 失败" vs "async 时序"):btn.dispatchEvent(new Event('click')) 直接触发 button.dom_element 上的 addEventListener 监听器(bricks bind() 就是 dom_element.addEventListener,见 widget.js:351)。如果 dispatchEvent 能触发 fetch,说明 bind 成功、问题在时序;如果不能,才是 bind 失败。
bricks.Message / show_message / show_error
bricks.Message 继承 PopupWindow,构造函数里 opts.auto_open = true(message.js:11),即构造时自动打开。所以:
- 用官方推荐
bricks.show_message({title, message})/bricks.show_error({title, message})(message.js:36-48) - 不要
new bricks.Message({title, message}).open()——.open()多余,auto_open 已打开,二次 open 可能 toggle 关闭
Pipeline Platform APIs
Reference: references/pipeline-cockpit-apis.md
Verification Workflow
- Clear stale cookies — old sessions from sibling apps (e.g. pipeline) cause auth confusion even after login:
browser_cdp(method='Storage.clearCookies', params={}) browser_navigate(url)→ wait 2-3sbrowser_console()→ check for 401/auth errors- Login via
browser_console+ fetch API if needed browser_clickmenu items →browser_consoleverify logsbrowser_console(expression=...)for DOM content verification