--- name: bricks-ui-testing description: "Test bricks UIs: click, console, DOM, login, pitfalls." version: 1 created: 2026-08-07 tags: [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`: ```js 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. ```python 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: ```python 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` 解析 `app` → `bricks.app`(body),然后 `body.querySelector('#目标ID')` ```json "target": "app.main_content" // ✅ 从 body querySelector "target": "main_content" // ❌ closest/querySelector 找不到兄弟 ``` ### Dashboard 统计 API 返回格式 urlwidget 渲染的 DSPY 必须返回 Bricks Widget 格式 `{widgettype, options}`,不能返回裸数据 `{status: "ok", data: {...}}`: ```python # ❌ 裸数据 — 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: ```python 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: 1. Navigate with a cache-busting query: `browser_navigate('https://host/?t=')` — 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: ```bash 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_navigate` → `browser_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 `
` 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 失效",实际只是时序问题。 ```javascript // ❌ 立即检查 —— 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 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