Skip to content

Commit 60da5f0

Browse files
authored
Merge pull request #311 from DT3264/ContainsString
Add support for substring matching within output assertion
2 parents b5a06bb + 4764a69 commit 60da5f0

File tree

7 files changed

+212
-0
lines changed

7 files changed

+212
-0
lines changed

sass/assert/_output.scss

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,39 @@
166166
@mixin contains($selector: true) {
167167
@include utils.content('contains', $selector) { @content; }
168168
}
169+
170+
171+
// Contains String
172+
// ---------------
173+
/// Describe a case-sensitive substring expected to be found within the paired `output()` block.
174+
/// The `contains-string()` mixin requires a string argument,
175+
/// and should be nested inside the `assert()` mixin
176+
/// along with a single `output()` block.
177+
/// Assertions are used inside the `test()` mixin
178+
/// to define the expected results of the test.
179+
/// - These mixins together describe a comparison on output CSS,
180+
///   checking if the compiled CSS-results of the `output()` mixin
181+
///   contain the specified `$string-to-find`.
182+
/// - When using Mocha/Jest integration, the output comparison is automated –
183+
///   otherwise you will have to compare the output manually.
184+
///   Using `git diff` is a great way to watch for changes in output.
185+
///
186+
/// @group api-assert-output
187+
///
188+
/// @param {string} $string-to-find -
189+
///   The substring to search for within the compiled CSS output.
190+
///
191+
/// @example scss -
192+
///   @include true.test('Can find partial strings') {
193+
///     @include true.assert {
194+
///       @include true.output {
195+
///         font-size: 1em;
196+
///         line-height: 1.5;
197+
///       }
198+
///       @include true.contains-string('font-size');
199+
///       @include true.contains-string('line');
200+
///     }
201+
///   }
202+
@mixin contains-string($string-to-find) {
203+
@include utils.content-string('contains-string', $string-to-find);
204+
}

sass/assert/_utils.scss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,38 @@
4141
@include config.message(' END_#{$block} ', 'comments');
4242
}
4343

44+
// Content String
45+
// --------------
46+
/// Prepares and formats the output for string-based assertions,
47+
/// wrapping the target string with appropriate CSS comments.
48+
/// This mixin is used internally by assertion mixins like `contains-string`.
49+
///
50+
/// @access private
51+
/// @group assert-utils
52+
///
53+
/// @param {string} $type -
54+
///   The type of content being output (e.g., `contains-string`).
55+
/// @param {string} $string-to-find -
56+
///   The string that will be searched for in the compiled CSS output.
57+
/// @param {string} $description [null] -
58+
///   An optional description for the assertion being tested.
59+
///
60+
/// @output - The `$string-to-find` wrapped in relevant CSS comments.
61+
@mixin content-string($type, $string-to-find, $description: null) {
62+
@include data.output-context($type);
63+
64+
$block: map.get(config.$output, $type);
65+
$start: if(
66+
$description or ($block == 'ASSERT'),
67+
'#{$block}: #{$description}',
68+
$block
69+
);
70+
71+
@include config.message(' #{$start} ', 'comments');
72+
@include config.message('#{$string-to-find}', 'comments');
73+
@include config.message(' END_#{$block} ', 'comments');
74+
}
75+
4476
// Setup
4577
// -----
4678
/// Setup the proper context for value assertions before testing

sass/config/_terms.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ $output: (
88
'output': 'OUTPUT',
99
'expect': 'EXPECTED',
1010
'contains': 'CONTAINED',
11+
'contains-string': 'CONTAINS_STRING'
1112
);

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const OUTPUT_START_TOKEN = 'OUTPUT';
1818
export const OUTPUT_END_TOKEN = 'END_OUTPUT';
1919
export const EXPECTED_START_TOKEN = 'EXPECTED';
2020
export const EXPECTED_END_TOKEN = 'END_EXPECTED';
21+
export const CONTAINS_STRING_START_TOKEN = 'CONTAINS_STRING';
22+
export const CONTAINS_STRING_END_TOKEN = 'END_CONTAINS_STRING';
2123
export const CONTAINED_START_TOKEN = 'CONTAINED';
2224
export const CONTAINED_END_TOKEN = 'END_CONTAINED';
2325
export const ASSERT_END_TOKEN = 'END_ASSERT';

src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,10 @@ export const parse = function (
550550
ctx.currentExpectedRules = [];
551551
return parseAssertionContained;
552552
}
553+
if(text === constants.CONTAINS_STRING_START_TOKEN){
554+
ctx.currentExpectedRules = [];
555+
return parseAssertionContainsString;
556+
}
553557
throw parseError(
554558
`Unexpected comment "${text}"`,
555559
'EXPECTED',
@@ -629,5 +633,28 @@ export const parse = function (
629633
return parseAssertionContained;
630634
};
631635

636+
const parseAssertionContainsString: Parser = function (rule, ctx) {
637+
if (isCommentNode(rule)) {
638+
if (rule.comment?.trim() === constants.CONTAINS_STRING_END_TOKEN) {
639+
/* istanbul ignore else */
640+
if (ctx.currentAssertion) {
641+
// The string to find is wrapped in a Sass comment because it might not always be a complete, valid CSS block on its own.
642+
// These replace calls are necessary to strip the leading `/*` and trailing `*/` characters that enclose the string,
643+
// so we're left with just the raw string to find for accurate comparison.
644+
ctx.currentAssertion.expected = cssStringify({
645+
type: CssTypes.stylesheet,
646+
stylesheet: { rules: ctx.currentExpectedRules || [] },
647+
}).replace(new RegExp('^/\\*'), '').replace(new RegExp('\\*/$'), '').trim();
648+
ctx.currentAssertion.passed = ctx.currentAssertion.output?.includes(ctx.currentAssertion.expected);
649+
ctx.currentAssertion.assertionType = 'contains-string';
650+
}
651+
delete ctx.currentExpectedRules;
652+
return parseEndAssertion;
653+
}
654+
}
655+
ctx.currentExpectedRules?.push(rule);
656+
return parseAssertionContainsString;
657+
};
658+
632659
return parseCss();
633660
};

test/main.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,4 +1174,87 @@ describe('#parse', () => {
11741174
expect(sassTrue.parse(css)).to.deep.equal(expected);
11751175
});
11761176
});
1177+
describe('#contains-string', () => {
1178+
it('parses a passing output test', () => {
1179+
const css = [
1180+
'/* # Module: Contains-string */',
1181+
'/* Test: CSS output contains-string */',
1182+
'/* ASSERT: Output selector pattern contains-string input pattern */',
1183+
'/* */',
1184+
'/* OUTPUT */',
1185+
'.test-output {',
1186+
' height: 10px;',
1187+
' width: 20px; }',
1188+
'/* END_OUTPUT */',
1189+
'/* */',
1190+
'/* CONTAINS_STRING */',
1191+
'/* height */',
1192+
'/* END_CONTAINS_STRING */',
1193+
'/* */',
1194+
'/* END_ASSERT */',
1195+
].join('\n');
1196+
const expected = [
1197+
{
1198+
module: 'Contains-string',
1199+
tests: [
1200+
{
1201+
test: 'CSS output contains-string',
1202+
assertions: [
1203+
{
1204+
description:
1205+
'Output selector pattern contains-string input pattern',
1206+
assertionType: 'contains-string',
1207+
passed: true,
1208+
output: '.test-output {\n height: 10px;\n width: 20px;\n}',
1209+
expected: 'height',
1210+
},
1211+
],
1212+
},
1213+
],
1214+
},
1215+
];
1216+
expect(sassTrue.parse(css)).to.deep.equal(expected);
1217+
});
1218+
1219+
it('parses a failing output test', () => {
1220+
const css = [
1221+
'/* # Module: Contains-string */',
1222+
'/* Test: CSS output contains-string */',
1223+
'/* ASSERT: Output selector pattern contains-string input pattern */',
1224+
'/* */',
1225+
'/* OUTPUT */',
1226+
'.test-output {',
1227+
' height: 10px;',
1228+
' width: 20px; }',
1229+
'/* END_OUTPUT */',
1230+
'/* */',
1231+
'/* CONTAINS_STRING */',
1232+
'/* background-color */',
1233+
'/* END_CONTAINS_STRING */',
1234+
'/* */',
1235+
'/* END_ASSERT */',
1236+
].join('\n');
1237+
const expected = [
1238+
{
1239+
module: 'Contains-string',
1240+
tests: [
1241+
{
1242+
test: 'CSS output contains-string',
1243+
assertions: [
1244+
{
1245+
description:
1246+
'Output selector pattern contains-string input pattern',
1247+
assertionType: 'contains-string',
1248+
passed: false,
1249+
output: '.test-output {\n height: 10px;\n width: 20px;\n}',
1250+
expected: 'background-color',
1251+
},
1252+
],
1253+
},
1254+
],
1255+
},
1256+
];
1257+
expect(sassTrue.parse(css)).to.deep.equal(expected);
1258+
});
1259+
});
11771260
});

test/scss/assert/_output.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,37 @@
4949
}
5050
}
5151

52+
@include describe('Output Contains-string') {
53+
@include it('Contains sub-strings') {
54+
@include assert {
55+
@include output {
56+
height: 10px;
57+
width: 20px;
58+
}
59+
60+
@include contains-string('height');
61+
}
62+
}
63+
@include it('Contains properties') {
64+
@include assert {
65+
@include output {
66+
--my-custom-property: 3rem;
67+
}
68+
69+
@include contains-string('--my-custom-property');
70+
}
71+
}
72+
@include it('Contains values') {
73+
@include assert {
74+
@include output {
75+
font-family: Helvetica;
76+
}
77+
78+
@include contains-string('Helvetica');
79+
}
80+
}
81+
}
82+
5283
@include describe('Output Contains') {
5384
@include it('Contains sub-string') {
5485
@include assert {

0 commit comments

Comments
 (0)