Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

probe — what a control says about itself

control X { probe { bool dirty; int matches; } }

A `probe { }` block declares the facts a control publishes about its OWN internal state, so an app's tests can ask for them. It is the one direction across the contract that is not for the app: props go in, events come out, commands go in, and none of them answers "is the document dirty".

stable2 examples compiled by CIuicontrolsauthoringtesting

Summary#

A control's contract points three ways: props in, events out, commands in. All three are for the app — they are how it drives the control and hears back from it. None of them answers the question a test asks: is the document dirty right now? how many matches did the search find? what is selected?

Those facts reach the screen, if at all, as pixels — a dot beside a filename, a tinted range inside a contenteditable, a count the app happened to choose to render. A test could only assert them by reading rendered text, which measures the control's styling rather than its behaviour and breaks the day somebody edits a label. For a control that paints into a canvas, or one running under a headless DOM that lays nothing out, there is no text to read at all.

A probe { } block is the control author's answer: a small, typed surface of facts the control publishes about itself, asserted with Assert.Probe.

This is how you test a control you did not write. The alternative is reverse-engineering somebody else's DOM and coupling every assertion to internals their next release is free to change.

The block is optional, and a control that declares none is unaffected.

Signature#

control <Name> {
  probe {
    <ScalarType> <fieldName>;
    …
  }
}
Assert.Probe("<ControlName>", "<fieldName>", <expected>);

Description#

Declaring what a test may read — the probe block#

control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props { string title; }
  events { dirty(bool isDirty); }
  probe {
    /// Unsaved edits are pending.
    bool dirty;
    /// How many find-hits the document currently holds.
    int matches;
    /// What is highlighted right now, or nothing when the selection is empty.
    string? selection;
  }
}

A field's type is a comparable scalarbool, int, long, double, decimal, string, and their nullable forms. An assertion compares one value against one expected value, so a shape or an array has nothing it could be compared to; a control that wants to publish a structure publishes the parts of it a test can name, one field each.

A default is refused. A prop has a resting value because the app may decline to set one. A probe field is what is true at the instant it is read, so a default would be a value the control reports while stating nothing — and every assertion against it would pass, including against a shim that never answers at all.

A probe is not an event#

dirty above is both an event and a probe field, and that is not a duplication.

An event is "this just changed". A probe is "this is true now". A test that had to listen for a notification in order to learn a resting fact could not ask the question at all until the control happened to change its mind — so a page that opens with unsaved work, or a search already showing results, would be unassertable.

Publish as an event what the app must react to; publish as a probe what somebody may need to ask.

The shim must implement it#

The block generates a probe() method on what mount RETURNS, typed from the declaration:

export interface MarkdownEditorHandle extends ControlHandle<MarkdownEditorProps> {
  probe(): {
    dirty: boolean;
    matches: number;
    selection?: string | null;
  };
}
export function mount(el, props, host) {
  return {
    update(next) { … },
    destroy() { … },
    probe() {
      return {
        dirty,
        matches: findState.hits.length,
        selection: selected.length > 0 ? selected : null,
      };
    },
  };
}

Three properties are part of the contract:

  • It is a METHOD, never a property. A probe is what is true at the instant it is read. A property would be read once at mount and then handed out as a snapshot that stopped being true minutes ago.
  • It must not change anything. Read from live state; do not dispatch, do not move a selection, do not save. A probe that perturbed what it was measuring would prove nothing about it.
  • It must be cheap. It is called on demand, once per assertion.

The platform checks it when it takes the handle — at the mount that produced it, exactly as it checks update, destroy and the command bag. A control that declares a probe { } block and returns no probe() fails there, naming the fields it published. The alternative is that the shortfall is found by whoever first asks, which is a test, whose refusal then reads as "this control reports nothing" — a sentence about the control's behaviour, for a defect in its handle.

Asserting on a probe in a UI test#

Ui.Visit("/doc");
Ui.Fill("Body", "some new text");

Assert.Probe("MarkdownEditor", "dirty", true);
Assert.Probe("MarkdownEditor", "matches", 3);
Assert.Probe("MarkdownEditor", "selection", null);

Three operands — the control, the field, the expected value — the same shape as Assert.Cell(row, column, expected).

The control is named by its declared name, which is what the app wrote at the call site. A mount element carries no accessible name of its own, so when a page renders the same control twice they are told apart by within:.

null means the control reports nothing for this field, which is a different statement from the empty string: a caret sitting in a document selects nothing at all, and an assertion for "" must not pass on it.

Each way it can fail says something different, because each has a different fix:

What is wrongWhat you are told
No such control is mountedWhich controls are mounted here
The control implements no probe()That its shim owes one — not that the field is misspelled
It does not report that fieldThe fields it does report
It reports a different valueWhat it actually said

Two instances of one control#

A page may legitimately render the same control twice. within: narrows by which container each one mounted into — the same argument every other UI assertion takes:

Assert.Probe("MarkdownEditor", "dirty", true,  within: "Draft");
Assert.Probe("MarkdownEditor", "dirty", false, within: "Published");

Without a scope, two mounted instances are an ambiguity and are refused rather than resolved by picking the first — which would answer a question nobody asked, and differently depending on render order.

Which facts belong in a probe?#

Publish what a test needs and a screen does not carry. A probe field is part of the contract like everything else in the block, so it can be relied on and versioned — and it is equally a decision to leave something out.

Prefer a prop for something the app sets, an event for something it must react to, a command for something it asks for, and a probe field for something it (or its tests) may need to know.

Do not mirror the props back: the app already has those values, and reporting them says nothing about whether the control did anything with them. dirty is worth publishing precisely because nothing outside the control knows it.

Examples#

A chart that reports what it actually drew — the classic case, because a canvas has no DOM to read:

control LineChart {
  contractVersion "1.1"
  participation opaque
  props { string title; }
  probe {
    /// How many series were plotted after filtering.
    int seriesDrawn;
    /// The point the pointer is currently over, or nothing.
    string? hoveredPoint;
    /// The chart finished its entry animation and is at rest.
    bool settled;
  }
}

Nothing here is visible to a DOM query: the chart is one <canvas>. Without a probe, the only assertable fact about it is that the element exists — which is true of a chart that drew nothing.

See also#

Related

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…

commands — the verbs a control accepts

A `commands { }` block declares the verbs a control accepts — the mirror of its events. An event is the control telling…

Ui — drive the app's UI from a test

Drive the real UI from a test: navigate to a route, click what a person would click, and assert on what the screen…