import { describe, expect, test } from "bun:test"; import { compileCss } from "../views/css/index.ts"; import { evaluate } from "../views/css/evaluate.ts"; import { defineHooks } from "../views/hooks.ts"; /** * Tests of the structured css. */ describe("stcss", () => { test("flattens the nested rules", () => { const css = compileCss(` .card { color: red; &__title { font-weight: 700; } } `); expect(css).toContain(".card {"); expect(css).toContain(".card__title {"); expect(css).toContain("font-weight: 700;"); }); test("accepts Pug-like indentation, colonless declarations and blocks", () => { const css = compileCss(` $ink #101828 .card color $ink padding 1rem &__title font-weight 700 &:hover color #000 `); expect(css).toContain(".card {"); expect(css).toContain("color: #101828;"); expect(css).toContain("padding: 1rem;"); expect(css).toContain(".card__title {"); expect(css).toContain(".card:hover {"); }); test("accepts Pug-like at-rule blocks", () => { const css = compileCss(` .panel width: 30rem @media (max-width: 48rem) width 100% `); expect(css).toContain(".panel {"); expect(css).toContain("@media (max-width: 48rem)"); expect(css).toContain("width: 100%;"); }); test("runs application-defined STCSS expansions from a contextual registry", () => { const hooks = defineHooks((hook) => { hook.stcss.value( "double", ({ inner, resolve }) => `calc(${resolve(inner)} * 2)`, ); hook.stcss.declaration(({ selector, declaration }) => declaration.property === "outline-token" ? [`${selector}::before { content: "hook"; }`] : undefined, ); hook.stcss.atRule( "paint", ({ selector, params }) => `${selector} { color: ${params}; }`, ); }); const css = compileCss( `.card { width: double(12px); outline-token: yes; @paint rebeccapurple; }`, { hooks }, ); expect(css).toContain("width: calc(12px * 2);"); expect(css).toContain('.card::before { content: "hook"; }'); expect(css).toContain(".card { color: rebeccapurple; }"); }); test("expands calculation hooks and centered position aliases", () => { const css = compileCss(` .dialog { position: fixed-center; width: 30rem; margin-left: $offset($center(30rem), $pad); padding: $pad(2); } .page-dialog { position: page-center; transform: scale(.96); } `); expect(css).toContain("position: fixed;"); expect(css).toContain("left: 50%;"); expect(css).toContain("top: 50%;"); expect(css).toContain("transform: translate(-50%, -50%);"); expect(css).toContain( "margin-left: calc(calc(50% - (30rem / 2)) + var(--muddle-pad, 1rem));", ); expect(css).toContain("padding: calc(var(--muddle-pad, 1rem) * 2);"); expect(css).toContain("position: absolute;"); expect(css).toContain("transform: translate(-50%, -50%) scale(.96);"); }); test("registers application calculation hooks", () => { const hooks = defineHooks((hook) => { hook.stcss.calculation( "third", ({ args, resolve }) => `calc(${resolve(args ?? "100%")} / 3)`, ); }); expect(compileCss(".column { width: $third(90%); }", { hooks })).toContain( "width: calc(90% / 3);", ); }); test("resolves the variables", () => { const css = compileCss(` $ink: #101828; .body { color: $ink; } `); expect(css).toContain("color: #101828;"); }); test("interpolates an expression", () => { const css = compileCss(` $space: 1; .gutter { padding: #{$space * 2}rem; } `); expect(css).toContain("padding: 2rem;"); }); test("repeats a block", () => { const css = compileCss(` @each $space in 1 2 3 { .space-#{$space} { margin: #{$space * 0.5}rem; } } `); expect(css).toContain(".space-1 {"); expect(css).toContain(".space-2 {"); expect(css).toContain(".space-3 {"); expect(css).toContain("margin: 1.5rem;"); }); test("counts a range", () => { const css = compileCss(` @for $i from 1 through 2 { .col-#{$i} { width: #{$i * 10}%; } } `); expect(css).toContain(".col-1 {"); expect(css).toContain(".col-2 {"); }); test("expands a mixin", () => { const css = compileCss(` @mixin rounded($size) { border-radius: $size; } .button { @include rounded(4px); color: blue; } `); expect(css).toContain("border-radius: 4px;"); expect(css).toContain("color: blue;"); }); test("keeps the css functions untouched", () => { const css = compileCss(` .box { border: 1px solid rgba(16, 24, 40, 0.12); color: rgb(1, 2, 3); } `); expect(css).toContain("rgba(16, 24, 40, 0.12)"); expect(css).toContain("rgb(1, 2, 3)"); }); test("darkens a colour", () => { const css = compileCss(` .button { color: darken(#2b6cb0, 10); } `); expect(css).toMatch(/color: #[0-9a-f]{6};/); }); test("evaluates an expression", () => { expect(evaluate("$a * 2", { scope: { $a: "21" } })).toBe("42"); expect(evaluate("$a + 1", { scope: { $a: "1" } })).toBe("2"); }); test("compiles anchor relations, loop ranges and back-propagated state", () => { const css = compileCss(` @each $n in [0..2] { nav:with(@hello$n:active) li:nth-child(#{$n + 1}) { left: calc($relate(d"@hello{$n + 1}") + $relate[width](d"@hello{$n + 1}")); color: @hello$n.computed.backgroundColor; } } nav li:when(nav @hello1:clicked) { opacity: 1; } `); expect(css).toContain( 'nav:has([data-muddle-anchor="hello0"]:active) li:nth-child(1)', ); expect(css).toContain("var(--muddle-relate-hello1-x)"); expect(css).toContain("var(--muddle-relate-hello1-width)"); expect(css).toContain('nav:has([data-muddle-anchor="hello1"]:active) li'); }); test("resolves $refer as a relation value", () => { const css = compileCss( `.tooltip { left: $refer[left](@trigger); width: $refer[width](@trigger); }`, ); expect(css).toContain("var(--muddle-relate-trigger-x)"); expect(css).toContain("var(--muddle-relate-trigger-width)"); }); test("reflects dotted anchor groups without enumerating their members", () => { const css = compileCss(`:reflect(@docs) { scroll-margin-top: 4rem; }`); expect(css).toContain('[data-muddle-anchor^="docs."]'); }); test("expands concentric border radii", () => { const css = compileCss(` .panel { padding: 1rem; border: 2px solid #333; border-radius: concentric 0.75rem; } .panel-default { border-radius: concentric; } .blind { padding: 12px; border: var(--bw, 1px) solid; border-radius: concentric var(--outer, 12px); } `); expect(css).toContain( ".panel > * {\n\tborder-radius: calc((0.75rem) - (1rem) - (2px));", ); expect(css).toContain(".panel-default > * {\n\tborder-radius: inherit;"); expect(css).toContain( ".blind > * {\n\tborder-radius: calc((var(--outer, 12px)) - (12px) - (var(--bw, 1px)));", ); }); test("expands freeform gradients into radial gradient stacks", () => { const css = compileCss(` .paint { background-image: freeform-gradient(hsl(270 50% 60%) 10% 50%, red 70% 40%, rgb(0 128 0) 40% 80% 30%); } `); expect(css).toContain( "radial-gradient(circle farthest-side at 10% 50%, hsl(270 50% 60%), transparent)", ); expect(css).toContain( "radial-gradient(circle farthest-side at 70% 40%, red, transparent)", ); expect(css).toContain( "radial-gradient(circle at 40% 80%, rgb(0 128 0), rgb(0 128 0) calc(30% * 0.97), transparent 30%)", ); expect(() => compileCss( ".x { background-image: freeform-gradient(red 1% 2% 3% 4%); }", ), ).toThrow(); }); }); describe("stcss @measure", () => { test("records prediction targets and anchors them as relations", () => { const measured: string[] = []; const css = compileCss( ` .ribbon { @measure; } @measure @trigger, .card, panel; `, { measured }, ); expect(css).toContain("/* muddle-predict:"); expect(measured).toEqual([ ".ribbon", '[data-muddle-anchor="trigger"]', ".card", "panel", ]); }); });