Skip to content

Commit

Permalink
improve handling of selectors that include static classNames
Browse files Browse the repository at this point in the history
e.g.

& + &, & > &, & &

but not

&&, &&&, etc.
  • Loading branch information
quantizor committed Nov 12, 2019
1 parent 6931b65 commit 8c2ea4a
Showing 1 changed file with 66 additions and 27 deletions.
93 changes: 66 additions & 27 deletions src/toHaveStyleRule.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,126 @@
const { getCSS, matcherTest, buildReturnMessage } = require('./utils');
const { getCSS, matcherTest, buildReturnMessage } = require("./utils");

const shouldDive = node => typeof node.dive === 'function' && typeof node.type() !== 'string';
const shouldDive = node =>
typeof node.dive === "function" && typeof node.type() !== "string";

const isTagWithClassName = node => node.exists() && node.prop('className') && typeof node.type() === 'string';
const isTagWithClassName = node =>
node.exists() && node.prop("className") && typeof node.type() === "string";

const getClassNames = received => {
let className;

if (received) {
if (received.$$typeof === Symbol.for('react.test.json')) {
if (received.$$typeof === Symbol.for("react.test.json")) {
className = received.props.className || received.props.class;
} else if (typeof received.exists === 'function' && received.exists()) {
} else if (typeof received.exists === "function" && received.exists()) {
const tree = shouldDive(received) ? received.dive() : received;
const components = tree.findWhere(isTagWithClassName);
if (components.length) {
className = components.first().prop('className');
className = components.first().prop("className");
}
} else if (global.Element && received instanceof global.Element) {
className = Array.from(received.classList).join(' ');
className = Array.from(received.classList).join(" ");
}
}

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

const hasAtRule = options => Object.keys(options).some(option => ['media', 'supports'].includes(option));
const hasAtRule = options =>
Object.keys(options).some(option => ["media", "supports"].includes(option));

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

return Object.keys(options)
.map(option =>
ast.stylesheet.rules
.filter(rule => rule.type === option && rule[option] === options[option].replace(mediaRegex, '$1$2'))
.filter(
rule =>
rule.type === option &&
rule[option] === options[option].replace(mediaRegex, "$1$2")
)
.map(rule => rule.rules)
.reduce((acc, rules) => acc.concat(rules), [])
)
.reduce((acc, rules) => acc.concat(rules), []);
};

const getModifiedClassName = (className, modifier = '') => {
const getModifiedClassName = (className, staticClassName, modifier = "") => {
const classNameSelector = `.${className}`;
let prefix = '';
let prefix = "";

modifier = modifier.trim();
if (modifier.includes('&')) {
modifier = modifier.replace(/&/g, classNameSelector);
if (modifier.includes("&")) {
modifier = modifier
// & 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
.replace(/(&[^&]+?)&/g, `$1.${staticClassName}`)
.replace(/&/g, classNameSelector);
} else {
prefix += classNameSelector;
}
const first = modifier[0];
if (first !== ':' && first !== '[') {
prefix += ' ';
if (first !== ":" && first !== "[") {
prefix += " ";
}

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

const hasClassNames = (classNames, selectors, options) =>
classNames.some(className => selectors.includes(getModifiedClassName(className, options.modifier)));
const hasClassNames = (classNames, selectors, options) => {
const staticClassNames = classNames.filter(x => x.startsWith("sc-"));

return classNames.some(className =>
staticClassNames.some(staticClassName =>
selectors.includes(
getModifiedClassName(
className,
staticClassName,
options.modifier
).replace(/['"]/g, '"')
)
)
);
};

const getRules = (ast, classNames, options) => {
const rules = hasAtRule(options) ? getAtRules(ast, options) : ast.stylesheet.rules;

return rules.filter(rule => rule.type === 'rule' && hasClassNames(classNames, rule.selectors, options));
const rules = hasAtRule(options)
? getAtRules(ast, options)
: ast.stylesheet.rules;

return rules.filter(
rule =>
rule.type === "rule" && hasClassNames(classNames, rule.selectors, options)
);
};

const handleMissingRules = options => ({
pass: false,
message: () =>
`No style rules found on passed Component${
Object.keys(options).length ? ` using options:\n${JSON.stringify(options)}` : ''
}`,
Object.keys(options).length
? ` using options:\n${JSON.stringify(options)}`
: ""
}`
});

const getDeclaration = (rule, property) =>
rule.declarations
.filter(declaration => declaration.type === 'declaration' && declaration.property === property)
.filter(
declaration =>
declaration.type === "declaration" && declaration.property === property
)
.pop();

const getDeclarations = (rules, property) => rules.map(rule => getDeclaration(rule, property)).filter(Boolean);
const getDeclarations = (rules, property) =>
rules.map(rule => getDeclaration(rule, property)).filter(Boolean);

const normalizeOptions = options =>
options.modifier
? Object.assign({}, options, {
modifier: Array.isArray(options.modifier) ? options.modifier.join('') : options.modifier,
modifier: Array.isArray(options.modifier)
? options.modifier.join("")
: options.modifier
})
: options;

Expand All @@ -101,11 +137,14 @@ function toHaveStyleRule(component, property, expected, options = {}) {
const declarations = getDeclarations(rules, property);
const declaration = declarations.pop() || {};
const received = declaration.value;
const pass = !received && !expected && this.isNot ? false : matcherTest(received, expected);
const pass =
!received && !expected && this.isNot
? false
: matcherTest(received, expected);

return {
pass,
message: buildReturnMessage(this.utils, pass, property, received, expected),
message: buildReturnMessage(this.utils, pass, property, received, expected)
};
}

Expand Down

0 comments on commit 8c2ea4a

Please sign in to comment.