Summary#
Assert.Visible("Confirm") passes on a button that a person cannot press, because something is drawn on top of it.
Assert.Enabled("Save") passes on a button laid out past the edge of its own card. Assert.Visible("Quarterly revenue report") passes on a chip rendering Quarterly rev…, because the DOM holds the whole string whether the box
shows it or not. Every assertion in Assert is about text or state, and none of them can see any of that.
The assertions below ask about geometry, contrast and emphasis: what covers what, what is inside what, what is wider than what, which press is the filled one, and whether
anything is cut off. They need a renderer that has a font engine and a compositor — so they are checked by
osy test --pixels, which drives a real browser, and under plain osy test they report themselves NOT CHECKED
rather than passing.
Signature#
// is it USABLE?
Assert.Clickable(control); // a click at its own centre reaches IT — nothing covers it, its box is not empty
Assert.Inside(control, container); // its box lies within that container's
Assert.NoOverflow(); // nothing pushes the page wider than the window
// where things ARE
Assert.Above(first, second); // …and Assert.Below
Assert.LeftOf(first, second); // …and Assert.RightOf
// how big, relative to each other
Assert.Wider(first, second); // …and Assert.Narrower
Assert.SameWidth(first, second);
Assert.Taller(first, second); // …and Assert.Shorter — a sidebar that should run the page's height, not stop
Assert.SameHeight(first, second); // where the page's content stops
// is any of it cut off?
Assert.FitsOn(control);
// can its words be read on what is behind them?
Assert.Readable(control); // contrast against the ground actually painted — WCAG's 4.5:1, or 3:1 for large text
// is it the FILLED press — or, with false, a plain one?
Assert.Filled(control, true); // its own fill stands out 3:1 from the ground behind it (WCAG 2.2, 1.4.11)
Assert.Filled(control, false); // plain, outlined or bare text: nothing of its own that stands out
// and a photograph, which asserts nothing
Ui.Shot("the empty board");
// at a PHONE's width — every claim above, judged at the width the test chose (the default is 1280, a desktop)
Ui.Viewport(390); // before Ui.Visit: render at 390px; after it: RESIZE, and the page must re-flowosy test # behaviour. Layout claims report NOT CHECKED — this renderer has no compositor.
osy test --pixels # the same tests in a real browser. The claims are judged; `Ui.Shot` writes PNGs.
osy test --pixels --headed # …and SHOW the browser, slowed down, so you can watch it drive the page.Description#
At a phone's width#
Every UI test renders at 1280px unless it says otherwise. Ui.Viewport(390); as a test's first line runs it at a
phone's width, and every geometry claim — Assert.NoOverflow() above all — is judged there. Called after a
Ui.Visit, it RESIZES the open page, so the same test can prove the page re-flows. The width sticks across visits.
See [[testing-ui#viewport]].
Why these are separate from every other assertion#
A UI test asserts three different kinds of thing, and only two of them were expressible before:
| the question | the assertion | can a text renderer answer it? |
|---|---|---|
| is this text on the page? | Assert.Visible | yes |
| is this control enabled, checked, selected? | Assert.Enabled, Assert.Checked | yes |
| can a person actually use it? | Assert.Clickable, Assert.FitsOn | no — it needs a compositor |
The third row is not a nicety. A control that renders, satisfies every text assertion, and is unclickable is a shipped bug that a full green suite reports as fine.
NOT CHECKED is not passed#
⛔ A renderer that cannot judge a claim never reports it as holding. Plain osy test renders in happy-dom, which
has no font engine and no compositor: every box it measures is 0×0 and nothing is ever on top of anything. So a
layout assertion there answers NOT CHECKED, with the flag that checks it:
│ Assert.Clickable(…) — NOT CHECKED: this run renders in happy-dom, which has no compositor — every
box it reports is 0×0, so nothing here can judge what covers what. Run the same tests with
`osy test --pixels` to check them in a real browser.The test carries on — its behavioural assertions are real and worth having — and the run keeps the count, so a green tally can never read as "the layout was checked".
⚑ This is why one test file runs in both tiers. The alternative — refusing — would make a .test.osy containing
a layout assertion runnable under --pixels and not under osy test, which is a worse product than having no tier
at all. You write one file; you choose the renderer at the command line.
Assert.Clickable — the one to reach for first#
It resolves the control exactly as Ui.Click would, takes the point a click would land on, and asks the page what
is actually there. Three ways it refuses, each with the numbers:
- something is covering it — the refusal names what a press would hit instead;
- its box is empty (0×0) — in the DOM, taking up no space, pressable by nobody;
- it is laid out off the window entirely — where no scrolling reaches it.
A control below the fold is not refused: it is scrolled into view first, as a person would scroll to it, and then
measured. Only what scrolling cannot bring on screen (a box placed at -9999px, say) is off the window.
Assert.Clickable(…) failed: a click at 'Confirm's own centre (68, 219) lands on <span> "half price
today" instead — something is covering it. That is what a person pressing it would hit, so the control
is on screen and unusable.Assert.FitsOn — the claim no text assertion can make#
textContent reads the whole string whether the box shows it or not, so a truncated label is invisible to every
other assertion in the language. This is the only one that can see it:
Assert.FitsOn(…) failed: 'Quarterly revenue report for the northern region' has content 257px WIDER
than its box (box 90×24, content 347×24), so part of it is CUT OFF on screen. It reads "Quarterly
revenue report for the northern region" to the DOM, which is why every text assertion passes on it.Assert.Readable — dark words on a dark ground#
Words in an ink too close to their ground are there, on screen, fit their box and are covered by nothing — so every
other assertion passes on them. This one compares the browser's computed colours (every theme token, mode and
var() fallback resolved): the words' ink against the ground actually painted behind them — the nearest opaque
background up the page, with any translucent ones between laid over it. It holds at WCAG 2's bar, 4.5:1, or 3:1
for large text (24px, or 18.66px bold):
Assert.Readable(…) failed: '“The gamay tastes of cherries.”' <span> is set in #141414 on #41303d (the ground
<div> "…" paints) — a contrast of 1.63:1, where words at 36px need 3:1 to be read. Every text assertion passes
on it — the words ARE there. If the ground is one the caller chose, set the words in the ink the theme pairs
with it: `color: Colors.On(ground, <your ink>)`.⚠ Words over a picture are not measured — a photograph has no one colour to compare them with — and the run says
so (NOT CHECKED) rather than judging a white headline against the colour under the photograph. Look at the
screenshot. The fix for a block on a ground staff choose is [[ui-blocks#paired-ink|Colors.On]].
Assert.Filled — which press is the filled one#
A page gives its one next step a filled shape and everything else a plain one, so the eye lands on what to do next. To every other assertion the two are the same thing: the same words, both enabled, both clickable, nothing on top of either. So "one filled press per screen" could be a rule in a design document and checked by nothing — the filled button that should have gone plain stays filled through every green run.
Assert.Filled answers it from paint, not from a kit's word for it. A tone: or a variant: is a name in one
kit; what a person sees is a shape whose colour stands out from what is around it. So it takes the control's own
fill — its background, or a part of it painting most of its box — fades it by every opacity on the way up (a filled
button greyed out to 40 % is judged as what it looks like), and compares it with the ground painted behind the
control. A contrast of 3:1 is a filled shape: WCAG 2.2's bar (1.4.11) for a shape to be seen against what is next
to it. Less than that reads as part of the ground — a plain, outlined or bare-text press.
Assert.Filled("Open the app", true);
Assert.Filled("Open app: choose how", false);Assert.Filled(…) failed: 'Open app: choose how' <button> paints #00566f on #f4f1ea — a contrast of 7.45:1, so
it reads as a FILLED press — the shape a page gives its one next step. 3:1 is the bar (WCAG 2.2, 1.4.11) at
which a shape stands out from what is behind it.The control is found as Ui.Click finds it, so name it by the words a person presses or by its announceAs:. A fill
that is a picture or a gradient counts as filled. Like every claim on this page it needs a real browser: under plain
osy test it reports NOT CHECKED.
No pixel counts, deliberately#
⛔ There is no Assert.Equal(200, <the card's width>), and that is the design rather than an omission. An
absolute pixel breaks when a font ships, a browser rounds differently, a theme token moves by 2px, or the display
changes scale — and a noisy suite gets deleted, taking the good assertions with it.
Every assertion here is an invariant: it compares two things on the same screen, so it survives a restyle, a font
update and a browser version. Assert.Wider(a, b) still holds after you change every size in the theme;
Assert.Equal(200, …) does not.
Assert.Above is not Assert.Before#
Ui — drive the app's UI from a test's Assert.Before is about document order among the rows of a list, and it holds in either
renderer. These are about where things are. The two disagree exactly when the app reorders visually — a
column-reverse, a CSS order:, grid placement, absolute positioning — which is the case where the page reads one
way and the markup says another, and only these can see it.
What a name refers to#
A locator names the thing that reads the words, not the box around it — the same rule every locator in
Ui — drive the app's UI from a test follows. So Assert.Wider("Sidebar", "Rail") compares the two pieces of TEXT, and a 300px panel with
a short label measures as its label. That is correct and surprising, so the refusal prints the tag it measured and
names the fix: give the container a label: and name that, exactly as within: names one.
Box(label: "Sidebar panel", w: 300) { Text("Sidebar"); }
Assert.Wider("Sidebar panel", "Rail panel"); // the PANELS — not the words inside themUi.Shot — evidence, not an assertion#
Ui.Shot("label") photographs the page. It asserts nothing and can never fail a test: under --pixels it writes a
PNG beside your project, and under plain osy test it records "no image: this run renders in happy-dom, which has
no compositor" and carries on. It never writes something that is not a photograph under a photograph's name.
Reach for it when you want to LOOK at what a test is driving — it is the fastest way to understand a layout failure, and often faster than reading the assertion that caught it.
One part of the screen, where a person scrolled to it. A whole-page photograph is laid out unscrolled, so what
covers what once the reader HAS scrolled — a dock over the last message, a sticky header over a row — is not in it.
within: photographs just the part it names (the same container a within: names for Ui.Click), scrolled into
view in the page and in any scrolling list it sits in. The capture is the screen in that part's box, so whatever is
drawn over it there is in the picture too:
Ui.Shot("the last message under the dock", within: "Last message");
Ui.Within("Basket") { Ui.Shot("the basket"); } // the block form photographs the block's containerA mail shot (Ui.Shot(label, mail)) is a page of its own, so it takes no within:.
osy shots — every screen together, not one loose file at a time#
--pixels leaves one PNG per shot under osy-shots/. That answers "what did this ONE test draw" — a question
about placement, hierarchy or whether the app's design language holds together needs every screen at once, and a
folder of files nobody opens together does not answer it.
osy shots builds that view: one self-contained HTML file (osy-shots/contact-sheet.html) showing every shot at
the same scale, grouped by the app's own declared route, in a STABLE order — sorted by the route string itself,
never by file time or which test happened to run first, so two runs are a real comparison rather than a reshuffle.
$ osy shots
✓ contact sheet written: osy-shots/contact-sheet.html
9 shot(s), 9 with a recorded route/test
! 1 of 10 declared route(s) have NO shot:
/checkout/confirmThat last line is the point as much as the picture: a declared route with zero shots is NAMED, never silently absent — a contact sheet that only showed what exists would hide a missing screen by omission, which is the one failure a person scanning thumbnails would not otherwise notice.
Running the pixel tier#
osy test --pixels runs the same tests in a real browser instead of happy-dom. It is opt-in because it costs a
browser: the tier needs Playwright and a Chromium (or your system Chrome, which it prefers). The check runs before
anything is compiled or booted, so a machine that cannot run it is told in a second, with the command that fixes
it — never a stack trace a minute into a run.
Everything else is identical: the same locators, the same refusals, the same test file.
A tier that cannot run REFUSES — it never becomes a quieter run#
If this machine has no browser, osy test --pixels runs nothing and exits non-zero. It does not fall back to
happy-dom: a run that saw no pixels must never be mistakable for one that did, and the flag exists precisely to be
put in a gate.
✗ `--pixels` cannot run here, so NOTHING WAS RUN — this is a REFUSAL, not a test result: no test was
executed, nothing passed and nothing failed.
the pixel tier drives a REAL browser and playwright is not installed here.
→ to run these same tests one tier down, right now: osy test --filter CheckoutTwo things about that message are the whole design. It says nothing ran, so it can never be read as a failing
test — a refusal and a red are different facts with different next moves. And it hands back your own command with
the flag removed, so you are one paste from a real test result; the layout claims then report NOT CHECKED, as they
always do under plain osy test.
⚠ --pixels is a modifier, so this judges only a run that asked for it. Plain osy test on a machine with no
browser is untouched — it runs, and says per-assertion what it could not check.
A run where the tier did start but no test drove the screen — --pixels with a filter that selected only
non-UI tests — is also non-zero, and says so in its own words. The flag checked nothing, so the run is not evidence
about layout; that is the same rule, one step later.
Add --headed to watch it: the browser is shown and each interaction is slowed down enough to follow, which is
usually the fastest way to understand why a locator refused. It is only meaningful beside --pixels, and says so
rather than being quietly ignored.
Examples#
A page carrying the two shapes these exist for — a button something is drawn over, and a label too long for its box
— and the tests that catch both. Every assertion here passes in a real browser except the two that are meant to
fail, and every one of them reports NOT CHECKED under plain osy test.
using Osysharp.Ui;
[Composable]
component Clipped(string text = "") {
variants {
base { W = 90; Overflow = Overflow.Hidden; WhiteSpace = WhiteSpace.Nowrap; TextOverflow = TextOverflow.Ellipsis; }
}
render { Text(text); }
}
[Route("/checkout")]
[AllowAnonymous]
[Render(CSR)]
component CheckoutPage() {
action Nothing() { }
render {
Stack(gap: 4, p: 6) {
Text("Masthead");
Card("Actions") { Button("Cancel", onPress: Nothing); }
// A banner drawn over the button: on the page, enabled, correctly labelled, and unpressable.
Box(position: Position.Relative) {
Button("Confirm", onPress: Nothing);
Box(position: Position.Absolute, top: 0, left: 0, w: 400, h: 60, z: 50) { Text("half price today"); }
}
Clipped("A label far too long to fit inside the box it was given");
Text("Footer");
}
}
}
[Test]
void the_cancel_button_is_usable() {
Ui.Visit("/checkout");
Assert.Clickable("Cancel"); // nothing covers it, and its box is not empty
Assert.Inside("Cancel", "Actions"); // it has not been laid out past its own card
}
[Test]
void the_page_reads_the_way_it_is_laid_out() {
Ui.Visit("/checkout");
Assert.Above("Masthead", "Footer");
Assert.Below("Footer", "Masthead");
Assert.NoOverflow();
Ui.Shot("the checkout page"); // asserts nothing — a PNG under `--pixels`, a note without it
Ui.Shot("the actions", within: "Actions"); // one part of the screen, scrolled to where a person sees it
}See also#
- Ui — drive the app's UI from a test — the verbs and the text/state assertions these sit beside
- Assert — the general assertion vocabulary
- Running tests locally — running these, and what
--pixelsneeds - layout primitives —
gap,alignandjustify, which decide the geometry these measure - style props — the style props, including the three a truncated line needs