Skip to content

Commit fd63d2f

Browse files
committed
Allow multiple contains-string in a single assertion.
1 parent fa26919 commit fd63d2f

File tree

3 files changed

+303
-24
lines changed

3 files changed

+303
-24
lines changed

src/index.ts

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type Context = {
5757
currentAssertion?: Assertion;
5858
currentOutputRules?: Rule[];
5959
currentExpectedRules?: Rule[];
60+
currentExpectedStrings?: string[];
6061
};
6162

6263
export 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
};

test/css/test.css

Lines changed: 32 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)