mirror of
https://github.com/tabler/tabler.git
synced 2026-08-05 01:44:41 +04:00
Add SCSS unit tests for mixins, utilities, and helpers (#2699)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Added SCSS unit test suite with `sass-true` and `test:scss` Vitest config for Bootstrap and Tabler mixins, utilities, and helpers.
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Test build
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
@@ -7,7 +7,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
@@ -33,5 +33,5 @@ jobs:
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
working-directory: core
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm --filter @tabler/core test
|
||||
- name: Run JS tests
|
||||
run: pnpm --filter @tabler/core test:js
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: SCSS Tests
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run SCSS tests
|
||||
run: pnpm --filter @tabler/core test:scss
|
||||
+10
-3
@@ -38,9 +38,14 @@
|
||||
"format:check": "prettier --check \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
|
||||
"format:write": "prettier --write \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
"test": "concurrently \"pnpm run test:js\" \"pnpm run test:scss\"",
|
||||
"test:js": "vitest run",
|
||||
"test:scss": "vitest run --config vitest.scss.config.mjs",
|
||||
"test:js:watch": "vitest",
|
||||
"test:js:coverage": "vitest run --coverage",
|
||||
"test:scss:watch": "vitest --config vitest.scss.config.mjs",
|
||||
"test:watch": "pnpm run test:js:watch",
|
||||
"test:coverage": "pnpm run test:js:coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -179,6 +184,8 @@
|
||||
"nouislider": "^15.8.1",
|
||||
"plyr": "^3.8.4",
|
||||
"signature_pad": "^5.1.3",
|
||||
"sass": "1.101.0",
|
||||
"sass-true": "^9.0.0",
|
||||
"sortablejs": "^1.15.7",
|
||||
"star-rating.js": "^4.3.1",
|
||||
"tom-select": "^2.6.1",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Priority 1 — unit tests for `add()` and `subtract()` from
|
||||
// scss/mixins/_functions.scss. Both guard against `null` operands and fall back
|
||||
// to a `calc()` expression when the operands are unit-incompatible, so the
|
||||
// branches around null-handling and the numeric-vs-calc path are what matter.
|
||||
|
||||
@use "sass:meta";
|
||||
@use "sass:string";
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("add") {
|
||||
@include it("treats a null operand as the identity") {
|
||||
@include assert-equal(add(null, 5px), 5px);
|
||||
@include assert-equal(add(5px, null), 5px);
|
||||
}
|
||||
|
||||
@include it("adds two compatible numbers directly") {
|
||||
@include assert-equal(add(2px, 3px), 5px);
|
||||
}
|
||||
|
||||
// add() returns a real `calculation` here; two structurally-identical
|
||||
// calculations aren't `==` in Sass, so compare the rendered output instead.
|
||||
@include it("falls back to calc() for incompatible units") {
|
||||
@include assert-equal(meta.inspect(add(2px, 3%)), "calc(2px + 3%)");
|
||||
}
|
||||
|
||||
@include it("builds a plain expression when calc is disabled") {
|
||||
@include assert-equal(add(2px, 3%, false), string.unquote("2px + 3%"));
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("subtract") {
|
||||
@include it("returns null when both operands are null") {
|
||||
@include assert-equal(subtract(null, null), null);
|
||||
}
|
||||
|
||||
@include it("handles a single null operand") {
|
||||
@include assert-equal(subtract(5px, null), 5px);
|
||||
@include assert-equal(subtract(null, 5px), -5px);
|
||||
}
|
||||
|
||||
@include it("subtracts two compatible numbers directly") {
|
||||
@include assert-equal(subtract(5px, 2px), 3px);
|
||||
}
|
||||
|
||||
@include it("falls back to calc() for incompatible units") {
|
||||
@include assert-equal(meta.inspect(subtract(5px, 2%)), "calc(5px - 2%)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Next batch — the radius helper functions from
|
||||
// scss/mixins/bootstrap/_border-radius.scss. Both are self-contained (only
|
||||
// sass:list / sass:math / sass:meta) and operate on single values or lists.
|
||||
//
|
||||
// They return Sass lists, so the results are compared through `meta.inspect(...)`
|
||||
// (a single-item list is not `==` to the bare number it contains).
|
||||
|
||||
@use "sass:meta";
|
||||
@use "../mixins/bootstrap/border-radius" as *;
|
||||
|
||||
@include describe("valid-radius") {
|
||||
@include it("clamps a negative value to 0") {
|
||||
@include assert-equal(meta.inspect(valid-radius(-5px)), "0");
|
||||
}
|
||||
|
||||
@include it("leaves a non-negative value untouched") {
|
||||
@include assert-equal(meta.inspect(valid-radius(8px)), "8px");
|
||||
}
|
||||
|
||||
@include it("clamps only the negative members of a list") {
|
||||
@include assert-equal(meta.inspect(valid-radius(4px -2px 6px)), "4px 0 6px");
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("squircle-radius") {
|
||||
@include it("keeps 0 as 0") {
|
||||
@include assert-equal(meta.inspect(squircle-radius(0)), "0");
|
||||
}
|
||||
|
||||
@include it("scales a non-zero radius by 2.5 via calc()") {
|
||||
@include assert-equal(meta.inspect(squircle-radius(4px)), "calc(4px * 2.5)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Output tests for the `box-shadow` mixin in scss/mixins/bootstrap/_box-shadow.scss.
|
||||
// With `$enable-shadows: true` (the default) it filters out null values, keeps a
|
||||
// single keyword (none/inherit/...) as-is, and otherwise joins the shadows into a
|
||||
// comma list. The `$enable-shadows: false` gate depends on overriding that global
|
||||
// and is not covered here.
|
||||
|
||||
@use "../mixins/bootstrap/box-shadow" as *;
|
||||
|
||||
@include describe("box-shadow") {
|
||||
@include it("emits a single shadow value") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include box-shadow(0 1px 2px #000); } }
|
||||
@include expect() { .t { box-shadow: 0 1px 2px #000; } }
|
||||
}
|
||||
}
|
||||
|
||||
@include it("passes a keyword through untouched") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include box-shadow(none); } }
|
||||
@include expect() { .t { box-shadow: none; } }
|
||||
}
|
||||
}
|
||||
|
||||
@include it("joins multiple shadows into a comma list") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include box-shadow(0 1px 2px #000, 0 2px 4px #111); } }
|
||||
@include expect() { .t { box-shadow: 0 1px 2px #000, 0 2px 4px #111; } }
|
||||
}
|
||||
}
|
||||
|
||||
@include it("filters out null values") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include box-shadow(null, 0 1px 2px #000); } }
|
||||
@include expect() { .t { box-shadow: 0 1px 2px #000; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Priority 4 — unit tests for the responsive breakpoint helpers. These are the
|
||||
// "needs config" tier: they default to the global `$grid-breakpoints`, but every
|
||||
// function also accepts an explicit `$breakpoints` map, so the tests pass a
|
||||
// local fixture map and stay independent of the project variables.
|
||||
//
|
||||
// `breakpoint-prev` lives in mixins/_functions.scss; the rest live in
|
||||
// mixins/bootstrap/_breakpoints.scss.
|
||||
|
||||
@use "../mixins/functions" as fn;
|
||||
@use "../mixins/bootstrap/breakpoints" as *;
|
||||
|
||||
$grid: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px);
|
||||
|
||||
@include describe("breakpoint-next / breakpoint-prev") {
|
||||
@include it("returns the neighbouring breakpoint name") {
|
||||
@include assert-equal(breakpoint-next(md, $grid), lg);
|
||||
@include assert-equal(fn.breakpoint-prev(md, $grid), sm);
|
||||
}
|
||||
|
||||
@include it("returns null past the last / before the first breakpoint") {
|
||||
@include assert-equal(breakpoint-next(xl, $grid), null);
|
||||
@include assert-equal(fn.breakpoint-prev(xs, $grid), null);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("breakpoint-min") {
|
||||
@include it("returns the min width, or null for the zero breakpoint") {
|
||||
@include assert-equal(breakpoint-min(md, $grid), 768px);
|
||||
@include assert-equal(breakpoint-min(xs, $grid), null);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("breakpoint-max") {
|
||||
@include it("subtracts 0.02px to avoid min/max overlap") {
|
||||
@include assert-equal(breakpoint-max(md, $grid), 767.98px);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("breakpoint-infix") {
|
||||
@include it("is blank for the smallest breakpoint and dash-prefixed otherwise") {
|
||||
@include assert-equal(breakpoint-infix(xs, $grid), "");
|
||||
@include assert-equal(breakpoint-infix(md, $grid), "-md");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Output tests for the caret mixins in scss/mixins/bootstrap/_caret.scss.
|
||||
// The `caret-{down,up,end,start}` helpers draw a triangle out of borders; the
|
||||
// main `caret()` builds a pseudo-element and branches on `$direction` for the
|
||||
// rotation and (for `left`) the ::before/::after structure.
|
||||
|
||||
@use "../mixins/bootstrap/caret" as *;
|
||||
|
||||
@include describe("caret-* border helpers") {
|
||||
@include it("caret-down points the border triangle downward") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include caret-down(0.3em); } }
|
||||
@include expect() {
|
||||
.t {
|
||||
border-top: 0.3em solid;
|
||||
border-right: 0.3em solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: 0.3em solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("caret-up points the border triangle upward") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include caret-up(0.3em); } }
|
||||
@include expect() {
|
||||
.t {
|
||||
border-top: 0;
|
||||
border-right: 0.3em solid transparent;
|
||||
border-bottom: 0.3em solid;
|
||||
border-left: 0.3em solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("caret-start omits the left border") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include caret-start(0.3em); } }
|
||||
@include expect() {
|
||||
.t {
|
||||
border-top: 0.3em solid transparent;
|
||||
border-right: 0.3em solid;
|
||||
border-bottom: 0.3em solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("caret mixin") {
|
||||
@include it("renders an ::after caret rotated for the down direction") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include caret(down); } }
|
||||
@include expect() {
|
||||
.t:after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
vertical-align: 0.255em;
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
border-bottom: 1px var(--tblr-border-style);
|
||||
border-inline-start: 1px var(--tblr-border-style);
|
||||
margin-inline-end: 0.1em;
|
||||
margin-inline-start: 0.255em;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("uses ::before and clears ::after for the left direction") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include caret(left); } }
|
||||
@include expect() {
|
||||
.t:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
vertical-align: 0.255em;
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
border-bottom: 1px var(--tblr-border-style);
|
||||
border-inline-start: 1px var(--tblr-border-style);
|
||||
margin-inline-end: 0.1em;
|
||||
margin-inline-end: 0.255em;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.t:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Tests for `color-contrast()` from scss/mixins/_functions.scss.
|
||||
//
|
||||
// The globals it reads ($white, $black, $color-contrast-*, $min-contrast-ratio)
|
||||
// live in scss/_settings.scss, which functions.scss already `@use`s, so no
|
||||
// fixtures are needed — a plain `@use` of the functions module is enough.
|
||||
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("color-contrast") {
|
||||
@include it("picks black text on a light background") {
|
||||
@include assert-equal(color-contrast(#ffffff), #000000);
|
||||
}
|
||||
|
||||
@include it("picks white text on a dark background") {
|
||||
@include assert-equal(color-contrast(#000000), #ffffff);
|
||||
}
|
||||
|
||||
// When no candidate reaches the required ratio the function falls back to the
|
||||
// highest-contrast option (and emits a @warn, which sass-true ignores). An
|
||||
// impossible ratio of 21 against mid-grey forces that branch; black wins.
|
||||
@include it("falls back to the highest-contrast colour when none qualifies") {
|
||||
@include assert-equal(color-contrast(#808080, #000, #fff, 21), #000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Output test for the `color-mode` mixin in scss/mixins/bootstrap/_color-mode.scss.
|
||||
//
|
||||
// With the default `$color-mode-type: data`, the mixin scopes its content under
|
||||
// the `[data-bs-theme]` / `[data-theme]` attribute selectors (the mechanism
|
||||
// Tabler uses for dark mode). The `media-query` branch depends on overriding
|
||||
// that global through the variables module, so it is not covered here.
|
||||
|
||||
@use "../mixins/bootstrap/color-mode" as *;
|
||||
|
||||
@include describe("color-mode") {
|
||||
@include it("scopes content under the theme attribute selectors") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include color-mode(dark) {
|
||||
.x { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
[data-bs-theme=dark] .x,
|
||||
[data-theme=dark] .x {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Priority 2 — unit tests for the colour/contrast helpers in
|
||||
// scss/mixins/_functions.scss. These are deterministic and, unlike
|
||||
// `color-contrast`, do not read any global config, so they can be exercised in
|
||||
// isolation. A regression here silently degrades accessibility, which is why
|
||||
// they rank high.
|
||||
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("luminance") {
|
||||
@include it("is 0 for black and 1 for white") {
|
||||
@include assert-equal(luminance(#000), 0);
|
||||
@include assert-equal(luminance(#fff), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("contrast-ratio") {
|
||||
@include it("is 21 for the black/white extreme") {
|
||||
@include assert-equal(contrast-ratio(#000, #fff), 21);
|
||||
}
|
||||
|
||||
@include it("is 1 for a colour against itself") {
|
||||
@include assert-equal(contrast-ratio(#fff, #fff), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("shade-color / tint-color") {
|
||||
@include it("shade mixes the colour toward black") {
|
||||
@include assert-equal(shade-color(#ffffff, 10%), rgb(229.5, 229.5, 229.5));
|
||||
}
|
||||
|
||||
@include it("tint mixes the colour toward white") {
|
||||
@include assert-equal(tint-color(#000000, 10%), rgb(25.5, 25.5, 25.5));
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("shift-color") {
|
||||
@include it("shades when the weight is positive") {
|
||||
@include assert-equal(shift-color(#808080, 20%), shade-color(#808080, 20%));
|
||||
}
|
||||
|
||||
@include it("tints when the weight is negative") {
|
||||
@include assert-equal(shift-color(#808080, -20%), tint-color(#808080, 20%));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Next batch — self-contained helper functions from scss/mixins/_functions.scss
|
||||
// that need no project config (arguments passed inline). Covers colour channel
|
||||
// extraction, alpha flattening, percentage coercion, the transparent/SVG string
|
||||
// builders and the higher-order `map-loop`.
|
||||
//
|
||||
// Some functions return a `calculation`, an unquoted `color-mix(...)` string, or
|
||||
// a `url(...)` token; those are asserted through `meta.inspect(...)` so the
|
||||
// comparison is against the exact rendered output.
|
||||
|
||||
@use "sass:meta";
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("to-rgb") {
|
||||
@include it("returns the comma-separated rgb channels") {
|
||||
@include assert-equal(to-rgb(#ff0000), (255, 0, 0));
|
||||
@include assert-equal(to-rgb(#123456), (18, 52, 86));
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("opaque") {
|
||||
@include it("flattens a translucent foreground over the background") {
|
||||
@include assert-equal(opaque(#fff, rgba(#000, 0.5)), rgb(127.5, 127.5, 127.5));
|
||||
}
|
||||
|
||||
@include it("returns the foreground unchanged when it is fully opaque") {
|
||||
@include assert-equal(opaque(#fff, #000), black);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("to-percentage") {
|
||||
@include it("converts a unitless number to a percentage") {
|
||||
@include assert-equal(to-percentage(0.5), 50%);
|
||||
}
|
||||
|
||||
@include it("passes a value that already has a unit straight through") {
|
||||
@include assert-equal(to-percentage(10px), 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("color-transparent") {
|
||||
@include it("returns the colour untouched when alpha is 1") {
|
||||
@include assert-equal(color-transparent(#f00, 1), #f00);
|
||||
}
|
||||
|
||||
@include it("builds a color-mix() with the alpha as a percentage otherwise") {
|
||||
@include assert-equal(
|
||||
meta.inspect(color-transparent(#f00, 0.5)),
|
||||
"color-mix(in srgb, #f00 50%, transparent)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("url-svg") {
|
||||
@include it("percent-encodes '#' so the data URI stays valid") {
|
||||
@include assert-equal(
|
||||
meta.inspect(url-svg("#")),
|
||||
'url("data:image/svg+xml;charset=UTF-8,%23")'
|
||||
);
|
||||
}
|
||||
|
||||
@include it("injects the SVG xmlns attribute") {
|
||||
@include assert-equal(
|
||||
meta.inspect(url-svg("<svg></svg>")),
|
||||
"url('data:image/svg+xml;charset=UTF-8,<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>')"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("map-loop") {
|
||||
// Applies a module-level function (resolved by name via meta.get-function) to
|
||||
// every value in the map. `to-percentage` lives in the same module, so it
|
||||
// resolves correctly.
|
||||
@include it("maps every value through the named function") {
|
||||
@include assert-equal(
|
||||
map-loop((a: 0.5, b: 0.25), "to-percentage", "$value"),
|
||||
(a: 50%, b: 25%)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Tests for the CSS custom-property builders in scss/mixins/_functions.scss:
|
||||
// `varify()` and `rgba-css-var()`. Both read the global `$prefix`, which lives
|
||||
// in scss/_settings.scss (already `@use`d by functions.scss), so a plain `@use`
|
||||
// of the functions module is enough — no fixtures.
|
||||
|
||||
@use "sass:meta";
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("varify") {
|
||||
// `varify` seeds its accumulator with `null`, so the raw list inspects as
|
||||
// `null var(...) ...`; Sass drops the null when serialising to CSS, so the
|
||||
// behaviour is asserted through the rendered output rather than the value.
|
||||
@include it("prefixes each entry as a space-separated var() list") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
padding: varify(("gutter-x" "gutter-y"));
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
padding: var(--tblr-gutter-x) var(--tblr-gutter-y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("rgba-css-var") {
|
||||
@include it("builds a color-mix() from the identifier and target opacity") {
|
||||
@include assert-equal(
|
||||
meta.inspect(rgba-css-var("primary", "bg")),
|
||||
"color-mix(in srgb, var(--tblr-primary) calc(var(--tblr-bg-opacity) * 100%), transparent)"
|
||||
);
|
||||
}
|
||||
|
||||
@include it("special-cases the body background") {
|
||||
@include assert-equal(
|
||||
meta.inspect(rgba-css-var("body", "bg")),
|
||||
"color-mix(in srgb, var(--tblr-body-bg) calc(var(--tblr-bg-opacity) * 100%), transparent)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Priority 1 — unit tests for `divide($dividend, $divisor, $precision: 10)`
|
||||
// from scss/mixins/_functions.scss. This is a hand-rolled long-division
|
||||
// algorithm (sign handling, configurable precision, unit propagation), so it
|
||||
// carries the most branching logic of any function in the file.
|
||||
//
|
||||
// Note: the divide-by-zero path uses Sass `@error`, which aborts compilation
|
||||
// and cannot be caught by sass-true (there is no assert-throws), so it is not
|
||||
// covered here.
|
||||
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("divide") {
|
||||
@include it("divides two unitless numbers") {
|
||||
@include assert-equal(divide(4, 2), 2);
|
||||
}
|
||||
|
||||
@include it("returns a fractional result") {
|
||||
@include assert-equal(divide(10, 4), 2.5);
|
||||
@include assert-equal(divide(1, 2), 0.5);
|
||||
}
|
||||
|
||||
@include it("returns 0 when the dividend is 0") {
|
||||
@include assert-equal(divide(0, 5), 0);
|
||||
}
|
||||
|
||||
@include it("keeps the correct sign for mixed-sign operands") {
|
||||
@include assert-equal(divide(-6, 2), -3);
|
||||
@include assert-equal(divide(6, -2), -3);
|
||||
@include assert-equal(divide(-6, -2), 3);
|
||||
}
|
||||
|
||||
@include it("propagates the dividend unit when the divisor is unitless") {
|
||||
@include assert-equal(divide(10px, 2), 5px);
|
||||
@include assert-equal(divide(5%, 2), 2.5%);
|
||||
}
|
||||
|
||||
@include it("cancels matching units to a unitless result") {
|
||||
@include assert-equal(divide(10px, 2px), 5);
|
||||
}
|
||||
|
||||
@include it("rounds to the requested precision") {
|
||||
@include assert-equal(divide(2, 3, 0), 1);
|
||||
@include assert-equal(divide(2, 3, 2), 0.67);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Tests for `escape-svg()` from scss/mixins/_functions.scss.
|
||||
//
|
||||
// It iterates the global `$escaped-characters` map, which lives in
|
||||
// scss/_settings.scss (already `@use`d by functions.scss), so a plain `@use` of
|
||||
// the functions module is enough — no fixtures. Escaping only happens when the
|
||||
// string looks like an SVG data URI; anything else is returned untouched.
|
||||
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("escape-svg") {
|
||||
@include it("returns a non data-URI string unchanged") {
|
||||
@include assert-equal(escape-svg("no data uri <here>"), "no data uri <here>");
|
||||
}
|
||||
|
||||
@include it("percent-encodes reserved characters inside an SVG data URI") {
|
||||
@include assert-equal(
|
||||
escape-svg("data:image/svg+xml,<svg><path/></svg>"),
|
||||
"data:image/svg+xml,%3csvg%3e%3cpath/%3e%3c/svg%3e"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Output tests for the `generate-utility` mixin from
|
||||
// scss/mixins/bootstrap/_utilities.scss — the engine behind Tabler's utility
|
||||
// classes. `@use "../mixins/bootstrap/utilities"` pulls in the real
|
||||
// settings/variables chain, so `$prefix` = "tblr-" and
|
||||
// `$enable-important-utilities` = true (hence the `!important` in the output).
|
||||
//
|
||||
// Each `it` feeds the mixin a hand-built utility map and asserts the generated
|
||||
// classes, covering the branches that shape the output: map vs list values,
|
||||
// single vs multiple properties, custom class, css-var mode, pseudo-class
|
||||
// states and the responsive infix.
|
||||
|
||||
@use "sass:map";
|
||||
@use "../mixins/bootstrap/utilities" as *;
|
||||
@use "../mixins/bootstrap/breakpoints" as *;
|
||||
|
||||
@include describe("generate-utility") {
|
||||
@include it("generates a class per key from a values map") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility((property: opacity, values: (0: 0, 50: 0.5)));
|
||||
}
|
||||
@include expect() {
|
||||
.opacity-0 { opacity: 0 !important; }
|
||||
.opacity-50 { opacity: 0.5 !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("zips a plain list of values into key/value pairs") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility((property: float, class: float, values: (left, right)));
|
||||
}
|
||||
@include expect() {
|
||||
.float-left { float: left !important; }
|
||||
.float-right { float: right !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("emits every property when several are given") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility((property: (margin-right margin-left), class: mx, values: (0: 0, auto: auto)));
|
||||
}
|
||||
@include expect() {
|
||||
.mx-0 {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.mx-auto {
|
||||
margin-right: auto !important;
|
||||
margin-left: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("sets a custom property instead of a declaration in css-var mode") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility((property: opacity, class: opacity, css-var: true, values: (50: 0.5)));
|
||||
}
|
||||
@include expect() {
|
||||
.opacity-50 {
|
||||
--tblr-opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("adds a pseudo-class variant for each declared state") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility((property: text-decoration, class: text-decoration, state: hover, values: (underline)));
|
||||
}
|
||||
@include expect() {
|
||||
.text-decoration-underline { text-decoration: underline !important; }
|
||||
.text-decoration-underline-hover:hover { text-decoration: underline !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("injects the responsive infix into the class name") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility((property: display, class: d, values: (none)), "-md");
|
||||
}
|
||||
@include expect() {
|
||||
.d-md-none { display: none !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `generate-utility` itself does not read the `responsive` key — that gate lives
|
||||
// in bootstrap/utilities/_api.scss, which loops the breakpoints and only calls
|
||||
// the mixin for a given infix when `map.get($utility, responsive)` is truthy (or
|
||||
// it's the base, infix-less breakpoint). This reproduces that gate against a local
|
||||
// breakpoint map with two utilities: one `responsive: true`, one without.
|
||||
@include describe("generate-utility (responsive property)") {
|
||||
$g: (xs: 0, sm: 576px, md: 768px);
|
||||
$utils: (
|
||||
"display": (property: display, class: d, responsive: true, values: (none, flex)),
|
||||
"float": (property: float, class: float, values: (left, right)),
|
||||
);
|
||||
|
||||
@include it("only a `responsive: true` utility gets breakpoint-infixed variants") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@each $bp in map.keys($g) {
|
||||
@include media-breakpoint-up($bp, $g) {
|
||||
$infix: breakpoint-infix($bp, $g);
|
||||
@each $key, $utility in $utils {
|
||||
@if map.get($utility, responsive) or $infix == "" {
|
||||
@include generate-utility($utility, $infix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
// base breakpoint: both utilities emit bare classes
|
||||
.d-none { display: none !important; }
|
||||
.d-flex { display: flex !important; }
|
||||
.float-left { float: left !important; }
|
||||
.float-right { float: right !important; }
|
||||
|
||||
// responsive breakpoints: only `d` (responsive: true) is repeated; `float` is not
|
||||
@media (min-width: 576px) {
|
||||
.d-sm-none { display: none !important; }
|
||||
.d-sm-flex { display: flex !important; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.d-md-none { display: none !important; }
|
||||
.d-md-flex { display: flex !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Output tests for the grid mixins in scss/mixins/bootstrap/_grid.scss.
|
||||
// `make-col`/`make-col-offset`/`row-cols` do the real width maths (via `divide`
|
||||
// + math.percentage); `$columns` is passed explicitly so the tests don't depend
|
||||
// on the global `$grid-columns`.
|
||||
|
||||
@use "../mixins/bootstrap/grid" as *;
|
||||
|
||||
@include describe("make-col") {
|
||||
@include it("sets a fixed width as a percentage of the column count") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-col(6, 12); } }
|
||||
@include expect() { .t { flex: 0 0 auto; width: 50%; } }
|
||||
}
|
||||
}
|
||||
|
||||
@include it("grows to fill the row when no size is given") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-col(); } }
|
||||
@include expect() { .t { flex: 1 1 0; max-width: 100%; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("make-col-auto") {
|
||||
@include it("sizes to the content") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-col-auto(); } }
|
||||
@include expect() { .t { flex: 0 0 auto; width: auto; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("make-col-offset") {
|
||||
@include it("offsets by a percentage of the column count") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-col-offset(3, 12); } }
|
||||
@include expect() { .t { margin-left: 25%; } }
|
||||
}
|
||||
}
|
||||
|
||||
@include it("collapses a zero offset to 0 (not 0%)") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-col-offset(0, 12); } }
|
||||
@include expect() { .t { margin-left: 0; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("row-cols") {
|
||||
@include it("splits children into equal fractions of the row") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include row-cols(3); } }
|
||||
@include expect() { .t > * { flex: 0 0 auto; width: 33.33333333%; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("make-row") {
|
||||
@include it("sets the gutter custom properties and negative margins") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-row(1rem); } }
|
||||
@include expect() {
|
||||
.t {
|
||||
--tblr-gutter-x: 1rem;
|
||||
--tblr-gutter-y: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: calc(-1 * var(--tblr-gutter-y));
|
||||
margin-right: calc(-0.5 * var(--tblr-gutter-x));
|
||||
margin-left: calc(-0.5 * var(--tblr-gutter-x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("make-col-ready") {
|
||||
@include it("primes a column with full width and gutter padding") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include make-col-ready(); } }
|
||||
@include expect() {
|
||||
.t {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-right: calc(var(--tblr-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--tblr-gutter-x) * 0.5);
|
||||
margin-top: var(--tblr-gutter-y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Priority 3 — unit tests for the map utilities in scss/mixins/_functions.scss.
|
||||
// The test maps are built inline, so no project config is required.
|
||||
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("negativify-map") {
|
||||
@include it("prefixes keys with `n` and negates the values") {
|
||||
@include assert-equal(
|
||||
negativify-map((1: 10px, 2: 20px)),
|
||||
("n1": -10px, "n2": -20px)
|
||||
);
|
||||
}
|
||||
|
||||
@include it("leaves a 0 key untouched") {
|
||||
@include assert-equal(
|
||||
negativify-map((0: 0, 1: 5px)),
|
||||
("n1": -5px)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("map-get-multiple") {
|
||||
@include it("keeps only the requested keys, preserving map order") {
|
||||
@include assert-equal(
|
||||
map-get-multiple((a: 1, b: 2, c: 3), (a c)),
|
||||
(a: 1, c: 3)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("map-merge-multiple") {
|
||||
@include it("merges maps left-to-right, later keys winning") {
|
||||
@include assert-equal(
|
||||
map-merge-multiple((a: 1), (b: 2), (a: 3)),
|
||||
(a: 3, b: 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Output tests for the responsive media-query mixins. Like the breakpoint
|
||||
// functions, every mixin accepts an explicit `$breakpoints` map, so the tests
|
||||
// pass a local fixture map and stay independent of the global `$grid-breakpoints`.
|
||||
//
|
||||
// `media-breakpoint-{up,down,between,only}` come from
|
||||
// mixins/bootstrap/_breakpoints.scss; `media-breakpoint-down-than` lives in
|
||||
// mixins/_functions.scss. Their names don't collide, so both are used as `*`.
|
||||
//
|
||||
// Max-widths are the breakpoint value minus 0.02px (the min-/max- overlap fix);
|
||||
// the smallest/edge cases emit the content with no media query at all.
|
||||
|
||||
@use "../mixins/bootstrap/breakpoints" as *;
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
$g: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px);
|
||||
|
||||
@include describe("media-breakpoint-up") {
|
||||
@include it("wraps content in a min-width query") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-up(md, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media (min-width: 768px) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("emits content directly for the zero breakpoint") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-up(xs, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("media-breakpoint-down") {
|
||||
@include it("wraps content in a max-width query (value - 0.02px)") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-down(md, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media (max-width: 767.98px) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("emits content directly for the zero breakpoint") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-down(xs, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("media-breakpoint-between") {
|
||||
@include it("combines a min-width and a max-width query") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-between(sm, md, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media (min-width: 576px) and (max-width: 767.98px) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("media-breakpoint-only") {
|
||||
@include it("spans from the breakpoint min to the next breakpoint max") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-only(md, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media (min-width: 768px) and (max-width: 991.98px) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("media-breakpoint-down-than") {
|
||||
@include it("targets everything below the previous breakpoint") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-down-than(md, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media (max-width: 575.98px) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("emits content directly when there is no previous breakpoint") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-breakpoint-down-than(xs, $g) {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t { color: red; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Output tests for the self-contained Bootstrap-layer mixins. These have no
|
||||
// dependency on project variables, so they are pulled in directly by path.
|
||||
// `output()` / `expect()` come from the `true` module (injected by the runner).
|
||||
|
||||
@use "../mixins/bootstrap/clearfix" as *;
|
||||
@use "../mixins/bootstrap/visually-hidden" as *;
|
||||
|
||||
@include describe("clearfix mixin") {
|
||||
@include it("emits a clearing ::after pseudo-element") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include clearfix();
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t::after {
|
||||
display: block;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("visually-hidden mixin") {
|
||||
@include it("hides the element while keeping it accessible") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include visually-hidden();
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
.t:not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
.t * {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Output tests for the Tabler mixins in scss/mixins/_mixins.scss.
|
||||
//
|
||||
// `@use "../mixins/mixins"` transitively pulls in the real settings/variables
|
||||
// chain (all relative to that file, so no extra load paths are needed), which
|
||||
// means the assertions run against real values — `$prefix` = "tblr-", the real
|
||||
// `$h5-font-size`, etc. — instead of fixtures.
|
||||
|
||||
@use "../mixins/mixins" as *;
|
||||
|
||||
@include describe("subheader mixin") {
|
||||
@include it("outputs the full subheader with colour and line-height") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include subheader();
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--tblr-font-weight-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1rem;
|
||||
color: var(--tblr-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("drops colour and line-height when the flags are false") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include subheader($include-color: false, $include-line-height: false);
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--tblr-font-weight-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("autodark-image mixin") {
|
||||
@include it("inverts the image via filter") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include autodark-image;
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("elements-list mixin") {
|
||||
@include it("sets up a wrapping flex list with the default gap") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include elements-list();
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
--tblr-list-gap: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--tblr-list-gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("honours a custom gap argument") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include elements-list(1rem);
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
--tblr-list-gap: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--tblr-list-gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("focus-ring mixin") {
|
||||
@include it("outputs the ring without a border by default") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include focus-ring();
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem color-mix(in srgb, var(--tblr-primary) 25%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("adds a border colour when requested") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include focus-ring($show-border: true);
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem color-mix(in srgb, var(--tblr-primary) 25%, transparent);
|
||||
border-color: color-mix(in srgb, var(--tblr-primary) 25%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("media-print mixin") {
|
||||
@include it("wraps content in a print media query") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include media-print {
|
||||
.x {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media print {
|
||||
.x {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("scrollbar mixin") {
|
||||
// The mixin resolves `&` to the current selector (or `*` at root); nested here,
|
||||
// every rule is prefixed with `.scroll`. Also covers the reduced-motion query
|
||||
// injected by the transition mixin.
|
||||
@include it("emits the webkit scrollbar rule set") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.scroll {
|
||||
@include scrollbar;
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
.scroll {
|
||||
scrollbar-color: color-mix(in srgb, var(--tblr-scrollbar-color, var(--tblr-body-color)) 20%, transparent) transparent;
|
||||
}
|
||||
.scroll::-webkit-scrollbar {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.scroll::-webkit-scrollbar {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.scroll::-webkit-scrollbar-thumb {
|
||||
border-radius: 1rem;
|
||||
border: 5px solid transparent;
|
||||
box-shadow: inset 0 0 0 1rem color-mix(in srgb, var(--tblr-scrollbar-color, var(--tblr-body-color)) 20%, transparent);
|
||||
}
|
||||
.scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.scroll:hover::-webkit-scrollbar-thumb {
|
||||
box-shadow: inset 0 0 0 1rem color-mix(in srgb, var(--tblr-scrollbar-color, var(--tblr-body-color)) 40%, transparent);
|
||||
}
|
||||
.scroll::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Tests for the RFS (responsive font sizing) helper functions in
|
||||
// scss/mixins/bootstrap/_rfs.scss. Both build their result via string
|
||||
// concatenation, so they return unquoted strings (compared as strings here).
|
||||
//
|
||||
// `rfs-value` gives the static value (px normalised to rem); `rfs-fluid-value`
|
||||
// gives the fluid value — unchanged below the base size, a responsive `calc()`
|
||||
// above it.
|
||||
|
||||
@use "../mixins/bootstrap/rfs" as *;
|
||||
|
||||
@include describe("rfs-value") {
|
||||
@include it("passes rem values through and keeps 0") {
|
||||
@include assert-equal(rfs-value(2rem), "2rem");
|
||||
@include assert-equal(rfs-value(0), "0");
|
||||
}
|
||||
|
||||
@include it("normalises px to rem") {
|
||||
@include assert-equal(rfs-value(20px), "1.25rem");
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("rfs-fluid-value") {
|
||||
@include it("leaves values at or below the base size unchanged") {
|
||||
@include assert-equal(rfs-fluid-value(1rem), "1rem");
|
||||
}
|
||||
|
||||
@include it("produces a responsive calc() above the base size") {
|
||||
@include assert-equal(rfs-fluid-value(3rem), "calc(1.425rem + 2.1vw)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Unit tests for the `str-replace($string, $search, $replace: '')` function
|
||||
// defined in scss/mixins/_functions.scss.
|
||||
//
|
||||
// The file is picked up automatically by scss.test.mjs — no manual list.
|
||||
// `describe`, `it` and `assert-equal` come from the `true` module, which the
|
||||
// runner injects for us.
|
||||
|
||||
@use "../mixins/functions" as *;
|
||||
|
||||
@include describe("str-replace") {
|
||||
@include it("replaces a single occurrence") {
|
||||
@include assert-equal(str-replace("a-b-c", "-", "_"), "a_b_c");
|
||||
}
|
||||
|
||||
@include it("replaces every occurrence, not just the first") {
|
||||
@include assert-equal(str-replace("a.b.c.d", ".", "/"), "a/b/c/d");
|
||||
}
|
||||
|
||||
@include it("returns the string unchanged when the search is not found") {
|
||||
@include assert-equal(str-replace("hello world", "x", "y"), "hello world");
|
||||
}
|
||||
|
||||
@include it("removes the search string when no replacement is given") {
|
||||
@include assert-equal(str-replace("1,000,000", ","), "1000000");
|
||||
}
|
||||
|
||||
@include it("handles a replacement longer than the search string") {
|
||||
@include assert-equal(str-replace("aaa", "a", "bb"), "bbbbbb");
|
||||
}
|
||||
|
||||
@include it("returns an empty string unchanged") {
|
||||
@include assert-equal(str-replace("", "-", "_"), "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Output tests for the `transition` mixin in scss/mixins/bootstrap/_transition.scss.
|
||||
// With `$enable-transitions` and `$enable-reduced-motion` both true (the
|
||||
// defaults), a real transition also emits a `prefers-reduced-motion` reset —
|
||||
// except when the value itself is `none`.
|
||||
|
||||
@use "../mixins/bootstrap/transition" as *;
|
||||
|
||||
@include describe("transition") {
|
||||
@include it("emits the transition plus a reduced-motion reset") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include transition(color 0.3s); } }
|
||||
@include expect() {
|
||||
.t {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.t {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("skips the reduced-motion reset when the value is none") {
|
||||
@include assert() {
|
||||
@include output() { .t { @include transition(none); } }
|
||||
@include expect() { .t { transition: none; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it } from 'vitest'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { runSass } from 'sass-true'
|
||||
|
||||
const dir = path.dirname(fileURLToPath(import.meta.url))
|
||||
// Auto-discover test files with recursive readdir (fs.globSync needs Node 22+,
|
||||
// while the package supports Node >=20).
|
||||
const files = readdirSync(dir, { recursive: true })
|
||||
.filter((f) => f.endsWith('.test.scss'))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f))
|
||||
|
||||
for (const file of files) {
|
||||
const data = readFileSync(file, 'utf8')
|
||||
// sass-true needs the "true" module; add it only when the file doesn't load it
|
||||
// itself. Anchored to the start of a line so a mention inside a comment can't
|
||||
// false-trigger and suppress the injection.
|
||||
const needsTrue = !/^\s*@(use|import)\s+["']true["']/m.test(data)
|
||||
const sass = (needsTrue ? '@use "true" as *;\n' : '') + data
|
||||
|
||||
runSass(
|
||||
{ describe, it, sourceType: 'string' },
|
||||
sass,
|
||||
{ loadPaths: [path.dirname(file)] }, // lets test files use relative @use / @import
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
// Standalone project for SCSS unit tests (sass-true).
|
||||
// Runs in the default Node environment — intentionally separate from the
|
||||
// browser-mode JS/TS config in vitest.config.mts so the two never overlap.
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['scss/tests/**/*.test.mjs'],
|
||||
},
|
||||
})
|
||||
Generated
+99
@@ -169,6 +169,12 @@ importers:
|
||||
plyr:
|
||||
specifier: ^3.8.4
|
||||
version: 3.8.4
|
||||
sass:
|
||||
specifier: 1.101.0
|
||||
version: 1.101.0
|
||||
sass-true:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0(sass@1.101.0)
|
||||
signature_pad:
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3
|
||||
@@ -275,6 +281,9 @@ packages:
|
||||
resolution: {integrity: sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@adobe/css-tools@4.5.0':
|
||||
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -792,6 +801,18 @@ packages:
|
||||
resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@jest/diff-sequences@30.4.0':
|
||||
resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/get-type@30.1.0':
|
||||
resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/schemas@30.4.1':
|
||||
resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -1137,6 +1158,9 @@ packages:
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@sinclair/typebox@0.34.52':
|
||||
resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==}
|
||||
|
||||
'@sindresorhus/slugify@2.2.1':
|
||||
resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1316,6 +1340,10 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2237,6 +2265,10 @@ packages:
|
||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
jest-diff@30.4.1:
|
||||
resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
js-beautify@2.0.3:
|
||||
resolution: {integrity: sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -2442,6 +2474,9 @@ packages:
|
||||
lodash.startcase@4.4.0:
|
||||
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
|
||||
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
lru-cache@11.5.1:
|
||||
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -2949,6 +2984,10 @@ packages:
|
||||
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pretty-format@30.4.1:
|
||||
resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
pretty-hrtime@1.0.3:
|
||||
resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -3008,6 +3047,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.2.7
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-is@19.2.7:
|
||||
resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==}
|
||||
|
||||
react@19.2.7:
|
||||
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3096,6 +3141,18 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sass-true@9.0.0:
|
||||
resolution: {integrity: sha512-p+ReCwIrk2/m85uQ3w1hGqiTFhqiY4Pc7juHcvDYvrVWy4DvVmCyLbkypdHWOFnT2OV5DpgqyNk2JN+zm4GUig==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
sass: '>=1.45.0'
|
||||
sass-embedded: '>=1.45.0'
|
||||
peerDependenciesMeta:
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
|
||||
sass@1.101.0:
|
||||
resolution: {integrity: sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
@@ -3808,6 +3865,8 @@ snapshots:
|
||||
minimatch: 3.1.5
|
||||
slash: 3.0.0
|
||||
|
||||
'@adobe/css-tools@4.5.0': {}
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
@@ -4320,6 +4379,14 @@ snapshots:
|
||||
|
||||
'@istanbuljs/schema@0.1.6': {}
|
||||
|
||||
'@jest/diff-sequences@30.4.0': {}
|
||||
|
||||
'@jest/get-type@30.1.0': {}
|
||||
|
||||
'@jest/schemas@30.4.1':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.34.52
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -4581,6 +4648,8 @@ snapshots:
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@sinclair/typebox@0.34.52': {}
|
||||
|
||||
'@sindresorhus/slugify@2.2.1':
|
||||
dependencies:
|
||||
'@sindresorhus/transliterate': 1.6.0
|
||||
@@ -4778,6 +4847,8 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -5692,6 +5763,13 @@ snapshots:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
jest-diff@30.4.1:
|
||||
dependencies:
|
||||
'@jest/diff-sequences': 30.4.0
|
||||
'@jest/get-type': 30.1.0
|
||||
chalk: 4.1.2
|
||||
pretty-format: 30.4.1
|
||||
|
||||
js-beautify@2.0.3:
|
||||
dependencies:
|
||||
config-chain: 1.1.13
|
||||
@@ -5851,6 +5929,8 @@ snapshots:
|
||||
|
||||
lodash.startcase@4.4.0: {}
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
lru-cache@11.5.1: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -6448,6 +6528,13 @@ snapshots:
|
||||
|
||||
pretty-bytes@5.6.0: {}
|
||||
|
||||
pretty-format@30.4.1:
|
||||
dependencies:
|
||||
'@jest/schemas': 30.4.1
|
||||
ansi-styles: 5.2.0
|
||||
react-is-18: react-is@18.3.1
|
||||
react-is-19: react-is@19.2.7
|
||||
|
||||
pretty-hrtime@1.0.3: {}
|
||||
|
||||
property-information@7.2.0: {}
|
||||
@@ -6492,6 +6579,10 @@ snapshots:
|
||||
react: 19.2.7
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-is@19.2.7: {}
|
||||
|
||||
react@19.2.7: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
@@ -6621,6 +6712,14 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass-true@9.0.0(sass@1.101.0):
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.5.0
|
||||
jest-diff: 30.4.1
|
||||
lodash: 4.18.1
|
||||
optionalDependencies:
|
||||
sass: 1.101.0
|
||||
|
||||
sass@1.101.0:
|
||||
dependencies:
|
||||
chokidar: 5.0.0
|
||||
|
||||
Reference in New Issue
Block a user