一键导入
qml-testing
Headless QML integration testing using the bridge's DOM introspection, JS evaluation, and screenshot APIs. Use when writing or debugging QML UI tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Headless QML integration testing using the bridge's DOM introspection, JS evaluation, and screenshot APIs. Use when writing or debugging QML UI tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Write system prompts, tool docs, and agent definitions. Combines research-backed prompt engineering (+15-30% measured improvements) with project XML conventions. Covers tag hierarchy, structural templates, high-impact interventions, anti-patterns.
Write and compile excellent Typst documents — brand-aware, component-driven, data-hydrated
Interactive wizard to bootstrap a .spell/ directory with spell-server configuration, Telegram bridge, autonomy goals, state stores, and optional memory system. Run in any project directory.
Spawn a native QML canvas window to display structured data (tables, diffs, trees, markdown, images) and collect user input via prompts. Use when presenting comparisons, tabular data, large diffs, or multi-step decisions.
Voice-to-agent flow: start recording, see live transcript, speak the wake word "spell" followed by a command, and the agent processes it automatically.
Generate brand asset variations (ad creatives, logos, mood boards) using gemini-image, display them in a native Qt QML gallery window, and let users rate/compare/regenerate without touching the terminal.
| name | qml-testing |
| description | Headless QML integration testing using the bridge's DOM introspection, JS evaluation, and screenshot APIs. Use when writing or debugging QML UI tests. |
import { isBridgeAvailable, QmlTestHarness } from "@oh-my-pi/pi-qml";
import * as path from "node:path";
const HARNESS_QML = path.resolve(import.meta.dir, "../../src/modes/qml/panels/ChatPanelTestHarness.qml");
describe.skipIf(!isBridgeAvailable())("MyComponent QML", () => {
const harness = new QmlTestHarness();
beforeAll(async () => { await harness.setup(HARNESS_QML); });
afterAll(async () => { await harness.teardown(); });
beforeEach(async () => { await harness.reset(); });
it("test", async () => {
await harness.sendMessage({ type: "user_message", text: "hello" });
await Bun.sleep(200); // settle QML rendering
const texts = await harness.findVisibleText();
expect(texts).toContain("hello");
});
});
The harness QML must be co-located with the component under test so relative import paths ("../SpellUI") resolve correctly.
findItems(selector?, options?) → QueryItem[]Walks the visual tree from the QML root and returns all matching items.
const items = await harness.findItems(
{ type: "QQuickText", textContains: "hello", visible: true },
{ properties: ["text", "color"], includeGeometry: true, maxDepth: 15 },
);
Selector fields (all optional — empty selector matches everything):
type: className prefix match. "QQuickText" matches QQuickText and QQuickText_QMLTYPE_42.objectName: exact match on QML id (which sets the objectName). Set id: myItem in QML.visible: true / false — filters by isVisible().textContains: substring match on the text property.Options:
properties: list of property names to read. Dotted paths work: "font.pixelSize", "anchors.topMargin".includeGeometry: include geometry (local x/y/width/height) and scenePosition (absolute x/y) fields.maxDepth: max tree depth (default 20). Reduce for performance when you know the depth.QueryItem shape:
{
className: string; // e.g. "QQuickText"
objectName: string; // QML id value, or ""
visible: boolean;
opacity: number;
enabled: boolean;
clip: boolean;
childCount: number;
path: string; // e.g. "ApplicationWindow/Rectangle/Text"
geometry?: { x: number; y: number; width: number; height: number };
scenePosition?: { x: number; y: number };
properties: Record<string, unknown>;
}
findVisibleText() → string[]Shorthand: all visible QQuickText elements, returning their text property values.
const texts = await harness.findVisibleText();
expect(texts).toContain("Expected label");
assertVisible(selector) → QueryItemAsserts an element exists, is visible, and has positive dimensions. Throws with a descriptive message on failure.
const item = await harness.assertVisible({ type: "QQuickText", textContains: "Submit" });
assertNotFound(selector)Asserts no element matches the selector.
await harness.assertNotFound({ objectName: "errorBanner" });
evaluate<T>(expression) → TEvaluates a JS expression in the QML engine context. root refers to the root QML object.
const count = await harness.evaluate<number>("root.messagesModel.count");
const isStreaming = await harness.evaluate<boolean>("root.chatPanel.isStreaming");
await harness.evaluate("root.chatPanel.messagesModel.clear()");
Throws if the expression produces a QJSValue error.
const texts = await harness.findVisibleText();
expect(texts).toContain("Expected text");
const items = await harness.findItems(
{ type: "QQuickRectangle", visible: true },
{ includeGeometry: true },
);
expect(items[0].geometry!.height).toBeGreaterThan(0);
const items = await harness.findItems(
{ type: "QQuickText", visible: true },
{ includeGeometry: true, properties: ["text"] },
);
const a = items.find(i => i.properties["text"] === "first");
const b = items.find(i => i.properties["text"] === "second");
expect(b!.scenePosition!.y).toBeGreaterThan(a!.scenePosition!.y);
Use evaluate for internal QML model state, not findItems. The visual tree doesn't expose model data directly.
const count = await harness.evaluate<number>("root.messagesModel.count");
The harness.query(queryName) method sends {type:"query", query:queryName} to the QML window and awaits a query_response event. This requires the test harness QML to implement the query handler. Use evaluate instead where possible — it requires no QML-side boilerplate.
QML rendering is asynchronous. After sending messages, wait 200ms before issuing DOM queries:
await harness.sendMessage({ ... });
await Bun.sleep(200);
const texts = await harness.findVisibleText();
For complex animations or heavy delegates, increase to 500ms.
Test harness QML files must:
ApplicationWindow as root (required for QQuickWindow content item access)reset messages and confirm with bridge.send({ type: "reset_done" })Connections { target: bridge; function onMessageReceived(p) { ... } }Prefer DOM queries for all correctness tests. Screenshots are evidence, not assertions.
messagesModel.count tells you the model is correct, not that anything renders. Use findVisibleText() to verify rendering.path strings from QueryItem. Paths include QML-generated suffixes that change. Use selector-based matching.sendMessage returns immediately; the visual tree updates on the next Qt event loop cycle. Always await Bun.sleep(200) before visual queries.objectName requirements on production QML: The selector works without objectName via type + property matching. Don't add id properties to production QML just for tests.For integration tests that simulate full user journeys through the shell, use QmlJourney from packages/coding-agent/test/helpers/qml-journey.ts. It wraps QmlTestHarness with semantic methods that read like user story scripts.
import { QmlJourney, isBridgeAvailable } from "../helpers/qml-journey";
describe.skipIf(!isBridgeAvailable())("My Journey", () => {
let journey: QmlJourney;
beforeAll(async () => {
journey = await QmlJourney.launch("shell.qml", {
props: {
panels: [
{ id: "chat", title: "Chat", icon: "\u25cf", path: "panels/ChatPanel.qml" },
{ id: "dashboard", title: "Dashboard", icon: "\u25a0", path: "panels/DashboardPanel.qml" },
],
},
});
await journey.settle(100);
});
afterAll(async () => { await journey.teardown(); });
});
Paths passed to QmlJourney.launch() are resolved relative to packages/coding-agent/src/modes/qml/.
| Category | Method | Description |
|---|---|---|
| Lifecycle | QmlJourney.launch(qmlFile, options?) | Static factory. Resolves path, calls harness.setup(). Returns ready journey. |
| Lifecycle | teardown() | Calls harness.teardown(). |
| Agent sim | agentSends(payload) | Sends message to QML via bridge (add_panel, dashboard_update, etc.). |
| User sim | click(selector) | Selector-based click at element center. |
| User sim | clickAt(x, y) | Coordinate-based click. |
| User sim | clickId(id) | Click element from last observe() by ID. |
| User sim | type(text, selector?) | Type text. Optional selector clicks first to focus. |
| User sim | press(key) | Press named key (Return, Escape, Tab, etc.). |
| User sim | scroll(selector, deltaY) | Scroll at element center. Positive deltaY = scroll up. |
| User sim | observe() | Returns numbered interactive elements (MouseArea, TextEdit, TextInput, Flickable). |
| Assert | expectVisible(selector) | Assert element exists, is visible, has positive dimensions. |
| Assert | expectNotFound(selector) | Assert no element matches. |
| Assert | expectText(text) | Assert text appears in visible Text elements. |
| Assert | expectTextAbsent(text) | Assert text does NOT appear. |
| Utility | settle(delayMs?) | Wait for QML event loop quiescence (eval round-trip + sleep). Default 50ms. |
| Utility | screenshot(name) | Capture PNG to test/integration/screenshots/{name}. |
| Utility | findItems(selector, options?) | Direct harness.findItems() for custom assertions. |
| Utility | evaluate(expression) | Direct harness.evaluate() for QML JS evaluation. |
| Utility | waitForEvent(predicate, timeout?) | Wait for raw bridge event. |
it("click submit button", async () => {
const elements = await journey.observe();
// elements: [{ id: 1, className: "QQuickMouseArea", text: "Submit", ... }]
await journey.clickId(1);
await journey.settle();
await journey.expectText("Submitted");
});
it("dashboard shows orchestrator", async () => {
await journey.agentSends({
type: "dashboard_update",
orchestrators: [{ windowId: "w1", scope: "Review auth" }],
// ... other fields
});
await journey.settle(100);
await journey.expectText("Review auth");
});
packages/coding-agent/test/integration/QmlJourney.launch("shell.qml", { props: { panels: [...] } }) to set updescribe.skipIf(!isBridgeAvailable()) to skip when bridge is not builtagentSends() to simulate agent actions, assertions to verify QML statejourney.settle() after sends before assertionsScreenshots are saved to packages/coding-agent/test/integration/screenshots/. Use descriptive names:
canvas-orchestrator-flow.pngqueue-backpressure-high.pngTests require the bridge binary. Build it first:
cd packages/qml && bun run build:bridge
Tests use describe.skipIf(!isBridgeAvailable()) so they skip gracefully if the bridge is not built.