# probe — what a control says about itself

> 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".

<!-- id: ui-control-probe · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-probe/ -->

## Summary        {#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`](#asserting).

**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      {#signature}
```osy title="what the control author declares" syntax
control <Name> {
  probe {
    <ScalarType> <fieldName>;
    …
  }
}
```

```osy title="what a test then asserts" syntax
Assert.Probe("<ControlName>", "<fieldName>", <expected>);
```

## Description    {#description}

### Declaring what a test may read — the `probe` block   {#declaring}

```osy title="an editor that publishes what a test needs" test app=ui-control-probe
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 scalar** — `bool`, `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    {#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    {#implementing}

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

```ts
export interface MarkdownEditorHandle extends ControlHandle<MarkdownEditorProps> {
  probe(): {
    dirty: boolean;
    matches: number;
    selection?: string | null;
  };
}
```

```ts syntax
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 — and a fourth for a control that finishes starting **after** `mount`
returns: hand back `ready`, a promise that rejects when the start failed, so the probe reports *never started* rather
than a value from a control that is not running (see [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/)).


- **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](#see-also). 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   {#asserting}

```osy title="asserting a control's own state" syntax
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:`](#two-instances).

`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 wrong | What you are told |
|---|---|
| No such control is mounted | Which 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 field | The fields it does report |
| It reports a different value | What it actually said |

### Two instances of one control    {#two-instances}

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:

```osy syntax
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?   {#what-to-publish}

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       {#examples}

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

```osy title="a control whose output is pixels" test app=ui-control-probe-chart
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       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block these are declared in
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the verbs a control accepts, the mirror of its events
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — the UI test surface `Assert.Probe` belongs to
