Osy#betaRun it on Osyrin: osyrin.com

Reference / Testing

Theme assertions — does the app really look the way the theme says?

Assert.Token(token, expected[, mode: "dark"]) · Assert.Font(family) · Assert.Contrast(ink, ground[, atLeast][, mode:])

A theme was write-only — Theme.Toggle and Theme.Set are both writes, and a token identifier is compile-time style material that never becomes a value — so an app author could not test their own theme at all. Assert.Token reads the RESOLVED token, following token references the way the cascade does, and `mode:` reads the per-mode value, which is where theming actually goes wrong. Assert.Font asks the other question a token cannot: whether the app really serves a face for a family its theme merely names. Both ask the server, so neither needs a browser.

preview4 examples compiled by CItestinguithemetokens

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 mode

A 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 themeon the pageAssert.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 blockthe 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#

Related

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…

Assert

The assertions a test makes. Beyond the usual equality and null checks there are comparisons (Greater, Less, InRange)…

[Test] / [TestFixture]

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and…

web fonts — shipping a typeface with your app

Naming a font in your theme asks for it; `osy font add` ships it. The command pins a font file in your project's lock…

Layout assertions — is it actually usable on screen?

Every other assertion is about TEXT or STATE, and all of them pass on a screen that is visually broken: a control can…