@@ -57,6 +57,7 @@ export type Context = {
5757 currentAssertion ?: Assertion ;
5858 currentOutputRules ?: Rule [ ] ;
5959 currentExpectedRules ?: Rule [ ] ;
60+ currentExpectedStrings ?: string [ ] ;
6061} ;
6162
6263export type Rule = CssComment | CssRule | CssAtRule ;
@@ -152,6 +153,25 @@ export const formatFailureMessage = function (assertion: Assertion) {
152153 if ( assertion . details ) {
153154 msg = `${ msg } -- ${ assertion . details } ` ;
154155 }
156+
157+ // For contains-string assertions with multiple strings, show which ones are missing
158+ if ( assertion . assertionType === 'contains-string' && assertion . expected ) {
159+ const expectedStrings = assertion . expected . split ( '\n' ) ;
160+ if ( expectedStrings . length > 1 ) {
161+ const output = assertion . output || '' ;
162+ const missing = expectedStrings . filter ( ( str ) => ! output . includes ( str ) ) ;
163+ if ( missing . length > 0 ) {
164+ msg = `${ msg } \n\nExpected output to contain all of the following strings:\n` ;
165+ expectedStrings . forEach ( ( str ) => {
166+ const found = output . includes ( str ) ;
167+ msg = `${ msg } ${ found ? '✓' : '✗' } "${ str } "\n` ;
168+ } ) ;
169+ msg = `${ msg } \nActual output:\n${ output } \n` ;
170+ return msg ;
171+ }
172+ }
173+ }
174+
155175 msg = `${ msg } \n\n${ diffStringsUnified (
156176 assertion . expected || '' ,
157177 assertion . output || '' ,
@@ -550,6 +570,10 @@ export const parse = function (
550570 }
551571 if ( text === constants . CONTAINS_STRING_START_TOKEN ) {
552572 ctx . currentExpectedRules = [ ] ;
573+ // Initialize array for multiple contains-string assertions
574+ if ( ! ctx . currentExpectedStrings ) {
575+ ctx . currentExpectedStrings = [ ] ;
576+ }
553577 return parseAssertionContainsString ;
554578 }
555579 throw parseError (
@@ -633,29 +657,65 @@ export const parse = function (
633657 isCommentNode ( rule ) &&
634658 rule . text . trim ( ) === constants . CONTAINS_STRING_END_TOKEN
635659 ) {
636- if ( ctx . currentAssertion ) {
637- // The string to find is wrapped in a Sass comment because it might not
638- // always be a complete, valid CSS block on its own. These replace calls
639- // are necessary to strip the leading `/*` and trailing `*/` characters
640- // that enclose the string, so we're left with just the raw string to
641- // find for accurate comparison.
642- ctx . currentAssertion . expected = generateCss (
643- ctx . currentExpectedRules || [ ] ,
644- )
645- . replace ( new RegExp ( '^/\\*' ) , '' )
646- . replace ( new RegExp ( '\\*/$' ) , '' )
647- . trim ( ) ;
648- ctx . currentAssertion . passed = ctx . currentAssertion . output ?. includes (
649- ctx . currentAssertion . expected ,
650- ) ;
651- ctx . currentAssertion . assertionType = 'contains-string' ;
652- }
660+ // The string to find is wrapped in a Sass comment because it might not
661+ // always be a complete, valid CSS block on its own. These replace calls
662+ // are necessary to strip the leading `/*` and trailing `*/` characters
663+ // that enclose the string, so we're left with just the raw string to
664+ // find for accurate comparison.
665+ const expectedString = generateCss ( ctx . currentExpectedRules || [ ] )
666+ . replace ( new RegExp ( '^/\\*' ) , '' )
667+ . replace ( new RegExp ( '\\*/$' ) , '' )
668+ . trim ( ) ;
669+
670+ // Add this string to the array of expected strings
671+ ctx . currentExpectedStrings ?. push ( expectedString ) ;
672+
653673 delete ctx . currentExpectedRules ;
654- return parseEndAssertion ;
674+ return parseAssertionContainsStringEnd ;
655675 }
656676 ctx . currentExpectedRules ?. push ( rule ) ;
657677 return parseAssertionContainsString ;
658678 } ;
659679
680+ const parseAssertionContainsStringEnd : Parser = function ( rule , ctx ) {
681+ if ( isCommentNode ( rule ) ) {
682+ const text = rule . text . trim ( ) ;
683+ if ( ! text ) {
684+ return parseAssertionContainsStringEnd ;
685+ }
686+ // Check for another CONTAINS_STRING block
687+ if ( text === constants . CONTAINS_STRING_START_TOKEN ) {
688+ ctx . currentExpectedRules = [ ] ;
689+ return parseAssertionContainsString ;
690+ }
691+ // Check for END_ASSERT - finalize the assertion
692+ if ( text === constants . ASSERT_END_TOKEN ) {
693+ if ( ctx . currentAssertion && ctx . currentExpectedStrings ) {
694+ // Check if all expected strings are found in the output
695+ const allFound = ctx . currentExpectedStrings . every ( ( str ) =>
696+ ctx . currentAssertion ?. output ?. includes ( str ) ,
697+ ) ;
698+ ctx . currentAssertion . passed = allFound ;
699+ ctx . currentAssertion . assertionType = 'contains-string' ;
700+ // Store all expected strings joined with newlines for display
701+ ctx . currentAssertion . expected = ctx . currentExpectedStrings . join ( '\n' ) ;
702+ }
703+ delete ctx . currentExpectedStrings ;
704+ finishCurrentAssertion ( ctx ) ;
705+ return parseAssertion ;
706+ }
707+ throw parseError (
708+ `Unexpected comment "${ text } "` ,
709+ 'CONTAINS_STRING or END_ASSERT' ,
710+ rule . source ?. start ,
711+ ) ;
712+ }
713+ throw parseError (
714+ `Unexpected rule type "${ rule . type } "` ,
715+ 'CONTAINS_STRING or END_ASSERT' ,
716+ rule . source ?. start ,
717+ ) ;
718+ } ;
719+
660720 return parseCss ( ) ;
661721} ;
0 commit comments