11 KiB
Raw Blame History

name description version created tags
bricks-ui-testing Test bricks UIs: click, console, DOM, login, pitfalls. 1 2026-08-07
bricks
browser
testing
ui
pipeline
sage

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 内部用 DOM el.closest() 向上查找 + el.querySelector() 向下查找。Menu 和主内容区是兄弟节点DOM 遍历不到。app. 前缀从 body querySelector 搜索

Menu target DOM 解析限制

bricks.getWidgetById(target, this) 从 Menu 调用时:

  1. el.closest('#target') — 只向上查祖先Menu 和主内容区是兄弟,找不到
  2. el.querySelector('#target') — 只向下查子节点,也找不到

唯一解法target 用 "app.目标ID" 格式,getWidgetById 解析 appbricks.appbody然后 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';
    })()
""")

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:

  1. Navigate with a cache-busting query: browser_navigate('https://host/?t=<timestamp>') — the new URL bypasses the cached HTML→bricks.js chain.
  2. Verify the bundle actually changed on the server first: curl -s http://host/bricks/bricks.js | grep -c YourNewWidget (and confirm wc -c grew).

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):

  1. browser_navigatebrowser_snapshot (confirm login/menu present)
  2. browser_click(ref=...) one element
  3. browser_snapshot to observe the result
  4. 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_widget HBox 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). Fires state_changed (open/close) → toggleExpandCollapse → lazy-load with params={id:...}.

  • SELECT → click the HBox row itself: node.dom_element.children[0] (node.node_widget.dom_element). Fires node_selected; node id is at node.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 scriptbinds点击测试async 时序陷阱

bricks 的 Button 通过 binds 绑定 click → script。bricks.buildScriptHandler 把 script 包装成 AsyncFunctionuniversal_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 = truemessage.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

  1. Clear stale cookies — old sessions from sibling apps (e.g. pipeline) cause auth confusion even after login:
    browser_cdp(method='Storage.clearCookies', params={})
    
  2. browser_navigate(url) → wait 2-3s
  3. browser_console() → check for 401/auth errors
  4. Login via browser_console + fetch API if needed
  5. browser_click menu items → browser_console verify logs
  6. browser_console(expression=...) for DOM content verification