1
0
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:
Paweł Kuna
2026-07-21 21:13:57 +02:00
committed by GitHub
parent b5d0caee42
commit a373e17c8e
29 changed files with 1492 additions and 7 deletions
+5
View File
@@ -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
+2 -2
View File
@@ -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
+33
View File
@@ -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
View File
@@ -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",
+48
View File
@@ -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%)");
}
}
+33
View File
@@ -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)");
}
}
+37
View File
@@ -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; } }
}
}
}
+44
View File
@@ -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");
}
}
+94
View File
@@ -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;
}
}
}
}
}
+24
View File
@@ -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);
}
}
+26
View File
@@ -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;
}
}
}
}
}
+44
View File
@@ -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%));
}
}
+79
View File
@@ -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%)
);
}
}
+43
View File
@@ -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)"
);
}
}
+45
View File
@@ -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);
}
}
+21
View File
@@ -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"
);
}
}
+142
View File
@@ -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; }
}
}
}
}
}
+93
View File
@@ -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);
}
}
}
}
}
+38
View File
@@ -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)
);
}
}
+139
View File
@@ -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;
}
}
}
}
}
+203
View File
@@ -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;
}
}
}
}
}
+30
View File
@@ -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)");
}
}
+34
View File
@@ -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("", "-", "_"), "");
}
}
+31
View File
@@ -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; } }
}
}
}
+28
View File
@@ -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
)
}
+10
View File
@@ -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'],
},
})
+99
View File
@@ -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