302 lines
7.6 KiB
TypeScript
302 lines
7.6 KiB
TypeScript
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",
|
|
]);
|
|
});
|
|
});
|