Summary#
A theme used to be the one part of an app you could write in Osy# and never read back. Theme.Toggle() and
Theme.Set(mode) are the whole runtime surface and both are writes; a token identifier is compile-time style
material that lowers to var(--path) and never becomes a value an expression can hold. So the answer to "is my
accent colour actually #00566f?" was: look at it.
Assert.Token("Colors.FillAccent", "#00566f") answers it, and mode: "dark" answers the half nobody looks
at. Assert.Font("Inter") answers a different question that a token cannot: a Font { Sans = "Inter, …" } token
says what to ask for; whether the platform actually serves a face for Inter is a fact about the files your
app pins.
Both ask the server what your stylesheet resolves to — the same read /_osy/app.css is built from — so they hold
under plain osy test. No browser, no driver, no --pixels.
Signature#
Assert.Token(<token path>, <expected value>); // what the app resolves by default
Assert.Token(<token path>, <expected value>, mode: "dark"); // …and what it resolves in that mode
Assert.Font(<family>); // the app serves a face for this family
Assert.Contrast(<ink path>, <ground path>); // the two colours contrast at 4.5:1 (WCAG AA)
Assert.Contrast(<ink path>, <ground path>, 3, mode: "dark"); // …at another bar, in a modeA token path is spelled the way your theme spells it — Colors.FillAccent, Radius.Card. The CSS custom property
(--colors-fillaccent) is accepted too, because that is what browser devtools shows you.
Description#
What "resolved" means, and why it is not the same as what you wrote#
A theme's values pass through three things before a browser paints them, and an assertion is only useful if it sees all three:
| in the theme | on the page | Assert.Token reports |
|---|---|---|
Border = "#c1d4d4"; | --colors-border: #c1d4d4 | #c1d4d4 |
Bg = Colors.Surface0; | --colors-bg: var(--colors-surface0) | the colour at the end of the chain |
Surface0 = Modes.Of(light: …, dark: …); | :root + a dark override block | the one for the mode you asked about |
The second row is the one that matters. A purpose-layer theme is mostly references — that is what lets a whole app be
re-skinned by changing values and editing no component — so an assertion that stopped at var(--colors-surface0)
would be asking you to assert the platform's lowering rather than your own colour. The chain is followed, per
mode at every hop, and a failure prints the route it took.
Which mode am I asserting?#
Omit mode: and you are asserting what the app paints before anybody touches a toggle — the Light values, or the
mode your theme pins with Mode = Dark;. Pass mode: "dark" and you are asserting the dark override.
⚑ A token with no per-mode value still has a value in dark. A mode block carries only the tokens that differ, so everything else keeps its base value there — exactly as the cascade behaves. An assertion that answered "not declared in dark" would be describing your stylesheet's shape rather than the colour on the screen.
⛔ The mode is a closed set (Light / Dark), checked when you compile. mode: "drak" is a compile error naming
both, not a run that quietly reads the base value and passes.
theme Console {
Colors {
Surface0 = Modes.Of(light: "#eef3f3", dark: "#08161b");
FillAccent = Modes.Of(light: "#00566f", dark: "#5fc8e8");
Border = "#c1d4d4";
Bg = Colors.Surface0; // a reference — the purpose layer
}
Radius { Card = "12px"; }
}
[Test]
void The_theme_resolves_what_it_says() {
Assert.Token("Radius.Card", "12px"); // a flat literal
Assert.Token("Colors.FillAccent", "#00566f"); // a mode map's default (its Light value)
Assert.Token("Colors.FillAccent", "#5fc8e8", mode: "dark"); // the half nobody looks at
Assert.Token("Colors.Border", "#c1d4d4", mode: "dark"); // no dark value: it keeps its one value
Assert.Token("Colors.Bg", "#eef3f3"); // through var(--colors-surface0)
Assert.Token("Colors.Bg", "#08161b", mode: "dark"); // …and the token it points at is mode-mapped
}Naming a font is not shipping one#
Font { Sans = "Inter, system-ui, sans-serif"; } is a request. The browser honours the first family it can
actually load, and a compiled app's What an app page is allowed to load blocks font CDNs — so a family your theme merely
names, with no face shipped, falls silently through to the next entry in the stack. The page still renders. It just
is not your typeface.
Assert.Font("Inter") asks whether the app serves a face for that family — a fact about the files pinned in your
osyrin.lock by osy font add, not about the stack in your theme.
theme Console {
Font { Sans = "Inter, system-ui, sans-serif"; }
}
[Test]
void The_app_ships_the_face_it_asks_for() {
Assert.Token("Font.Sans", "Inter, system-ui, sans-serif"); // the app ASKS for Inter
Assert.Font("Inter"); // …and really serves a face for it
}⚠ It asks what the app declares a face for, not whether the stored bytes are still fetchable. A family with no
row cannot produce an @font-face at all, which is the failure worth an app's own test; a stored blob that has gone
missing is a platform fault with its own guard, and nothing your app's test could act on.
Do my colours still read? — Assert.Contrast#
Assert.Contrast(ink, ground) takes two of the theme's colour tokens, resolves both the way Assert.Token does, and
holds their WCAG contrast ratio to 4.5:1 — the bar for words. A third argument sets another bar: 3 for large
text, or for something a person must see that is not words (a focus ring, a field's edge). mode: asks the pair in
a mode:
theme Console {
Colors {
Surface0 = Modes.Of(light: "#eef3f3", dark: "#08161b");
FillAccent = Modes.Of(light: "#00566f", dark: "#5fc8e8");
Border = "#c1d4d4";
}
}
[Test]
void The_accent_reads_on_the_page() {
Assert.Contrast("Colors.FillAccent", "Colors.Surface0"); // 7.3:1
Assert.Contrast("Colors.FillAccent", "Colors.Surface0", mode: "dark"); // 9.6:1
Assert.Contrast("Colors.Border", "Colors.Surface0", 1.3); // a hairline, not words
}It is how a theme's contrast becomes a unit test instead of a screenshot to squint at — run in seconds, in CI, with no browser. A finished look (Looks for business apps — start from a finished design and make it yours) ships its whole list of pairs this way; an app that derives from the look and changes its accent runs the same list to see what it broke. A failure names both colours, the mode and the ratio reached:
Assert.Contrast(…) failed: 'Colors.TextMuted' (#BBBBBB) on 'Colors.Surface' (#FFFFFF) in light mode is 1.92:1,
where it must reach 4.5:1. Darken the words or lighten the ground (the other way round on a dark ground) — or,
for a brand colour used as words, read it as `X.Text`, which the theme derives to read on the page.⚠ It compares two SOLID colours. A token that resolves to a translucent colour (rgba(…, .4)) or a CSS expression is
refused by name — its contrast depends on what is under it, so name the opaque token it sits on. Words drawn on a
ground the PAGE chose at run time are [[testing-ui-layout#readable|Assert.Readable]]'s question, in a real browser.
Why these are not UI assertions#
Every assertion in Layout assertions — is it actually usable on screen? needs a renderer with a compositor and reports itself NOT CHECKED under
plain osy test. These do not. The resolved tokens and the font manifest are loaded server-side, from the same
place the served stylesheet is generated, so a theme test is as cheap and as deterministic as a data test — and it
runs in CI without a browser anywhere.
That also means these say nothing about layout. A correct colour on an unclickable button is still an unclickable button; that is Layout assertions — is it actually usable on screen?'s question.
Examples#
What a failure tells you#
A refusal here is the thing a theme author meets most, so each one carries what you need to act:
# a token the theme does not declare — the CLOSED SET, grouped the way you wrote it. Every token the
# app RESOLVES is in it, so an app that also takes the kit's theme sees the kit's leaves beside its own.
Assert.Token (line 12) failed: this app's theme declares no token 'Colors.FilAccent'.
Did you mean 'Colors.FillAccent'? It declares: Colors { Bg, Border, FillAccent, Surface0, … } ·
Font { Mono, Sans } · Radius { Card, … } · …. A colour also carries its generated ramp steps
(`Colors.Primary.Hover`, `Colors.Primary[600]`), which are not listed here because every colour
has the same ones.
# a wrong value — with the route the token resolved through
Assert.Token (line 18) failed: 'Colors.Bg' resolves to "#eef3f3", not "#ffffff"
(through --colors-surface0)
# a wrong value in a mode — which mode is part of the sentence
Assert.Token (line 21) failed: 'Colors.FillAccent' in Dark mode resolves to "#5fc8e8", not "#00566f"
# a difference a browser would not have noticed
Assert.Token (line 24) failed: 'Colors.Border' resolves to "#c1d4d4", not "#C1D4D4" — they differ
only in CASE, which a browser does not care about but this comparison does.
# a family the app does not serve — again, the closed set
Assert.Font (line 27) failed: this app serves no font face for 'Intr'. It serves: Inter,
JetBrains Mono. Did you mean 'Inter'?Asserting that an assertion refuses#
Assert.Refuses composes with both, which is how you pin a refusal you are relying on:
theme Console {
Colors { Accent = "#00566f"; }
}
[Test]
void A_token_the_theme_lacks_is_refused_not_answered() {
Assert.Refuses(() => { Assert.Token("Colors.Nonesuch", "#000000"); }, "declares no token");
}See also#
- theme tokens — declaring the tokens these assert
- Assert — the rest of the assertion vocabulary
- [Test] / [TestFixture] — writing a
[Test]at all - Layout assertions — is it actually usable on screen? — the geometry claims, which DO need a real browser
- [[testing-ui#theme]] —
Ui.Theme("dark"): to SEE the app in a mode (aUi.Shot, anAssert.Readable) rather than read its tokens