Table of Contents

Class Ui

Namespace
GrindFest
Assembly
GrindFest.dll

Player-like helpers for UI dev tests (OneJS / UI Toolkit).

The point is to behave like a player, so tests survive a UI rewrite:

  • locate elements by the text a player can SEE (not by internal field names),
  • interact by dispatching real UI Toolkit pointer events, so the whole dispatcher + bubbling + OneJS (RegisterCallback) handler + JS logic runs,
  • assert on what is visible on screen, not on internal state (visible means laid out with a real rect - see HasGeometry),

Which level to use: L2 (this class) UI panels - real events through the real dispatcher. L1 (call the JS API directly, e.g. ScriptEngine.Eval) for setup only, or for entry points that cannot be reached from a UI event at all (e.g. ESC: PlayerInput reads the legacy Input Manager, which UI events never feed - so the ESC tests start at handleEscape).

Positions matter: UI Toolkit hit-tests pointer events by position, so an event with a bogus position (0,0) is retargeted and never reaches the element you meant. All the helpers below aim at the target's worldBound centre for that reason.

public static class Ui
Inheritance
object
Ui

Methods

All()

Every element in the tree, depth-first (root first).

Click(VisualElement, bool)

A player click: pointer down, pointer up, and (optionally) an explicit ClickEvent. UI Toolkit is supposed to synthesise ClickEvent from the down/up pair; pass alsoSendClickEvent = true if a target turns out to need it.

Returns false - and dispatches nothing - when the target has no rect yet (see HasGeometry(VisualElement)): a click aimed at a not-yet-laid-out element would land on whatever sits at (0,0) and quietly do something else.

ClickByName(string, bool)

Click the element with this id (JSX id="..."). For text-ambiguous controls.

ClickText(string, bool)

Click the visible element with this text. Returns false when it is not on screen.

ClickTextWithin(string, string, bool)

Click the control inside the row labelled rowText.

CloseAllPanels()

Close every OneJS panel through its own close callback (__grindfest.closeMainMenu and friends). The callback names are derived from PanelIds - "panel-report-bug" becomes closeReportBug - so there is no second list to keep in sync.

Panels are application state, not spawned GameObjects: nothing else cleans them up, so a run that ends while a panel is open (or a test that opens one and forgets it) leaves it on screen for whatever runs next. Silent when there is no app around (editor-only contexts).

Closing is not the same as resetting: see ReloadApp() for the state a closed panel keeps.

DescribeHitStackAt(Vector2, int)

Who would receive a click at this point? UI Toolkit hit-tests pointer events by position, so when a click does nothing the answer is usually "something else is on top". Returns the pickable elements under the point, topmost last (tree order).

DescribeVisible(int)

Comma-joined visible texts - handy for assertion messages.

Drag(VisualElement, float, float, int)

Drag across an element (used by the onejs-comps Slider: it needs down -> move* -> up, and captures the pointer on down, so the same pointerId has to be reused). fromT / toT are 0..1 across its width.

FindAllByText(string)

Elements whose own text equals text (trimmed, case-sensitive).

FindAllByTextContaining(string)

Elements whose own text contains fragment.

FindByName(string)

Element by id from JSX (OneJS maps id -> VisualElement.name). Use when text is ambiguous.

FindByText(string)

The visible element with this exact text - what a player would point at. Prefers visible matches, because every panel stays mounted (display:none), so the same string usually exists several times in the tree.

FindVisibleByText(string)

Visible element with this exact text, or null when it is not on screen.

FindVisibleByTextWithin(VisualElement, string)

Visible element with this exact text inside scope's subtree.

FindWithinRow(string, string)

The element a player means by "the '+ next to Strength'": find the visible label rowText and walk up until an ancestor also contains targetText. That ancestor is the row a player sees.

HasGeometry(VisualElement)

Does this element have a real rect on screen yet?

This is the difference between "the element is in the tree" and "a player can hit it": a freshly shown panel reports a zero-size worldBound until the next layout pass, and since UI Toolkit hit-tests pointer events by POSITION, a click aimed at such an element lands on whatever happens to sit at (0,0) instead - a silent no-op that looks like a broken control.

HitStackTopAt(Vector2)

The element that would really receive a click at this point: the last pickable, visible element containing it in tree order (UI Toolkit paints in tree order, so later is on top).

Use it as a precondition before clicking - a panel renders a frame after it is shown, so an element can already be "visible" while a click still lands on whatever sits above it.

IsFullyOnScreen(VisualElement)

Is this element COMPLETELY inside the panel - nothing hanging over the edge?

The bottom HUD is the reason this exists: with bottom: -px(120) the bar sat 72 px below the panel, so its orbs were half off-screen and the whole bar (and every hover target in it) was outside the pickable area while still "visible" and on-screen by the loose test.

IsOnScreen(VisualElement)

Is this element on screen at all - visible AND overlapping the root's rect?

IsVisible(VisualElement) answers a different question, and the difference is not academic: a DraggablePanel parked at its 9999 fallback is display:flex with a real 400x400 rect, so every "is it open?" check said yes while the panel sat 10 000 px to the right. Settings, Character, Achievements, Mods, Feedback and BotConfig were all in that state and all six passed the checks that existed (2026-09-17). Use this for "did it open where the player can see it".

IsPanelOnScreen(string)

Is this panel (a PanelIds entry) somewhere the player can see it?

IsPanelVisible(string)

Is the panel with this PanelIds entry on screen?

Panels express "hidden" in two different ways and IsVisible(VisualElement) covers both: some set display:none on their own root (caught by walking ancestors), while DraggablePanel panels keep their wrapper displayed and hide the styled child inside it - the wrapper then has no geometry left, which the rect check catches.

IsTextVisible(string)
IsVisible(VisualElement)

Is this element actually on screen - what a player could see and aim a click at? Any hidden ancestor hides it, and an element with no rect does not count either: it exists in the tree, but the layout pass has not run yet, so it is nowhere on screen.

PointerDown(VisualElement, Vector2)
PointerEnter(VisualElement)

Enter this element with the pointer, the way a real hover does.

Enter/leave are generated by the panel's own pointer bookkeeping, which synthetic events cannot reach, so this sends the event straight to the element - exactly the target the dispatcher would have picked. That makes it a real test of the app's handler: the app must be listening for pointerenter (OneJS v3 forwards pointer events and nothing else - there is no mouseenter path in QuickJSUIBridge), and if it listens for the old mouse event instead, nothing happens here.

PointerLeave(VisualElement)

The companion of PointerEnter(VisualElement).

PointerMove(VisualElement, Vector2, Vector2)
PointerUp(VisualElement, Vector2)
ReloadApp()

Re-evaluates app.js, so the UI starts from a state neither a test nor a player left behind: every panel closed, every tab back on its default, every bit of React state gone.

A reload is the only reset that reaches the state a panel keeps while it stays mounted but hidden. App.tsx toggles each panel's visible prop instead of unmounting it (for instant open), so the tab the player picked survives a close: two CharacterScreenUiTests failed with "Timeout waiting for 'Strength' visible" because the Skills tab - picked by hand before the run - was still selected, so StatsTab was never rendered. Closing the panel cannot fix that, and naming the default tab from here would put app internals in the test framework.

The app is quick back: its __grindfest.openCharacterScreen is registered again three frames after JSRunner.ForceReload() (measured 2026-09-16), and the 23 s UiReloadTests takes is the menu opening, not the reload. That is the mount, not the cost - an app that has just mounted keeps initialising behind the scene, and a test running in that window pays for it. Measured by doing this before every test: 888 s of a 1194 s suite outside the test bodies, against 119 s of a 358 s suite without it, about 8,5 s per test. Called only for the tests that read the UI (NeedsFreshUi).

Root()

The live OneJS UI root (first UIDocument's root element), or null.

SetText(VisualElement, string)

Set a text field's value the way the UI does (a ChangeEvent), for TextInput/onValueChanged bindings. Typing key-by-key would be more faithful but is not worth it for most tests.

TextOf(VisualElement)

Text a player can read on this element (empty when it is not a text element).

VisibleTexts()

Everything a player can currently read.