mirror of
https://github.com/twbs/bootstrap.git
synced 2025-08-12 16:44:17 +02:00
SCSS testing of the utilities API (#36029)
* Set up CSS testing using sass-true and mocha Use mocha to handle the heavy lifting of finding tests and running them. Mocha is made to look directly for SCSS files which are compiled thanks to Node's require.extensions mechanism. * Add CSS tests to workflow * Add tests for the generate-utility mixin * Add tests for utilities generation * Fix linting issues * Fix test contents Don't know why the whole utilities.test.scss ended up copied in the api.test.scss * Remove unnecessary entry in package.json * Move to Jasmine for running the tests * Move running of CSS tests before CSS build * Update linting set up Add exceptions for test files in stylelint * Remove irrelevant option for sass-true * Fix linting issues after rebase * Add color mode tests * Fix linter Co-authored-by: Mark Otto <markdotto@gmail.com>
This commit is contained in:
3
.github/workflows/css.yml
vendored
3
.github/workflows/css.yml
vendored
@@ -33,5 +33,8 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run CSS tests
|
||||
run: npm run css-test
|
||||
|
||||
- name: Build CSS
|
||||
run: npm run css
|
||||
|
11
.stylelintrc
11
.stylelintrc
@@ -27,5 +27,14 @@
|
||||
}
|
||||
],
|
||||
"scss/selector-no-union-class-name": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "scss/**/*.{test,spec}.scss",
|
||||
"rules": {
|
||||
"scss/dollar-variable-default": null,
|
||||
"declaration-no-important": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
3825
package-lock.json
generated
3825
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@
|
||||
"css-prefix-main": "postcss --config build/postcss.config.js --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"",
|
||||
"css-prefix-examples": "postcss --config build/postcss.config.js --replace \"site/content/**/*.css\"",
|
||||
"css-prefix-examples-rtl": "cross-env-shell NODE_ENV=RTL postcss --config build/postcss.config.js --dir \"site/content/docs/$npm_package_config_version_short/examples/\" --ext \".rtl.css\" --base \"site/content/docs/$npm_package_config_version_short/examples/\" \"site/content/docs/$npm_package_config_version_short/examples/{blog,carousel,dashboard,cheatsheet}/*.css\" \"!site/content/docs/$npm_package_config_version_short/examples/{blog,carousel,dashboard,cheatsheet}/*.rtl.css\"",
|
||||
"css-test": "jasmine --config=scss/tests/jasmine.js",
|
||||
"js": "npm-run-all js-compile js-minify",
|
||||
"js-compile": "npm-run-all --aggregate-output --parallel js-compile-*",
|
||||
"js-compile-standalone": "rollup --environment BUNDLE:false --config build/rollup.config.js --sourcemap",
|
||||
@@ -94,6 +95,7 @@
|
||||
"watch-css-main": "nodemon --watch scss/ --ext scss --exec \"npm-run-all css-lint css-compile css-prefix\"",
|
||||
"watch-css-dist": "nodemon --watch dist/css/ --ext css --ignore \"dist/css/*.rtl.*\" --exec \"npm run css-rtl\"",
|
||||
"watch-css-docs": "nodemon --watch site/assets/scss/ --ext scss --exec \"npm run css-lint\"",
|
||||
"watch-css-test": "nodemon --watch scss/ --ext scss,js --exec \"npm run css-test\"",
|
||||
"watch-js-main": "nodemon --watch js/src/ --ext js --exec \"npm-run-all js-lint js-compile\"",
|
||||
"watch-js-docs": "nodemon --watch site/assets/js/ --ext js --exec \"npm run js-lint\""
|
||||
},
|
||||
@@ -124,6 +126,7 @@
|
||||
"hammer-simulator": "0.0.1",
|
||||
"hugo-bin": "^0.96.0",
|
||||
"ip": "^2.0.0",
|
||||
"jasmine": "^4.0.2",
|
||||
"jquery": "^3.6.2",
|
||||
"karma": "^6.4.1",
|
||||
"karma-browserstack-launcher": "1.4.0",
|
||||
@@ -143,6 +146,7 @@
|
||||
"rollup-plugin-istanbul": "^4.0.0",
|
||||
"rtlcss": "^4.0.0",
|
||||
"sass": "^1.57.1",
|
||||
"sass-true": "^6.1.0",
|
||||
"shelljs": "^0.8.5",
|
||||
"stylelint": "^14.16.0",
|
||||
"stylelint-config-twbs-bootstrap": "^7.0.0",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// Utility generator
|
||||
// Used to generate utilities & print utilities
|
||||
@mixin generate-utility($utility, $infix, $is-rfs-media-query: false) {
|
||||
@mixin generate-utility($utility, $infix: "", $is-rfs-media-query: false) {
|
||||
$values: map-get($utility, values);
|
||||
|
||||
// If the values are a list or string, convert it into a map
|
||||
|
17
scss/tests/jasmine.js
Normal file
17
scss/tests/jasmine.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const path = require('node:path')
|
||||
|
||||
module.exports = {
|
||||
spec_dir: 'scss' /* eslint-disable-line camelcase */,
|
||||
|
||||
// Make Mocha look for `.test.scss` files
|
||||
spec_files: ['**/*.{test,spec}.scss'] /* eslint-disable-line camelcase */,
|
||||
|
||||
// Compile them into JS scripts running `sass-true`
|
||||
requires: [path.join(__dirname, 'sass-true/register')],
|
||||
|
||||
// Ensure we use `require` so that the require.extensions works
|
||||
// as `import` completely bypasses it
|
||||
jsLoader: 'require'
|
||||
}
|
69
scss/tests/mixins/_color-modes.test.scss
Normal file
69
scss/tests/mixins/_color-modes.test.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
// stylelint-disable selector-attribute-quotes
|
||||
|
||||
@import "../../functions";
|
||||
@import "../../variables";
|
||||
@import "../../variables-dark";
|
||||
@import "../../maps";
|
||||
@import "../../mixins";
|
||||
|
||||
@include describe("global $color-mode-type: data") {
|
||||
@include it("generates data attribute selectors for dark mode") {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include color-mode(dark) {
|
||||
.element {
|
||||
color: var(--bs-primary-text);
|
||||
background-color: var(--bs-primary-bg-subtle);
|
||||
}
|
||||
}
|
||||
@include color-mode(dark, true) {
|
||||
--custom-color: #{mix($indigo, $blue, 50%)};
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
[data-bs-theme=dark] .element {
|
||||
color: var(--bs-primary-text);
|
||||
background-color: var(--bs-primary-bg-subtle);
|
||||
}
|
||||
[data-bs-theme=dark] {
|
||||
--custom-color: #3a3ff8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("global $color-mode-type: media-query") {
|
||||
@include it("generates media queries for dark mode") {
|
||||
$color-mode-type: media-query !global;
|
||||
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include color-mode(dark) {
|
||||
.element {
|
||||
color: var(--bs-primary-text);
|
||||
background-color: var(--bs-primary-bg-subtle);
|
||||
}
|
||||
}
|
||||
@include color-mode(dark, true) {
|
||||
--custom-color: #{mix($indigo, $blue, 50%)};
|
||||
}
|
||||
}
|
||||
@include expect() {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.element {
|
||||
color: var(--bs-primary-text);
|
||||
background-color: var(--bs-primary-bg-subtle);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--custom-color: #3a3ff8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$color-mode-type: data !global;
|
||||
}
|
||||
}
|
393
scss/tests/mixins/_utilities.test.scss
Normal file
393
scss/tests/mixins/_utilities.test.scss
Normal file
@@ -0,0 +1,393 @@
|
||||
$prefix: bs-;
|
||||
$enable-important-utilities: false;
|
||||
|
||||
// Important: Do not import rfs to check that the mixin just calls the appropriate functions from it
|
||||
@import "../../mixins/utilities";
|
||||
|
||||
@mixin test-generate-utility($params...) {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@include generate-utility($params...);
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe(generate-utility) {
|
||||
@include it("generates a utility class for each value") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: "padding",
|
||||
values: (small: .5rem, large: 2rem)
|
||||
)
|
||||
) {
|
||||
.padding-small {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.padding-large {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("global $enable-important-utilities: true") {
|
||||
@include it("sets !important") {
|
||||
$enable-important-utilities: true !global;
|
||||
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: "padding",
|
||||
values: (small: .5rem, large: 2rem)
|
||||
)
|
||||
) {
|
||||
.padding-small {
|
||||
padding: .5rem !important;
|
||||
}
|
||||
|
||||
.padding-large {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
$enable-important-utilities: false !global;
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("$utility") {
|
||||
@include describe("values") {
|
||||
@include it("supports null keys") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: "padding",
|
||||
values: (null: 1rem, small: .5rem, large: 2rem)
|
||||
),
|
||||
) {
|
||||
.padding {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-small {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.padding-large {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("ignores null values") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: "padding",
|
||||
values: (small: null, large: 2rem)
|
||||
)
|
||||
) {
|
||||
.padding-large {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("supports lists") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: "padding",
|
||||
values: 1rem 2rem
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-2rem {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("supports single values") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
values: 1rem
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("class & property") {
|
||||
@include it("adds each property to the declaration") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
class: padding-x,
|
||||
property: padding-inline-start padding-inline-end,
|
||||
values: 1rem
|
||||
)
|
||||
) {
|
||||
.padding-x-1rem {
|
||||
padding-inline-start: 1rem;
|
||||
padding-inline-end: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("uses the first property as class name") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding-inline-start padding-inline-end,
|
||||
values: 1rem
|
||||
)
|
||||
) {
|
||||
.padding-inline-start-1rem {
|
||||
padding-inline-start: 1rem;
|
||||
padding-inline-end: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("supports a null class to create classes straight from the values") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: visibility,
|
||||
class: null,
|
||||
values: (
|
||||
visible: visible,
|
||||
invisible: hidden,
|
||||
)
|
||||
)
|
||||
) {
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("state") {
|
||||
@include it("Generates selectors for each states") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
values: 1rem,
|
||||
state: hover focus,
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-1rem-hover:hover {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-1rem-focus:focus {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("css-var"){
|
||||
@include it("sets a CSS variable instead of the property") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
css-variable-name: padding,
|
||||
css-var: true,
|
||||
values: 1rem 2rem
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
--bs-padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-2rem {
|
||||
--bs-padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("defaults to class") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
class: padding,
|
||||
css-var: true,
|
||||
values: 1rem 2rem
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
--bs-padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-2rem {
|
||||
--bs-padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("local-vars") {
|
||||
@include it("generates the listed variables") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: color,
|
||||
class: desaturated-color,
|
||||
local-vars: (
|
||||
color-opacity: 1,
|
||||
color-saturation: .25
|
||||
),
|
||||
values: (
|
||||
blue: hsla(192deg, var(--bs-color-saturation), 0, var(--bs-color-opacity))
|
||||
)
|
||||
)
|
||||
) {
|
||||
.desaturated-color-blue {
|
||||
--bs-color-opacity: 1;
|
||||
// Sass compilation will put a leading zero so we want to keep that one
|
||||
// stylelint-disable-next-line number-leading-zero
|
||||
--bs-color-saturation: 0.25;
|
||||
color: hsla(192deg, var(--bs-color-saturation), 0, var(--bs-color-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("css-var & state") {
|
||||
@include it("Generates a rule with for each state with a CSS variable") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
css-var: true,
|
||||
css-variable-name: padding,
|
||||
values: 1rem,
|
||||
state: hover focus,
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
--bs-padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-1rem-hover:hover {
|
||||
--bs-padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-1rem-focus:focus {
|
||||
--bs-padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("rtl") {
|
||||
@include it("sets up RTLCSS for removal when false") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
values: 1rem,
|
||||
rtl: false
|
||||
)
|
||||
) {
|
||||
/* rtl:begin:remove */
|
||||
|
||||
.padding-1rem {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* rtl:end:remove */
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("rfs") {
|
||||
@include it("sets the fluid value when not inside media query") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
values: 1rem,
|
||||
rfs: true
|
||||
)
|
||||
) {
|
||||
.padding-1rem {
|
||||
padding: rfs-fluid-value(1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("sets the value when inside the media query") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: padding,
|
||||
values: 1rem,
|
||||
rfs: true
|
||||
),
|
||||
$is-rfs-media-query: true
|
||||
) {
|
||||
.padding-1rem {
|
||||
padding: rfs-value(1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include describe("$infix") {
|
||||
@include it("inserts the given infix") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: "padding",
|
||||
values: (null: 1rem, small: .5rem, large: 2rem)
|
||||
),
|
||||
$infix: -sm
|
||||
) {
|
||||
.padding-sm {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.padding-sm-small {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.padding-sm-large {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include it("strips leading - if class is null") {
|
||||
@include test-generate-utility(
|
||||
(
|
||||
property: visibility,
|
||||
class: null,
|
||||
values: (
|
||||
visible: visible,
|
||||
invisible: hidden,
|
||||
)
|
||||
),
|
||||
-sm
|
||||
) {
|
||||
.sm-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.sm-invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
scss/tests/sass-true/register.js
Normal file
10
scss/tests/sass-true/register.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const runnerPath = require('node:path').join(__dirname, 'runner')
|
||||
|
||||
require.extensions['.scss'] = function (module, filename) {
|
||||
return module._compile(`
|
||||
const runner = require('${runnerPath}')
|
||||
runner('${filename}',{describe, it})
|
||||
`, filename)
|
||||
}
|
15
scss/tests/sass-true/runner.js
Normal file
15
scss/tests/sass-true/runner.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const { runSass } = require('sass-true')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
module.exports = function (filename, { describe, it }) {
|
||||
const data = fs.readFileSync(filename, { encoding: 'utf8' })
|
||||
const TRUE_SETUP = '$true-terminal-output: false; @import \'true\';'
|
||||
|
||||
runSass({
|
||||
data: TRUE_SETUP + data,
|
||||
includePaths: [path.dirname(filename)]
|
||||
}, { describe, it })
|
||||
}
|
75
scss/tests/utilities/_api.test.scss
Normal file
75
scss/tests/utilities/_api.test.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
@import "../../functions";
|
||||
@import "../../variables";
|
||||
@import "../../maps";
|
||||
@import "../../mixins";
|
||||
|
||||
$utilities: ();
|
||||
|
||||
@include describe("utilities/api") {
|
||||
@include it("generates utilities for each breakpoints") {
|
||||
$utilities: (
|
||||
margin: (
|
||||
property: margin,
|
||||
values: auto
|
||||
),
|
||||
padding: (
|
||||
property: padding,
|
||||
responsive: true,
|
||||
values: 1rem
|
||||
),
|
||||
font-size: (
|
||||
property: font-size,
|
||||
values: (large: 1.25rem),
|
||||
print: true
|
||||
)
|
||||
) !global;
|
||||
|
||||
$grid-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 576px,
|
||||
md: 768px
|
||||
);
|
||||
|
||||
|
||||
@include assert() {
|
||||
@include output() {
|
||||
@import "../../utilities/api";
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
// margin is not set to responsive
|
||||
.margin-auto {
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
// padding is, though
|
||||
.padding-1rem {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
.font-size-large {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.padding-sm-1rem {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.padding-md-1rem {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.font-size-print-large {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user