跳到正文
cccskills
免费在 GitHub 公开

Universal Component Testing (WebView)

How to write and run

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical星标 31(GitHub 31 + cccskills 0)
集成工具Playwright

实测评分 未实测

查看安装方法

包含的文件(1)

  • SKILL.md16.3 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Universal Component Testing (WebView)

#universal_test runs a universal component in a real WebView, renders its fixture with the exact production SSR + hydration pipeline (#html / universal_cbi), and runs arbitrary JavaScript against the hydrated DOM.

It is the native alternative to the Playwright components-e2e suite. Use it when you want to test components in Chemical itself, on the same engine your WebView app ships with. Use components_e2e for headless CI / cross-browser coverage.

  • Syntax: a top-level macro, so JSX is legal (it lives inside a #-macro).
  • Fixture: inline HTML/JSX — rendered once with production SSR, wrapped in <div data-ut="<name>">.
  • Steps: one <script> element containing raw JS (full JS power).
  • Default: one process, one page, one WebView, many tests, sequential.
  • Isolation: isolate gives a test its own page + WebView.

Requires GTK3 + WebKit2GTK and a display. It is an opt-in suite (--universal) and is part of ./scripts/test.sh --all. For headless CI wrap the run in xvfb-run.


Quick start

#universal Counter(props) {
    state count = props.start || 0
    return <div>
        <button data-testid="inc" onClick={() => count += 1}>Increment</button>
        <span data-testid="count">Count: {count}</span>
    </div>
}

#universal_test("counter increments") {
    <Counter start={0} />
    <script>
        expect($('[data-testid=count]').text()).toBe('Count: 0')  // SSR before hydration
        $('[data-testid=inc]').click()
        expect($('[data-testid=count]').text()).toBe('Count: 1')
    </script>
}

public func main(argc : int, argv : **char) : int {
    return universal_test_runner(argc, argv)
}

Module (chemical.mod) must import the macro plugin, the runner and the WebView stack:

application my_component_tests
source "src"
import cstd
import std
import page
import html_cbi
import css_cbi
import js_cbi
import universal_cbi
import universal_test
import webview
import window

Run it:

./scripts/test.sh --tcc --universal                 # all tests
./scripts/test.sh --tcc --universal --test-names "counter increments"
# show the WebView window (default is hidden/off-screen):
./scripts/test.sh --tcc --universal --ut-headed
# or build the module directly:
cmake-build-debug/TCCCompiler path/to/chemical.mod -o /tmp/tests \
    --mode debug_quick --no-cache -frecompile-plugins
/tmp/tests [--test-names a,b] [--ut-headed]

Output is PASS <name> / FAIL <name>: <message> plus a summary; exit code is non-zero if any test failed. The WebView is hidden by default (no window flashes); pass --ut-headed to watch a run.

--test-names splits on commas, so a test whose name contains a comma cannot be selected individually. Keep test names comma-free.


Syntax

#universal_test("name" [, isolate] [, group = "g"]) {
    <fixture JSX/HTML/>          // zero or more root elements
    <script> ...raw JS steps... </script>   // exactly one
}
  • "name" is the test name used in reports and --test-names.
  • isolate runs the test in its own page + WebView (portals, globals, timers, navigation, flaky interference). Also required for native form-control groups: radio inputs are grouped by name across the whole document, so two fixtures on the shared page that both emit <input type=radio name="size"> form one group and uncheck each other (a fixture's hydration clears a sibling fixture's checked). Mark such tests isolate, or give each fixture a unique name.
  • group = "g" is stored on the test metadata (reserved for grouping).
  • The fixture may be omitted (empty container) for pure JS assertions.
  • The steps must be the single <script> element; its content is captured verbatim as JavaScript. It is not HTML-escaped and not Chemical — write plain JS.

Steps are async

Each <script> body is wrapped in async function(){ ... }, so you can await t.sleep(ms) (and return Promises). The context object is the global t (not a parameter) — a test may freely declare its own const t = ... without colliding, but then it shadows t for the rest of that test.

How the fixture is rendered

The macro desugars to ut_render_<name>(page : &mut HtmlPage), which:

  1. appends <div data-ut="<name>">,
  2. runs the fixture through ASTConverter.convertHtmlRoot — the same converter #html uses — so SSR markup, the <span data-chx-i> hydration boundary, the client component functions and the window.$__uni_dispatch(...) calls are byte-for-byte what a production page emits,
  3. appends </div> and window.__ut_register("<name>", <isolate>, async function(){ <steps> });.

The runner builds one HtmlPage, calls every fixture function (SSR markup and the __ut_register calls land in the body bundle), then emits window.__ut_expected = [...] and the JS harness into the head script block, loads the page into one WebView, and the in-page harness runs the registered tests sequentially.


Injected JS API

Everything is scoped to the current test's [data-ut="<name>"] container.

Locators (lazy handles)

HelperDescription
$(css) / byCss(css)first match by CSS selector
byTestId(id)[data-testid="id"]
byRole(role, { name })[role="role"] (optionally matching accessible name)
byText(text)first leaf element whose text equals text
byLabel(label)[aria-label="label"]

Handle methods

.text(), .value(), .attr(name), .exists(), .count(), .isVisible(), .click(), .dblclick(), .type(v) / .fill(v), .press(key), .hover(), .focus(), .blur(), .check(), .uncheck(), .selectOption(v), .scrollIntoView().

Assertions

expect(value).toBe(x); expect(value).toEqual(x); expect(value).toContain(s);
expect(value).toBeTruthy();
expect(handle).toHaveText(s); expect(handle).toContainText(s);
expect(handle).toHaveAttribute(k, v); expect(handle).toHaveCount(n);
expect(handle).toHaveClass(c);
expect(handle).toBeVisible(); expect(handle).toBeHidden();

Utilities

  • t.sleep(ms) — returns a Promise (the harness awaits returned Promises).
  • t.log(msg) — console log.
  • t.skip(reason) — reported as a pass with the SKIP: message.
  • Anything else is plain JS: document, fetch, setTimeout, etc.

Uncaught errors and unhandled rejections are captured per test and fail it with the message. A test that never settles is failed by a 15 s timeout and the suite continues.


Execution model

  • Default (fast): every non-isolated test's fixture is SSR'd into one page; the whole page is hydrated once in one WebView; tests run sequentially against their own containers (no reset needed).
  • isolate: the test gets its own page + WebView, created and destroyed around it. The suite keeps running after it.
  • Filters: --test-names a,b and --test-ids 1,2 select tests before any page is built. Names must match exactly (spaces included).
  • Results are keyed by name; the report follows declaration order.

Architecture (for extending/debugging)

LayerLocation
#universal_test macro (lexer, parse, symres, replacement)lang/libs/html_cbi/src/universal_test/ut.ch
Macro registration + universal_test collector annotationlang/libs/html_cbi/build.lab
Raw <script> lexinglang/libs/html_parser/src/lexer/{HtmlLexer.ch,nextToken.ch}
Verbatim <script> emission in #htmllang/libs/html_cbi/src/converter/language/main.ch
Discovery intrinsic intrinsics::get_universal_tests<UTFunction>()ast/utils/GlobalFunctions.cpp (InterpretGetUniversalTests)
Runner (UTFunction, universal_test_runner, run_universal_tests)lang/libs/universal_test/src/runner.ch
In-page JS harnesslang/libs/universal_test/src/harness.ch
Suitelang/tests/universal_webview/{chemical.mod,src/main.ch,src/fixtures.ch,src/tests_*.ch}
CLI./scripts/test.sh --universal (--ut-headed to show the window)

Discovery flow. ut_parseMacroNode creates the fixture function at parse time and controller.collect(...)s the embedded node with args [name, isolate, group, steps, fixture_fn]. The C++ intrinsic turns that collection into a UTFunction[]. universal_test_runner is a comptime function, so ut_all() is evaluated at the user's call site — after the test module has been parsed/collected (the same pattern as test_runner). That is why the test declarations must be reachable from the module that calls the runner.


Gotchas

  • JSX is only legal inside #-macros. You cannot write <Counter/> in a plain function; #universal_test's body is the macro that enables it.
  • #html cannot embed #js. #universal_test exists as its own macro because the steps need a raw-JS section, which #html does not have.
  • <script> is a raw-text element. Its content is lexed verbatim up to </script> (case-insensitive) — do not nest </script> inside the steps.
  • Locators are scoped to the test's data-ut container, so data-testid collisions across tests are fine. Accessible-name resolution is scoped too: aria-labelledby idrefs resolve inside the locator's own root first, then the data-ut container, then the document. This matters because component libraries reuse a constant default id across instances (e.g. every <Tabs> without an id prop emits id="tabs-tab-0"); a document-wide lookup can return a different instance's element and produce the wrong accessible name (a getByRole(..., {name}) lookup then finds nothing, intermittently).
  • name-based lookups with duplicated ids: if two components on one page emit the same element id, getByRole(role, { name }) is only reliable when the idref lives inside the locator's own root. Prefer data-testid-rooted locators, or give each instance a unique id.
  • UTFunction fields are string_view (like TestFunction). std::string fields in the comptime array break the 2c/TCC translation, and the member order must match the struct declaration (id, name, group, isolate, fixture_fn, steps).
  • Display required. If webview_create fails, the runner prints a hint and exits non-zero. Use xvfb-run -a ./tests on headless machines.
  • A broken page JS bundle is reported, not hung. The harness is emitted into the page head <script> (via HtmlPage.append_head_js_view), separate from the body bundle holding the fixtures' __ut_register calls — a script that fails to parse is skipped whole, so a shared element would take the harness down with it. The harness therefore survives a broken bundle, catches its SyntaxError, and fails the affected tests by name (it iterates the runner-injected window.__ut_expected, not Object.keys(__ut_tests)). A host watchdog (window_set_timer, refilled by every bridge call) additionally stops the loop after 60 s of total bridge silence, so no page behaviour can hang a run — override with UT_TIMEOUT_MS (ms).
  • Debugging the generated page JS: set UT_DUMP_JS=1 to print every script element the page emits (head block, body bundle, body tail) between ===UT_JS_START===/===UT_JS_END=== markers, then exit before opening a WebView. node --check each dump to locate a syntax error.
  • Debugging the generated page HTML: set UT_DUMP_HTML=1 to print the full SSR page between ===UT_HTML_START===/===UT_HTML_END=== markers and exit. Use it to inspect hydration boundary ids, fixture containers, and duplicated ids/names across fixtures (the usual cause of cross-fixture interference).
  • Failure diagnostics. A failed assertion appends context to the message: toBeVisible prints the element (or the ancestor that hides it / element not found), toBeChecked prints tag/name/checked/defaultChecked, an unmatched getByRole(..., {name}) prints the accessible names it did see (candidates=[...]), and any console.error/console.warn emitted during the test is appended as | console: .... Console output is diagnostic only and never fails a test by itself (many tests deliberately exercise error paths).
  • --universal is included in --all (it needs a display; use xvfb-run headless).

When to use this vs. components_e2e

#universal_test (WebView)components_e2e (Playwright)
Enginethe app's real WebView (GTK/WebKit, WebView2)Chromium (Playwright)
LanguageChemical + raw JSTypeScript
CIneeds a display (xvfb-run)headless
Best fortesting components in the shipping engine, local dev, WebView integrationcross-browser, headless CI, broad component matrix

Both exercise the same SSR → hydration → interaction contract; keep the Playwright suite as the CI backend and use #universal_test for engine-native verification.

The ported suite (components-e2e → #universal_test)

lang/tests/universal_webview/ is a port of the Playwright components-e2e suite (142 tests). Layout:

  • src/main.ch — the runner main + a few small components.
  • src/fixtures.ch — all #universal *Fixture components copied verbatim from lang/compiled/components-e2e/app/src/main.ch.
  • src/tests_core.ch — counter/ssr/root-shapes/static-children.
  • src/tests_components.ch — the component suite.
  • src/tests_runtime.ch — batching/unmount/effect-deps/derived lists/keyed reconciliation/error boundaries/memo/SVG/ref/Suspense.
  • src/tests_edges.ch — edge + utility + controlled components.
  • src/tests_reactivity.ch — rapid interactions, hydration, ARIA.

Porting a Playwright test

  1. Reuse the existing fixture component (<XFixture />) if one exists; otherwise copy it from the e2e app's main.ch into fixtures.ch.
  2. Write a #universal_test("...") { <XFixture /> <script>...</script> }.
  3. Translate the Playwright API to the harness:
    • page.getByTestId(x) → byTestId(x); f.getByTestId(x) → byTestId('f').getByTestId(x); locator(css) → find(css) (first) or findAll(css) (collection); .nth(i)/.first()/.last()/.count().
    • getByRole(role, {name}) → byRole(role, { name }) (also matches native tags and aria-label/aria-labelledby/wrapping-label names).
    • expect(...) assertions map 1:1, including .not.*, toBeDisabled, toBeEnabled, toBeChecked, toBeFocused, toHaveValue, toHaveAttribute, toHaveCSS, toHaveJSProperty, toBeGreaterThanOrEqual, …
    • page.keyboard.press(k) → t.press(k) (Tab focus movement is emulated); locator.press(k) → handle .press(k).
    • locator.fill(v)/el.value=…; dispatch input → .setValue(v) / .fill(v).
    • await page.waitForTimeout(ms) → await t.sleep(ms).
    • page.evaluate(() => window.__x) → read window.__x directly.
  4. Portals (Dialog/Sheet/Select/Dropdown/Toast) append to document.body, outside the test's container — mark those tests isolate so their portal content cannot collide with another test's fixture on the shared page.
  5. Components that do not forward data-testid (Card, Badge, Avatar, Typography, Tooltip, Alert, Separator, Stack, Grid, Skeleton, Spinner, Text, Collapsible, Slider, Container, Fab) need structural selectors, text/role queries, or an attribute the component does render ([data-variant], [data-size], [data-muted], role, tag name).
  6. Run and iterate: cmake-build-debug/TCCCompiler lang/tests/universal_webview/chemical.mod -o /tmp/t \ --mode debug_quick --no-cache -frecompile-plugins && /tmp/t --test-names "your test"

Related

  • universal — the SSR/hydration pipeline the fixture uses.
  • testing — the general test infrastructure.
  • components_e2e — the Playwright browser suite.
  • lang/docs/universal-component-testing-design.md — full design + implementation plan.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Comprehensive guide to the Chemical compiler's annotation system (`@name`, `@name(args)`) — how annotations are lexed, parsed, registered in the `AnnotationController`, stored and queried centrally, consumed by the compiler and CBI plugins, and how to add a new one. Load when working on annotations, `AnnotationController`, compiler plugins that define/read annotations, or node attribute flags such as `@extern`, `@test`, `@make`, `@deprecated`, `@no_mangle`, `@inline`, and `@volatile`.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

The C++ AST node framework of the Chemical compiler — the ASTAny/ASTNode/Value/BaseType hierarchy, node/value/type kind enums and range predicates, the visitor dispatch framework, the arena allocator, TypeBuilder canonicalization, member containers, and cross-reference resolution. Load when adding or modifying AST nodes, writing compiler passes/codegen visitors, debugging kind mismatches or arena/lifetime issues, or extending the compiler API bindings.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

The Chemical compiler benchmark & release analytics system — how the static GitHub Pages dashboard works, the JSON data model (manifest/daily/release records), the bash collection scripts (bench-*.sh), the GitHub Actions workflows (benchmark-daily/release), and the critical pitfalls (hidden upload dirs, worktree submodules, gh-pages-only data). Load when working on the dashboard, benchmark collection, or analytics workflows.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

Comprehensive deep-dive into the Chemical build pipeline — how chemical.mod is converted to build.lab, how build scripts are JIT-compiled via TinyCC, how jobs are created and executed, and how ASTProcessor orchestrates the parallel compilation passes.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

Building

免费

All documentation related to building and running Project or Tests

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

Comprehensive guide to the C translation backend (2c) — how Chemical AST is translated to C code, key patterns, gotchas, and optimization strategies.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

查看 chemicallang 的全部技能

报告此技能的问题