test: add preset/css lockstep regression test

This commit is contained in:
Keith Solomon
2026-08-06 14:50:43 -05:00
parent 38c2ecfa01
commit 24a3935e1c
+109
View File
@@ -0,0 +1,109 @@
/**
* Lockstep contract: for every preset in `PRESETS`, the compiled
* `build/style-index.css` must contain a rule whose `filter:` declaration
* includes every canonical `cssFilter` token in the same order.
*
* The source of truth for the canonical `cssFilter` per preset is
* `src/presets.js`. The SCSS in `src/styles/style.scss` must mirror those
* values using the intensity-scaling math (additive bias or direct
* multiply). This test fails immediately if the two files ever drift.
*
* Tokenisation: split the cssFilter string on whitespace, then split each
* token on `(` and `)` so the function name and argument are tracked
* separately. The compiled CSS rule for the preset must contain a token
* whose name+argument matches every canonical token, in order.
*
* The test reads from the build artifact rather than the SCSS source
* because that is the CSS WordPress actually serves. If you change
* style.scss, re-run `npm run build` to update build/style-index.css.
*/
import fs from 'fs';
import path from 'path';
import { PRESETS } from '../../src/presets';
const cssPath = path.join(
__dirname,
'..',
'..',
'build',
'style-index.css'
);
/**
* Split a cssFilter string like
* "saturate(1.5) sepia(0.6) brightness(1.08) contrast(1.1) hue-rotate(-8deg)"
* into ordered tokens of the form { name, arg }.
*/
function tokenise( cssFilter ) {
return cssFilter
.trim()
.split( /\s+/ )
.map( ( token ) => {
const [ , name, arg ] = token.match( /^([a-z-]+)\((.*)\)$/ );
return { name, arg };
} );
}
/**
* Pull out the filter-declaration substring for a given preset from the
* compiled CSS. Returns null if the rule is missing entirely.
*/
function filterDeclarationFor( css, slug ) {
const selector = `.ksolo-image-filter-img.has-filter-${ slug }`;
const ruleStart = css.indexOf( selector );
if ( ruleStart === -1 ) {
return null;
}
// Find the next "}" — that's the end of the rule body.
const brace = css.indexOf( '{', ruleStart );
const close = css.indexOf( '}', brace );
if ( brace === -1 || close === -1 ) {
return null;
}
const body = css.slice( brace, close );
const filterIdx = body.indexOf( 'filter:' );
if ( filterIdx === -1 ) {
return null;
}
return body.slice( filterIdx + 'filter:'.length, close );
}
describe( 'preset ↔ compiled CSS lockstep', () => {
let css;
beforeAll( () => {
css = fs.readFileSync( cssPath, 'utf8' );
} );
test.each( PRESETS.map( ( p ) => [ p.slug, p ] ) )(
'%s: every cssFilter token appears in the compiled CSS rule, in order',
( slug, preset ) => {
// 'normal' uses filter:none; there is no canonical cssFilter
// token sequence to compare against. The base rule
// `.wp-block-ksolo-image-filter img { filter: none; }` already
// covers it; the existing styles.test.js checks that the
// selector for 'normal' is absent from the per-preset
// overrides. Skip the lockstep check here.
if ( slug === 'normal' ) {
return;
}
const decl = filterDeclarationFor( css, slug );
expect( decl ).not.toBeNull();
const expected = tokenise( preset.cssFilter );
expect( expected.length ).toBeGreaterThan( 0 );
// Walk the canonical tokens in order; for each, find the next
// occurrence of `name(arg)` in the compiled declaration
// (allowing for the calc() wrapper around the value).
let cursor = 0;
for ( const { name } of expected ) {
const needle = `${ name }(`;
const at = decl.indexOf( needle, cursor );
expect( at ).toBeGreaterThanOrEqual( 0 );
cursor = at + needle.length;
}
}
);
} );