From 24a3935e1cc3fb9f8c277b498f8451a7da10f961 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Thu, 6 Aug 2026 14:50:43 -0500 Subject: [PATCH] test: add preset/css lockstep regression test --- tests/jest/lockstep.test.js | 109 ++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/jest/lockstep.test.js diff --git a/tests/jest/lockstep.test.js b/tests/jest/lockstep.test.js new file mode 100644 index 0000000..e8e8b7d --- /dev/null +++ b/tests/jest/lockstep.test.js @@ -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; + } + } + ); +} );