Skip to content

Commit 8c2ea4a

Browse files
committed
improve handling of selectors that include static classNames
e.g. & + &, & > &, & & but not &&, &&&, etc.
1 parent 6931b65 commit 8c2ea4a

File tree

1 file changed

+66
-27
lines changed

1 file changed

+66
-27
lines changed

src/toHaveStyleRule.js

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,126 @@
1-
const { getCSS, matcherTest, buildReturnMessage } = require('./utils');
1+
const { getCSS, matcherTest, buildReturnMessage } = require("./utils");
22

3-
const shouldDive = node => typeof node.dive === 'function' && typeof node.type() !== 'string';
3+
const shouldDive = node =>
4+
typeof node.dive === "function" && typeof node.type() !== "string";
45

5-
const isTagWithClassName = node => node.exists() && node.prop('className') && typeof node.type() === 'string';
6+
const isTagWithClassName = node =>
7+
node.exists() && node.prop("className") && typeof node.type() === "string";
68

79
const getClassNames = received => {
810
let className;
911

1012
if (received) {
11-
if (received.$$typeof === Symbol.for('react.test.json')) {
13+
if (received.$$typeof === Symbol.for("react.test.json")) {
1214
className = received.props.className || received.props.class;
13-
} else if (typeof received.exists === 'function' && received.exists()) {
15+
} else if (typeof received.exists === "function" && received.exists()) {
1416
const tree = shouldDive(received) ? received.dive() : received;
1517
const components = tree.findWhere(isTagWithClassName);
1618
if (components.length) {
17-
className = components.first().prop('className');
19+
className = components.first().prop("className");
1820
}
1921
} else if (global.Element && received instanceof global.Element) {
20-
className = Array.from(received.classList).join(' ');
22+
className = Array.from(received.classList).join(" ");
2123
}
2224
}
2325

2426
return className ? className.split(/\s/) : [];
2527
};
2628

27-
const hasAtRule = options => Object.keys(options).some(option => ['media', 'supports'].includes(option));
29+
const hasAtRule = options =>
30+
Object.keys(options).some(option => ["media", "supports"].includes(option));
2831

2932
const getAtRules = (ast, options) => {
3033
const mediaRegex = /(\([a-z-]+:)\s?([a-z0-9.]+\))/g;
3134

3235
return Object.keys(options)
3336
.map(option =>
3437
ast.stylesheet.rules
35-
.filter(rule => rule.type === option && rule[option] === options[option].replace(mediaRegex, '$1$2'))
38+
.filter(
39+
rule =>
40+
rule.type === option &&
41+
rule[option] === options[option].replace(mediaRegex, "$1$2")
42+
)
3643
.map(rule => rule.rules)
3744
.reduce((acc, rules) => acc.concat(rules), [])
3845
)
3946
.reduce((acc, rules) => acc.concat(rules), []);
4047
};
4148

42-
const getModifiedClassName = (className, modifier = '') => {
49+
const getModifiedClassName = (className, staticClassName, modifier = "") => {
4350
const classNameSelector = `.${className}`;
44-
let prefix = '';
51+
let prefix = "";
4552

4653
modifier = modifier.trim();
47-
if (modifier.includes('&')) {
48-
modifier = modifier.replace(/&/g, classNameSelector);
54+
if (modifier.includes("&")) {
55+
modifier = modifier
56+
// & combined with other selectors and not a precedence boost should be replaced with the static className, but the first instance should be the dynamic className
57+
.replace(/(&[^&]+?)&/g, `$1.${staticClassName}`)
58+
.replace(/&/g, classNameSelector);
4959
} else {
5060
prefix += classNameSelector;
5161
}
5262
const first = modifier[0];
53-
if (first !== ':' && first !== '[') {
54-
prefix += ' ';
63+
if (first !== ":" && first !== "[") {
64+
prefix += " ";
5565
}
5666

5767
return `${prefix}${modifier}`.trim();
5868
};
5969

60-
const hasClassNames = (classNames, selectors, options) =>
61-
classNames.some(className => selectors.includes(getModifiedClassName(className, options.modifier)));
70+
const hasClassNames = (classNames, selectors, options) => {
71+
const staticClassNames = classNames.filter(x => x.startsWith("sc-"));
72+
73+
return classNames.some(className =>
74+
staticClassNames.some(staticClassName =>
75+
selectors.includes(
76+
getModifiedClassName(
77+
className,
78+
staticClassName,
79+
options.modifier
80+
).replace(/['"]/g, '"')
81+
)
82+
)
83+
);
84+
};
6285

6386
const getRules = (ast, classNames, options) => {
64-
const rules = hasAtRule(options) ? getAtRules(ast, options) : ast.stylesheet.rules;
65-
66-
return rules.filter(rule => rule.type === 'rule' && hasClassNames(classNames, rule.selectors, options));
87+
const rules = hasAtRule(options)
88+
? getAtRules(ast, options)
89+
: ast.stylesheet.rules;
90+
91+
return rules.filter(
92+
rule =>
93+
rule.type === "rule" && hasClassNames(classNames, rule.selectors, options)
94+
);
6795
};
6896

6997
const handleMissingRules = options => ({
7098
pass: false,
7199
message: () =>
72100
`No style rules found on passed Component${
73-
Object.keys(options).length ? ` using options:\n${JSON.stringify(options)}` : ''
74-
}`,
101+
Object.keys(options).length
102+
? ` using options:\n${JSON.stringify(options)}`
103+
: ""
104+
}`
75105
});
76106

77107
const getDeclaration = (rule, property) =>
78108
rule.declarations
79-
.filter(declaration => declaration.type === 'declaration' && declaration.property === property)
109+
.filter(
110+
declaration =>
111+
declaration.type === "declaration" && declaration.property === property
112+
)
80113
.pop();
81114

82-
const getDeclarations = (rules, property) => rules.map(rule => getDeclaration(rule, property)).filter(Boolean);
115+
const getDeclarations = (rules, property) =>
116+
rules.map(rule => getDeclaration(rule, property)).filter(Boolean);
83117

84118
const normalizeOptions = options =>
85119
options.modifier
86120
? Object.assign({}, options, {
87-
modifier: Array.isArray(options.modifier) ? options.modifier.join('') : options.modifier,
121+
modifier: Array.isArray(options.modifier)
122+
? options.modifier.join("")
123+
: options.modifier
88124
})
89125
: options;
90126

@@ -101,11 +137,14 @@ function toHaveStyleRule(component, property, expected, options = {}) {
101137
const declarations = getDeclarations(rules, property);
102138
const declaration = declarations.pop() || {};
103139
const received = declaration.value;
104-
const pass = !received && !expected && this.isNot ? false : matcherTest(received, expected);
140+
const pass =
141+
!received && !expected && this.isNot
142+
? false
143+
: matcherTest(received, expected);
105144

106145
return {
107146
pass,
108-
message: buildReturnMessage(this.utils, pass, property, received, expected),
147+
message: buildReturnMessage(this.utils, pass, property, received, expected)
109148
};
110149
}
111150

0 commit comments

Comments
 (0)