import type { Media , StyleRules } from 'css' ; import CSS from 'css' ; import type { MatchFilter } from './types' ; type Arg = [ { [ key ] : string ; } , MatchFilter ? ] ; const DEFAULT_MATCH_FILTER = { media : undefined , target : undefined } ; const kebabCase = ( str ) => str . replace ( /([a-z])([A-Z])/g , '$1-$2' ) . replace ( /\s+/g , '-' ) . toLowerCase ( ) ; const removeSpaces = ( str ? ) => str && str . replace ( /\s/g , '' ) ; const mapProperties = ( properties ) => Object . keys ( properties ) . map ( ( property ) => `${ kebabCase ( property ) }:${ properties [ property ] }` ) ; const onlyRules = ( rules ? ) => rules ?. filter ( ( r ) => r . type === 'rule' ) ; const findMediaRules = ( allRules = [ ] , media ) : Media [ 'rules' ] | undefined => { const rules = [ ] ; for ( const rule of allRules ) { if ( ! rule ) { continue ; } if ( 'media' in rule ) { if ( removeSpaces ( rule . media ) === removeSpaces ( media ) && 'rules' in rule && rule . rules ) { rules . push ( ... rule . rules ) ; } else if ( 'rules' in rule ) { const found = findMediaRules ( rule . rules , media ) ; found && rules . push ( ... found ) ; } } } return rules ; } ; const getRules = ( ast , filter , className ) => { const { media , target } = filter ; const getAllRules = ( ) => { if ( media ) { return onlyRules ( findMediaRules ( ast . stylesheet ?. rules , media ) ) ; } return ast . stylesheet ?. rules . filter ( ( r ) => ( r . type = 'rule' ) ) ; } ; const allRules = getAllRules ( ) ; const klass = target ? `.${ className }${ target }` : `.${ className }` ; return allRules ?. filter ( ( r ) => { if ( 'selectors' in r ) { return r . selectors ?. find ( ( s ) => removeSpaces ( s ) === removeSpaces ( klass ) ) ; } return ; } ) ; } ; const findStylesInRules = ( styles , rules ) => { const found = [ ] ; if ( ! rules ) return found ; styles . forEach ( ( s ) => { rules ?. forEach ( ( r ) => { if ( 'declarations' in r ) { r . declarations ?. forEach ( ( d ) => { if ( 'property' in d ) { if ( s === `${ d . property }:${ d . value }` ) found . push ( s ) ; } } ) ; } } ) ; } ) ; return found ; } ; export function toHaveCompiledCss ( this , element , ... args ) { const [ property , value , matchFilter = DEFAULT_MATCH_FILTER ] = args ; const properties = typeof property === 'string' ? { [ property ] : value } : property ; const styleElements = [ ... Array . from ( document . body . querySelectorAll ( 'style' ) ) , ... Array . from ( document . head . querySelectorAll ( 'style' ) ) , ] ; if ( ! styleElements ) { return { pass : false , message : ( ) => 'pairing style element was not found' , } ; } const stylesToFind = mapProperties ( properties ) ; const foundStyles = [ ] ; const classNames = element . className . split ( ' ' ) ; for ( const styleElement of styleElements ) { let css = styleElement . textContent || '' ; const styles = element [ Object . keys ( element ) [ 0 ] ] . memoizedProps . style ; if ( styles && Object . keys ( styles ) . length > 0 ) { Object . entries ( styles ) . forEach ( ( [ key , value ] ) => { css = css . split ( `var(${ key })` ) . join ( value ) ; } ) ; } const ast = CSS . parse ( css ) ; classNames . forEach ( ( c ) => { const rules = getRules ( ast , matchFilter , c ) ; foundStyles . push ( ... findStylesInRules ( stylesToFind , rules ) ) ; } ) ; } const notFoundStyles = stylesToFind . filter ( ( style ) => ! foundStyles . includes ( style ) ) ; const foundFormatted = stylesToFind . join ( ', ' ) ; const notFoundFormatted = notFoundStyles . join ( ', ' ) ; if ( foundStyles . length > 0 && notFoundStyles . length === 0 ) { return { pass : true , message : ! this . isNot ? ( ) => '' : ( ) => `Found "${ foundFormatted }" on ${ element . outerHTML } element.` , } ; } return { pass : false , message : ( ) => `Could not find "${ notFoundFormatted }" on ${ element . outerHTML } element.` , } ; }