@stylable/core-test-kit
Advanced tools
Comparing version 4.10.0 to 4.10.1
@@ -17,2 +17,15 @@ import type { Position } from 'postcss'; | ||
} | ||
export declare function matchDiagnostic(type: `analyze` | `transform`, meta: Pick<StylableMeta, `diagnostics` | `transformDiagnostics`>, expected: { | ||
label?: string; | ||
message: string; | ||
severity: string; | ||
location: Location; | ||
}, errors: { | ||
diagnosticsNotFound: (type: string, message: string, label?: string) => string; | ||
unsupportedSeverity: (type: string, severity: string, label?: string) => string; | ||
locationMismatch: (type: string, message: string, label?: string) => string; | ||
wordMismatch: (type: string, expectedWord: string, message: string, label?: string) => string; | ||
severityMismatch: (type: string, expectedSeverity: string, actualSeverity: string, message: string, label?: string) => string; | ||
expectedNotFound: (type: string, message: string, label?: string) => string; | ||
}): string; | ||
export declare function findTestLocations(css: string): { | ||
@@ -19,0 +32,0 @@ start: Position | undefined; |
@@ -6,3 +6,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.shouldReportNoDiagnostics = exports.expectTransformDiagnostics = exports.expectAnalyzeDiagnostics = exports.findTestLocations = void 0; | ||
exports.shouldReportNoDiagnostics = exports.expectTransformDiagnostics = exports.expectAnalyzeDiagnostics = exports.findTestLocations = exports.matchDiagnostic = void 0; | ||
const chai_1 = require("chai"); | ||
@@ -12,2 +12,65 @@ const deindent_1 = __importDefault(require("deindent")); | ||
const generate_test_util_1 = require("./generate-test-util"); | ||
const createMatchDiagnosticState = () => ({ | ||
matches: 0, | ||
location: ``, | ||
word: ``, | ||
severity: ``, | ||
}); | ||
const isSupportedSeverity = (val) => !!val.match(/info|warn|error/); | ||
function matchDiagnostic(type, meta, expected, errors) { | ||
const diagnostics = type === `analyze` ? meta.diagnostics : meta.transformDiagnostics; | ||
if (!diagnostics) { | ||
return errors.diagnosticsNotFound(type, expected.message, expected.label); | ||
} | ||
const expectedSeverity = expected.severity === `warn` ? `warning` : expected.severity || ``; | ||
if (!isSupportedSeverity(expectedSeverity)) { | ||
return errors.unsupportedSeverity(type, expected.severity || ``, expected.label); | ||
} | ||
let closestMatchState = createMatchDiagnosticState(); | ||
const foundPartialMatch = (newState) => { | ||
if (newState.matches >= closestMatchState.matches) { | ||
closestMatchState = newState; | ||
} | ||
}; | ||
for (const report of diagnostics.reports.values()) { | ||
const matchState = createMatchDiagnosticState(); | ||
if (report.message !== expected.message) { | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
// if (!expected.skipLocationCheck) { | ||
// ToDo: test all range | ||
if (report.node.source.start.offset !== expected.location.start.offset) { | ||
matchState.location = errors.locationMismatch(type, expected.message, expected.label); | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
// } | ||
if (expected.location.word) { | ||
if (report.options.word !== expected.location.word) { | ||
matchState.word = errors.wordMismatch(type, expected.location.word, expected.message, expected.label); | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
} | ||
if (expected.severity) { | ||
if (report.type !== expectedSeverity) { | ||
matchState.location = errors.severityMismatch(type, expectedSeverity, report.type, expected.message, expected.label); | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
} | ||
// expected matched! | ||
return ``; | ||
} | ||
return (closestMatchState.location || | ||
closestMatchState.word || | ||
closestMatchState.severity || | ||
errors.expectedNotFound(type, expected.message, expected.label)); | ||
} | ||
exports.matchDiagnostic = matchDiagnostic; | ||
function findTestLocations(css) { | ||
@@ -14,0 +77,0 @@ let line = 1; |
@@ -0,2 +1,6 @@ | ||
import type { StylableMeta } from '@stylable/core'; | ||
import type * as postcss from 'postcss'; | ||
interface Context { | ||
meta: Pick<StylableMeta, 'outputAst' | 'rawAst' | 'diagnostics' | 'transformDiagnostics'>; | ||
} | ||
/** | ||
@@ -34,13 +38,26 @@ * Test transformed stylesheets inline expectation comments | ||
*/ | ||
export declare function testInlineExpects(result: postcss.Root, expectedTestsCount?: number): void; | ||
export declare function testInlineExpects(result: postcss.Root | Context, expectedTestInput?: number): void; | ||
export declare const testInlineExpectsErrors: { | ||
matchAmount: (expectedAmount: number, actualAmount: number) => string; | ||
unsupportedNode: (testType: string, nodeType: string, label?: string) => string; | ||
selector: (expectedSelector: string, actualSelector: string, label?: string) => string; | ||
declarations: (expectedDecl: string, actualDecl: string, selector: string, label?: string) => string; | ||
unfoundMixin: (expectInput: string) => string; | ||
malformedDecl: (decl: string, expectInput: string) => string; | ||
unsupportedMixinNode: (type: string) => string; | ||
ruleMalformedDecl: (decl: string, expectInput: string) => string; | ||
atruleParams: (expectedParams: string, actualParams: string, label?: string) => string; | ||
atRuleMultiTest: (comment: string) => string; | ||
decl: (expected: string, actual: string, label?: string) => string; | ||
declMalformed: (expectedProp: string, expectedLabel: string, label?: string) => string; | ||
deprecatedRootInputNotSupported: (expectation: string) => string; | ||
diagnosticsMalformed: (type: string, expectation: string, label?: string) => string; | ||
diagnosticsNotFound: (type: string, message: string, label?: string) => string; | ||
diagnosticsUnsupportedSeverity: (type: string, severity: string, label?: string) => string; | ||
diagnosticsLocationMismatch: (type: string, message: string, label?: string) => string; | ||
diagnosticsWordMismatch: (type: string, expectedWord: string, message: string, label?: string) => string; | ||
diagnosticsSeverityMismatch: (type: string, expectedSeverity: string, actualSeverity: string, message: string, label?: string) => string; | ||
diagnosticExpectedNotFound: (type: string, message: string, label?: string) => string; | ||
combine: (errors: string[]) => string; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=inline-expectation.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.testInlineExpectsErrors = exports.testInlineExpects = void 0; | ||
const diagnostics_1 = require("./diagnostics"); | ||
const tests = { | ||
'@check': checkTest, | ||
'@rule': ruleTest, | ||
'@atrule': atRuleTest, | ||
'@decl': declTest, | ||
'@analyze': analyzeTest, | ||
'@transform': transformTest, | ||
}; | ||
const testScopes = Object.keys(tests); | ||
const testScopesRegex = () => testScopes.join(`|`); | ||
const isRoot = (val) => val.type === `root`; | ||
/** | ||
@@ -36,60 +48,67 @@ * Test transformed stylesheets inline expectation comments | ||
*/ | ||
function testInlineExpects(result, expectedTestsCount = result.toString().match(/@check/gm).length) { | ||
if (expectedTestsCount === 0) { | ||
throw new Error('no tests found try to add @check comments before any selector'); | ||
} | ||
function testInlineExpects(result, expectedTestInput) { | ||
var _a; | ||
// backward compatibility (no diagnostic checks) | ||
const isDeprecatedInput = isRoot(result); | ||
const context = isDeprecatedInput | ||
? { | ||
meta: { | ||
outputAst: result, | ||
rawAst: null, | ||
diagnostics: null, | ||
transformDiagnostics: null, | ||
}, | ||
} | ||
: result; | ||
// ToDo: support analyze mode | ||
const rootAst = context.meta.outputAst; | ||
const expectedTestAmount = expectedTestInput !== null && expectedTestInput !== void 0 ? expectedTestInput : (((_a = rootAst.toString().match(new RegExp(`${testScopesRegex()}`, `gm`))) === null || _a === void 0 ? void 0 : _a.length) || 0); | ||
const checks = []; | ||
const errors = []; | ||
// collect checks | ||
result.walkComments((comment) => { | ||
const checksInput = comment.text.split(`@check`); | ||
const rule = comment.next(); | ||
if (checksInput.length > 1 && rule) { | ||
if (rule.type === `rule`) { | ||
for (const checkInput of checksInput) { | ||
if (checkInput.trim()) { | ||
const check = createRuleCheck(rule, checkInput, errors); | ||
if (check) { | ||
checks.push(check); | ||
rootAst.walkComments((comment) => { | ||
const input = comment.text.split(/@/gm); | ||
const testCommentTarget = comment; | ||
const testCommentSrc = isDeprecatedInput | ||
? comment | ||
: getSourceComment(context.meta, comment) || comment; | ||
const nodeTarget = testCommentTarget.next(); | ||
const nodeSrc = testCommentSrc.next(); | ||
if (nodeTarget || nodeSrc) { | ||
while (input.length) { | ||
const next = `@` + input.shift(); | ||
const testMatch = next.match(new RegExp(`^(${testScopesRegex()})`, `g`)); | ||
if (testMatch) { | ||
const testScope = testMatch[0]; | ||
let testInput = next.replace(testScope, ``); | ||
// collect expectation inner `@` fragments | ||
while (input.length && | ||
!(`@` + input[0]).match(new RegExp(`^(${testScopesRegex()})`, `g`))) { | ||
testInput += `@` + input.shift(); | ||
} | ||
if (testInput) { | ||
if (isDeprecatedInput && | ||
(testScope === `@analyze` || testScope === `@transform`)) { | ||
// not possible with just AST root | ||
const result = { | ||
type: testScope, | ||
expectation: testInput.trim(), | ||
errors: [ | ||
exports.testInlineExpectsErrors.deprecatedRootInputNotSupported(testScope + testInput), | ||
], | ||
}; | ||
errors.push(...result.errors); | ||
checks.push(result); | ||
} | ||
else { | ||
const result = tests[testScope](context, testInput.trim(), nodeTarget, nodeSrc); | ||
result.type = testScope; | ||
errors.push(...result.errors); | ||
checks.push(result); | ||
} | ||
} | ||
} | ||
} | ||
if (rule.type === `atrule`) { | ||
if (checksInput.length > 2) { | ||
errors.push(exports.testInlineExpectsErrors.atRuleMultiTest(comment.text)); | ||
} | ||
const check = createAtRuleCheck(rule, checksInput[1]); | ||
if (check) { | ||
checks.push(check); | ||
} | ||
} | ||
} | ||
}); | ||
// check | ||
checks.forEach((check) => { | ||
if (check.kind === `rule`) { | ||
const { msg, rule, expectedSelector, expectedDeclarations, declarationCheck } = check; | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (rule.selector !== expectedSelector) { | ||
errors.push(exports.testInlineExpectsErrors.selector(expectedSelector, rule.selector, prefix)); | ||
} | ||
if (declarationCheck === `full`) { | ||
const actualDecl = rule.nodes.map((x) => x.toString()).join(`; `); | ||
const expectedDecl = expectedDeclarations | ||
.map(([prop, value]) => `${prop}: ${value}`) | ||
.join(`; `); | ||
if (actualDecl !== expectedDecl) { | ||
errors.push(exports.testInlineExpectsErrors.declarations(expectedDecl, actualDecl, rule.selector, prefix)); | ||
} | ||
} | ||
} | ||
else if (check.kind === `atrule`) { | ||
const { msg, rule, expectedParams } = check; | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (rule.params !== expectedParams) { | ||
errors.push(exports.testInlineExpectsErrors.atruleParams(expectedParams, rule.params, prefix)); | ||
} | ||
} | ||
}); | ||
// report errors | ||
@@ -99,74 +118,246 @@ if (errors.length) { | ||
} | ||
if (expectedTestsCount !== checks.length) { | ||
throw new Error(exports.testInlineExpectsErrors.matchAmount(expectedTestsCount, checks.length)); | ||
if (expectedTestAmount !== checks.length) { | ||
throw new Error(exports.testInlineExpectsErrors.matchAmount(expectedTestAmount, checks.length)); | ||
} | ||
} | ||
exports.testInlineExpects = testInlineExpects; | ||
function createRuleCheck(rule, expectInput, errors) { | ||
function checkTest(context, expectation, targetNode, srcNode) { | ||
const type = targetNode === null || targetNode === void 0 ? void 0 : targetNode.type; | ||
switch (type) { | ||
case `rule`: { | ||
return tests[`@rule`](context, expectation, targetNode, srcNode); | ||
} | ||
case `atrule`: { | ||
return tests[`@atrule`](context, expectation, targetNode, srcNode); | ||
} | ||
default: | ||
return { | ||
type: `@check`, | ||
expectation, | ||
errors: [exports.testInlineExpectsErrors.unsupportedNode(`@check`, type)], | ||
}; | ||
} | ||
} | ||
function ruleTest(context, expectation, targetNode, _srcNode) { | ||
var _a; | ||
const { msg, ruleIndex, expectedSelector, expectedBody } = expectInput.match(/(?<msg>\(.*\))*(\[(?<ruleIndex>\d+)\])*(?<expectedSelector>[^{}]*)\s*(?<expectedBody>.*)/s).groups; | ||
const targetRule = ruleIndex ? getNextMixinRule(rule, Number(ruleIndex)) : rule; | ||
if (!targetRule) { | ||
errors.push(exports.testInlineExpectsErrors.unfoundMixin(expectInput)); | ||
return; | ||
const result = { | ||
type: `@rule`, | ||
expectation, | ||
errors: [], | ||
}; | ||
const { msg, ruleIndex, expectedSelector, expectedBody } = expectation.match(/(?<msg>\(.*\))*(\[(?<ruleIndex>\d+)\])*(?<expectedSelector>[^{}]*)\s*(?<expectedBody>.*)/s).groups; | ||
let testNode = targetNode; | ||
// get mixed-in rule | ||
if (ruleIndex) { | ||
if ((targetNode === null || targetNode === void 0 ? void 0 : targetNode.type) !== `rule`) { | ||
result.errors.push(`mixed-in expectation is only supported for CSS Rule, not ${targetNode === null || targetNode === void 0 ? void 0 : targetNode.type}`); | ||
return result; | ||
} | ||
else { | ||
const actualTarget = getNextMixinRule(targetNode, Number(ruleIndex)); | ||
if (!actualTarget) { | ||
result.errors.push(exports.testInlineExpectsErrors.unfoundMixin(expectation)); | ||
return result; | ||
} | ||
testNode = actualTarget; | ||
} | ||
} | ||
const expectedDeclarations = []; | ||
const declsInput = expectedBody.trim().match(/^{(.*)}$/s); | ||
const declarationCheck = declsInput ? `full` : `none`; | ||
if (declsInput && ((_a = declsInput[1]) === null || _a === void 0 ? void 0 : _a.includes(`:`))) { | ||
for (const decl of declsInput[1].split(`;`)) { | ||
if (decl.trim() !== ``) { | ||
const [prop, value] = decl.split(':'); | ||
if (prop && value) { | ||
expectedDeclarations.push([prop.trim(), value.trim()]); | ||
// test by target node type | ||
const nodeType = testNode === null || testNode === void 0 ? void 0 : testNode.type; | ||
if (nodeType === `rule`) { | ||
const expectedDeclarations = []; | ||
const declsInput = expectedBody.trim().match(/^{(.*)}$/s); | ||
const declarationCheck = declsInput ? `full` : `none`; | ||
if (declsInput && ((_a = declsInput[1]) === null || _a === void 0 ? void 0 : _a.includes(`:`))) { | ||
for (const decl of declsInput[1].split(`;`)) { | ||
if (decl.trim() !== ``) { | ||
const [prop, value] = decl.split(':'); | ||
if (prop && value) { | ||
expectedDeclarations.push([prop.trim(), value.trim()]); | ||
} | ||
else { | ||
result.errors.push(exports.testInlineExpectsErrors.ruleMalformedDecl(decl, expectation)); | ||
} | ||
} | ||
else { | ||
errors.push(exports.testInlineExpectsErrors.malformedDecl(decl, expectInput)); | ||
} | ||
} | ||
} | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (testNode.selector !== expectedSelector.trim()) { | ||
result.errors.push(exports.testInlineExpectsErrors.selector(expectedSelector.trim(), testNode.selector, prefix)); | ||
} | ||
if (declarationCheck === `full`) { | ||
const actualDecl = testNode.nodes.map((x) => x.toString()).join(`; `); | ||
const expectedDecl = expectedDeclarations | ||
.map(([prop, value]) => `${prop}: ${value}`) | ||
.join(`; `); | ||
if (actualDecl !== expectedDecl) { | ||
result.errors.push(exports.testInlineExpectsErrors.declarations(expectedDecl, actualDecl, testNode.selector, prefix)); | ||
} | ||
} | ||
} | ||
return { | ||
kind: `rule`, | ||
msg, | ||
rule: targetRule, | ||
expectedSelector: expectedSelector.trim(), | ||
expectedDeclarations, | ||
declarationCheck, | ||
else if (nodeType === `atrule`) { | ||
// passing null to srcNode as atruleTest doesn't actually requires it. | ||
// if it would at some point, then its just a matter of searching the rawAst for it. | ||
return atRuleTest(context, expectation.replace(`[${ruleIndex}]`, ``), testNode, null); | ||
} | ||
else { | ||
// unsupported mixed-in node test | ||
result.errors.push(exports.testInlineExpectsErrors.unsupportedMixinNode(testNode.type)); | ||
} | ||
return result; | ||
} | ||
function atRuleTest(_context, expectation, targetNode, _srcNode) { | ||
const result = { | ||
type: `@atrule`, | ||
expectation, | ||
errors: [], | ||
}; | ||
const { msg, expectedParams } = expectation.match(/(?<msg>\([^)]*\))*(?<expectedParams>.*)/) | ||
.groups; | ||
if (expectedParams.match(/^\[\d+\]/)) { | ||
result.errors.push(exports.testInlineExpectsErrors.atRuleMultiTest(expectation)); | ||
return result; | ||
} | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (targetNode.type === `atrule`) { | ||
if (targetNode.params !== expectedParams.trim()) { | ||
result.errors.push(exports.testInlineExpectsErrors.atruleParams(expectedParams.trim(), targetNode.params, prefix)); | ||
} | ||
} | ||
else { | ||
result.errors.push(exports.testInlineExpectsErrors.unsupportedNode(`@atrule`, targetNode.type)); | ||
} | ||
return result; | ||
} | ||
function createAtRuleCheck(rule, expectInput) { | ||
const { msg, expectedParams } = expectInput.match(/(?<msg>\([^)]*\))*(?<expectedParams>.*)/) | ||
.groups; | ||
return { | ||
kind: `atrule`, | ||
msg, | ||
rule, | ||
expectedParams: expectedParams.trim(), | ||
function declTest(_context, expectation, targetNode, _srcNode) { | ||
const result = { | ||
type: `@decl`, | ||
expectation, | ||
errors: [], | ||
}; | ||
let { label, prop, value } = expectation.match(/(?<label>\([^)]*\))*(?<prop>[^:]*)\s*:?\s*(?<value>.*)/).groups; | ||
label = label ? label + `: ` : ``; | ||
prop = prop.trim(); | ||
value = value.trim(); | ||
if (!prop || !value) { | ||
result.errors.push(exports.testInlineExpectsErrors.declMalformed(prop, value, label)); | ||
} | ||
else if (targetNode.type === `decl`) { | ||
if (targetNode.prop !== prop.trim() || targetNode.value !== value) { | ||
const expected = prop.trim() + `: ` + value.trim(); | ||
const actual = targetNode.prop + `: ` + targetNode.value; | ||
result.errors.push(exports.testInlineExpectsErrors.decl(expected, actual, label)); | ||
} | ||
} | ||
else { | ||
result.errors.push(exports.testInlineExpectsErrors.unsupportedNode(`@decl`, targetNode.type, label)); | ||
} | ||
return result; | ||
} | ||
function getNextMixinRule(currentRule, count) { | ||
while (currentRule && count > 0) { | ||
const next = currentRule.next(); | ||
// next must be a rule sense mixin can only add rules | ||
if ((next === null || next === void 0 ? void 0 : next.type) === `rule`) { | ||
currentRule = next; | ||
function analyzeTest(context, expectation, targetNode, srcNode) { | ||
return diagnosticTest(`analyze`, context, expectation, targetNode, srcNode); | ||
} | ||
function transformTest(context, expectation, targetNode, srcNode) { | ||
return diagnosticTest(`transform`, context, expectation, targetNode, srcNode); | ||
} | ||
function diagnosticTest(type, { meta }, expectation, _targetNode, srcNode) { | ||
var _a, _b; | ||
const result = { | ||
type: `@${type}`, | ||
expectation, | ||
errors: [], | ||
}; | ||
const matchResult = expectation.match(/-(?<severity>\w+)(?<label>\([^)]*\))?\s?(?:word\((?<word>[^)]*)\))?\s?(?<message>.*)/); | ||
if (!matchResult) { | ||
result.errors.push(exports.testInlineExpectsErrors.diagnosticsMalformed(type, expectation)); | ||
return result; | ||
} | ||
let { label, severity, message, word } = matchResult.groups; | ||
label = label ? label + `: ` : ``; | ||
severity = (severity === null || severity === void 0 ? void 0 : severity.trim()) || ``; | ||
message = (message === null || message === void 0 ? void 0 : message.trim()) || ``; | ||
word = (word === null || word === void 0 ? void 0 : word.trim()) || ``; | ||
if (!message) { | ||
result.errors.push(exports.testInlineExpectsErrors.diagnosticsMalformed(type, expectation, label)); | ||
return result; | ||
} | ||
// check for diagnostic | ||
const error = (0, diagnostics_1.matchDiagnostic)(type, meta, { | ||
label, | ||
message, | ||
severity, | ||
location: { | ||
start: (_a = srcNode.source) === null || _a === void 0 ? void 0 : _a.start, | ||
end: (_b = srcNode.source) === null || _b === void 0 ? void 0 : _b.end, | ||
word, | ||
css: ``, | ||
}, | ||
}, { | ||
diagnosticsNotFound: exports.testInlineExpectsErrors.diagnosticsNotFound, | ||
unsupportedSeverity: exports.testInlineExpectsErrors.diagnosticsUnsupportedSeverity, | ||
locationMismatch: exports.testInlineExpectsErrors.diagnosticsLocationMismatch, | ||
wordMismatch: exports.testInlineExpectsErrors.diagnosticsWordMismatch, | ||
severityMismatch: exports.testInlineExpectsErrors.diagnosticsSeverityMismatch, | ||
expectedNotFound: exports.testInlineExpectsErrors.diagnosticExpectedNotFound, | ||
}); | ||
if (error) { | ||
result.errors.push(error); | ||
} | ||
return result; | ||
} | ||
function getSourceComment(meta, { source }) { | ||
let match = undefined; | ||
meta.rawAst.walkComments((srcComment) => { | ||
var _a, _b, _c, _d, _e, _f; | ||
if (((_b = (_a = srcComment.source) === null || _a === void 0 ? void 0 : _a.start) === null || _b === void 0 ? void 0 : _b.offset) === ((_c = source === null || source === void 0 ? void 0 : source.start) === null || _c === void 0 ? void 0 : _c.offset) && | ||
((_e = (_d = srcComment.source) === null || _d === void 0 ? void 0 : _d.end) === null || _e === void 0 ? void 0 : _e.offset) === ((_f = source === null || source === void 0 ? void 0 : source.end) === null || _f === void 0 ? void 0 : _f.offset)) { | ||
match = srcComment; | ||
return false; | ||
} | ||
return; | ||
}); | ||
return match; | ||
} | ||
function getNextMixinRule(originRule, count) { | ||
let current = originRule; | ||
while (current && count > 0) { | ||
current = current.next(); | ||
if ((current === null || current === void 0 ? void 0 : current.type) !== `comment`) { | ||
count--; | ||
} | ||
else { | ||
return; | ||
} | ||
} | ||
return currentRule && count === 0 ? currentRule : undefined; | ||
return current && count === 0 ? current : undefined; | ||
} | ||
exports.testInlineExpectsErrors = { | ||
matchAmount: (expectedAmount, actualAmount) => `Expected ${expectedAmount} checks to run but there was ${actualAmount}`, | ||
selector: (expectedSelector, actualSelector, label = ``) => `${label}expected ${actualSelector} to transform to ${expectedSelector}`, | ||
matchAmount: (expectedAmount, actualAmount) => `Expected "${expectedAmount}" checks to run but "${actualAmount}" were found`, | ||
unsupportedNode: (testType, nodeType, label = ``) => `${label}unsupported type "${testType}" for "${nodeType}"`, | ||
selector: (expectedSelector, actualSelector, label = ``) => `${label}expected "${actualSelector}" to transform to "${expectedSelector}"`, | ||
declarations: (expectedDecl, actualDecl, selector, label = ``) => `${label}expected ${selector} to have declaration {${expectedDecl}}, but got {${actualDecl}}`, | ||
unfoundMixin: (expectInput) => `cannot locate mixed-in rule for "${expectInput}"`, | ||
malformedDecl: (decl, expectInput) => `error in expectation "${decl}" of "${expectInput}"`, | ||
atruleParams: (expectedParams, actualParams, label = ``) => `${label}expected ${actualParams} to transform to ${expectedParams}`, | ||
atRuleMultiTest: (comment) => `atrule multi test is not supported (${comment})`, | ||
unsupportedMixinNode: (type) => `unsupported mixin expectation of type "${type}"`, | ||
ruleMalformedDecl: (decl, expectInput) => `error in expectation "${decl}" of "${expectInput}"`, | ||
atruleParams: (expectedParams, actualParams, label = ``) => `${label}expected "${actualParams}" to transform to ${expectedParams}`, | ||
atRuleMultiTest: (comment) => `atrule mixin is not supported: (${comment})`, | ||
decl: (expected, actual, label = ``) => `${label}expected "${actual}" to transform to "${expected}"`, | ||
declMalformed: (expectedProp, expectedLabel, label = ``) => { | ||
if (!expectedProp && !expectedLabel) { | ||
return `${label}malformed declaration expectation, format should be: "prop: value"`; | ||
} | ||
else if (!expectedProp) { | ||
return `${label}malformed declaration expectation missing prop: "???: ${expectedLabel}"`; | ||
} | ||
else { | ||
return `${label}malformed declaration expectation missing value: "${expectedProp}: ???"`; | ||
} | ||
}, | ||
deprecatedRootInputNotSupported: (expectation) => `"${expectation}" is not supported for with the used input, try calling testInlineExpects(generateStylableResults())`, | ||
diagnosticsMalformed: (type, expectation, label = ``) => `${label}malformed @${type} expectation "@${type}${expectation}". format should be: "@${type}-[severity] diagnostic message"`, | ||
diagnosticsNotFound: (type, message, label = ``) => `${label}${type} diagnostics not found for "${message}"`, | ||
diagnosticsUnsupportedSeverity: (type, severity, label = ``) => `${label}unsupported @${type}-[severity]: "${severity}"`, | ||
diagnosticsLocationMismatch: (type, message, label = ``) => `${label}expected "@${type}-[severity] "${message}" to be reported in this location, but got it somewhere else`, | ||
diagnosticsWordMismatch: (type, expectedWord, message, label = ``) => `${label}expected word in "@${type}-[severity] word(${expectedWord}) ${message}" was not found`, | ||
diagnosticsSeverityMismatch: (type, expectedSeverity, actualSeverity, message, label = ``) => `${label}expected ${type} diagnostic "${message}" to be reported with "${expectedSeverity}", but it was reported with "${actualSeverity}"`, | ||
diagnosticExpectedNotFound: (type, message, label = ``) => `${label}no "${type}" diagnostic found for "${message}"`, | ||
combine: (errors) => `\n${errors.join(`\n`)}`, | ||
}; | ||
//# sourceMappingURL=inline-expectation.js.map |
{ | ||
"name": "@stylable/core-test-kit", | ||
"version": "4.10.0", | ||
"version": "4.10.1", | ||
"description": "Stylable core test-kit", | ||
@@ -11,3 +11,3 @@ "main": "dist/index.js", | ||
"@file-services/memory": "^5.7.1", | ||
"@stylable/core": "^4.10.0", | ||
"@stylable/core": "^4.10.1", | ||
"chai": "^4.3.4", | ||
@@ -14,0 +14,0 @@ "flat": "^5.0.2", |
169
README.md
@@ -5,116 +5,125 @@ # @stylable/core-test-kit | ||
`@stylable/core-test-kit` is a collection of utilities aimed at making testing Stylable core behavior and functionality easier. | ||
## Inline expectations syntax | ||
## What's in this test-kit? | ||
The inline expectation syntax can be used with `testInlineExpects` for testing stylesheets transformation and diagnostics. | ||
### Matchers | ||
An expectation is written as a comment just before the code it checks on. All expectations support `label` that will be thrown as part of an expectation fail message. | ||
An assortment of `Chai` matchers used by Stylable. | ||
### `@rule` - check rule transformation including selector and nested declarations: | ||
- `flat-match` - flattens and matches passed arguments | ||
- `results` - test Stylable transpiled style rules output | ||
Selector - `@rule SELECTOR` | ||
```css | ||
/* @rule .entry__root::before */ | ||
.root::before {} | ||
``` | ||
### Diagnostics tooling | ||
Declarations - `@rule SELECTOR { decl: val; }` | ||
```css | ||
/* @rule .entry__root { color: red } */ | ||
.root { color: red; } | ||
A collection of tools used for testing Stylable diagnostics messages (warnings and errors). | ||
/* @rule .entry__root { | ||
color: red; | ||
background: green; | ||
}*/ | ||
.root { | ||
color: red; | ||
background: green; | ||
} | ||
``` | ||
- `expectAnalyzeDiagnostics` - processes a Stylable input and checks for diagnostics during processing | ||
- `expectTransformDiagnostics` - checks for diagnostics after a full transformation | ||
- `shouldReportNoDiagnostics` - helper to check no diagnostics were reported | ||
Target generated rules (mixin) - ` @rule[OFFSET] SELECTOR` | ||
```css | ||
.mix { | ||
color: red; | ||
} | ||
.mix:hover { | ||
color: green; | ||
} | ||
/* | ||
@rule .entry__root {color: red;} | ||
@rule[1] .entry__root:hover {color: green;} | ||
*/ | ||
.root { | ||
-st-mixin: mix; | ||
} | ||
``` | ||
### Testing infrastructure | ||
Label - `@rule(LABEL) SELECTOR` | ||
```css | ||
/* @rule(expect 1) .entry__root */ | ||
.root {} | ||
Used for setting up Stylable instances (`processor`/`transformer`) and their infrastructure: | ||
/* @rule(expect 2) .entry__part */ | ||
.part {} | ||
``` | ||
- `generateInfra` - create Stylable basic in memory infrastructure (`resolver`, `requireModule`, `fileProcessor`) | ||
- `generateStylableResult` - genetare transformation results from in memory configuration | ||
- `generateStylableRoot` - helper over `generateStylableResult` that returns the `outputAst` | ||
- `generateStylableExports` - helper over `generateStylableResult` that returns the `exports` mapping | ||
### `@atrule` - check at-rule transformation of params: | ||
### `testInlineExpects` utility | ||
Exposes `testInlineExpects` for testing transformed stylesheets that include inline expectation comments. These are the most common type of core tests and the recommended way of testing the core functionality. | ||
#### Supported checks: | ||
Rule checking (place just before rule) supporting multi-line declarations and multiple `@checks` statements | ||
##### Terminilogy | ||
- `LABEL: <string>` - label for the test expectation | ||
- `OFFEST: <number>` - offest for the tested rule after the `@check` | ||
- `SELECTOR: <string>` - output selector | ||
- `DECL: <string>` - declaration name | ||
- `VALUE: <string>` - declaration value | ||
Full options: | ||
AtRule params - `@atrule PARAMS`: | ||
```css | ||
/* @check(LABEL)[OFFEST] SELECTOR {DECL: VALUE} */ | ||
/* @atrule screen and (min-width: 900px) */ | ||
@media value(smallScreen) {} | ||
``` | ||
Basic - `@check SELECTOR` | ||
```css | ||
/* @check header::before */ | ||
header::before {} | ||
Label - `@atrule(LABEL) PARAMS` | ||
```css | ||
/* @atrule(jump keyframes) entry__jump */ | ||
@keyframes jump {} | ||
``` | ||
With declarations - ` @check SELECTOR {DECL1: VALUE1; DECL2: VALUE2;}` | ||
### `@decl` - check declaration transformation | ||
This will check full match and order. | ||
```css | ||
.my-mixin { | ||
color: red; | ||
Prop & value - `@decl PROP: VALUE` | ||
```css | ||
.root { | ||
/* @decl color: red */ | ||
color: red | ||
} | ||
``` | ||
/* @check .entry__container {color: red;} */ | ||
.container { | ||
-st-mixin: my-mixin; | ||
Label - `@decl(LABEL) PROP: VALUE` | ||
```css | ||
.root { | ||
/* @decl(color is red) color: red */ | ||
color: red; | ||
} | ||
``` | ||
Target generated rules (mixin) - ` @check[OFFEST] SELECTOR` | ||
### `@analyze` & `@transform` - check single file (analyze) and multiple files (transform) diagnostics: | ||
Severity - `@analyze-SEVERITY MESSAGE` / `@transform-SEVERITY MESSAGE` | ||
```css | ||
.my-mixin { | ||
color: blue; | ||
/* @analyze-info found deprecated usage */ | ||
@st-global-custom-property --x; | ||
/* @analyze-warn missing keyframes name */ | ||
@keyframes {} | ||
/* @analyze-error invalid functional id */ | ||
#id() {} | ||
.root { | ||
/* @transform-error unresolved "unknown" build variable */ | ||
color: value(unknown); | ||
} | ||
/* | ||
@check[1] .entry__container:hover {color: blue;} | ||
*/ | ||
.container { | ||
-st-mixin: my-mixin; | ||
} | ||
``` | ||
Support atrule params (anything between the @atrule and body or semicolon): | ||
Word - `@analyze-SEVERITY word(TEXT) MESSAGE` / `@transform-SEVERITY word(TEXT) MESSAGE` | ||
```css | ||
/* @check screen and (min-width: 900px) */ | ||
@media value(smallScreen) {} | ||
/* @transform-warn word(unknown) unknown pseudo element */ | ||
.root::unknown {} | ||
``` | ||
#### Example | ||
Here we are generating a Stylable AST which lncludes the `/* @check SELECTOR */` comment to test the root class selector target. | ||
The `testInlineExpects` function performs that actual assertions to perform the test. | ||
Label - `@analyze(LABEL) MESSAGE` / `@transform(LABEL) MESSAGE` | ||
```css | ||
/* @analyze-warn(local keyframes) missing keyframes name */ | ||
@keyframes {} | ||
```ts | ||
it('...', ()=>{ | ||
const root = generateStylableRoot({ | ||
entry: `/style.st.css`, | ||
files: { | ||
'/style.st.css': { | ||
namespace: 'ns', | ||
content: ` | ||
/* @check .ns__root */ | ||
.root {} | ||
` | ||
}, | ||
}); | ||
testInlineExpects(root, 1); | ||
}) | ||
/* @transform-warn(imported keyframes) unresolved keyframes "unknown" */ | ||
@keyframes unknown {} | ||
``` | ||
### Match rules | ||
Exposes two utility functions (`matchRuleAndDeclaration` and `matchAllRulesAndDeclarations`) used for testing Stylable generated AST representing CSS rules and declarations. | ||
## License | ||
Copyright (c) 2019 Wix.com Ltd. All Rights Reserved. Use of this source code is governed by a [MIT license](./LICENSE). |
@@ -29,2 +29,113 @@ import { expect } from 'chai'; | ||
interface MatchState { | ||
matches: number; | ||
location: string; | ||
word: string; | ||
severity: string; | ||
} | ||
const createMatchDiagnosticState = (): MatchState => ({ | ||
matches: 0, | ||
location: ``, | ||
word: ``, | ||
severity: ``, | ||
}); | ||
const isSupportedSeverity = (val: string): val is DiagnosticType => !!val.match(/info|warn|error/); | ||
export function matchDiagnostic( | ||
type: `analyze` | `transform`, | ||
meta: Pick<StylableMeta, `diagnostics` | `transformDiagnostics`>, | ||
expected: { | ||
label?: string; | ||
message: string; | ||
severity: string; | ||
location: Location; | ||
}, | ||
errors: { | ||
diagnosticsNotFound: (type: string, message: string, label?: string) => string; | ||
unsupportedSeverity: (type: string, severity: string, label?: string) => string; | ||
locationMismatch: (type: string, message: string, label?: string) => string; | ||
wordMismatch: ( | ||
type: string, | ||
expectedWord: string, | ||
message: string, | ||
label?: string | ||
) => string; | ||
severityMismatch: ( | ||
type: string, | ||
expectedSeverity: string, | ||
actualSeverity: string, | ||
message: string, | ||
label?: string | ||
) => string; | ||
expectedNotFound: (type: string, message: string, label?: string) => string; | ||
} | ||
): string { | ||
const diagnostics = type === `analyze` ? meta.diagnostics : meta.transformDiagnostics; | ||
if (!diagnostics) { | ||
return errors.diagnosticsNotFound(type, expected.message, expected.label); | ||
} | ||
const expectedSeverity = | ||
(expected.severity as any) === `warn` ? `warning` : expected.severity || ``; | ||
if (!isSupportedSeverity(expectedSeverity)) { | ||
return errors.unsupportedSeverity(type, expected.severity || ``, expected.label); | ||
} | ||
let closestMatchState = createMatchDiagnosticState(); | ||
const foundPartialMatch = (newState: MatchState) => { | ||
if (newState.matches >= closestMatchState.matches) { | ||
closestMatchState = newState; | ||
} | ||
}; | ||
for (const report of diagnostics.reports.values()) { | ||
const matchState = createMatchDiagnosticState(); | ||
if (report.message !== expected.message) { | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
// if (!expected.skipLocationCheck) { | ||
// ToDo: test all range | ||
if (report.node.source!.start!.offset !== expected.location.start!.offset) { | ||
matchState.location = errors.locationMismatch(type, expected.message, expected.label); | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
// } | ||
if (expected.location.word) { | ||
if (report.options.word !== expected.location.word) { | ||
matchState.word = errors.wordMismatch( | ||
type, | ||
expected.location.word, | ||
expected.message, | ||
expected.label | ||
); | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
} | ||
if (expected.severity) { | ||
if (report.type !== expectedSeverity) { | ||
matchState.location = errors.severityMismatch( | ||
type, | ||
expectedSeverity, | ||
report.type, | ||
expected.message, | ||
expected.label | ||
); | ||
foundPartialMatch(matchState); | ||
continue; | ||
} | ||
matchState.matches++; | ||
} | ||
// expected matched! | ||
return ``; | ||
} | ||
return ( | ||
closestMatchState.location || | ||
closestMatchState.word || | ||
closestMatchState.severity || | ||
errors.expectedNotFound(type, expected.message, expected.label) | ||
); | ||
} | ||
export function findTestLocations(css: string) { | ||
@@ -31,0 +142,0 @@ let line = 1; |
@@ -0,17 +1,29 @@ | ||
import { matchDiagnostic } from './diagnostics'; | ||
import type { StylableMeta } from '@stylable/core'; | ||
import type * as postcss from 'postcss'; | ||
interface RuleCheck { | ||
kind: `rule`; | ||
rule: postcss.Rule; | ||
msg?: string; | ||
expectedSelector: string; | ||
expectedDeclarations: [string, string][]; | ||
declarationCheck: 'full' | 'none'; | ||
interface Test { | ||
type: TestScopes; | ||
expectation: string; | ||
errors: string[]; | ||
} | ||
interface AtRuleCheck { | ||
kind: `atrule`; | ||
rule: postcss.AtRule; | ||
msg?: string; | ||
expectedParams: string; | ||
type AST = postcss.Rule | postcss.AtRule | postcss.Declaration; | ||
const tests = { | ||
'@check': checkTest, | ||
'@rule': ruleTest, | ||
'@atrule': atRuleTest, | ||
'@decl': declTest, | ||
'@analyze': analyzeTest, | ||
'@transform': transformTest, | ||
} as const; | ||
type TestScopes = keyof typeof tests; | ||
const testScopes = Object.keys(tests) as TestScopes[]; | ||
const testScopesRegex = () => testScopes.join(`|`); | ||
interface Context { | ||
meta: Pick<StylableMeta, 'outputAst' | 'rawAst' | 'diagnostics' | 'transformDiagnostics'>; | ||
} | ||
const isRoot = (val: any): val is postcss.Root => val.type === `root`; | ||
@@ -50,23 +62,72 @@ /** | ||
*/ | ||
export function testInlineExpects( | ||
result: postcss.Root, | ||
expectedTestsCount = result.toString().match(/@check/gm)!.length | ||
) { | ||
if (expectedTestsCount === 0) { | ||
throw new Error('no tests found try to add @check comments before any selector'); | ||
} | ||
const checks: Array<RuleCheck | AtRuleCheck> = []; | ||
export function testInlineExpects(result: postcss.Root | Context, expectedTestInput?: number) { | ||
// backward compatibility (no diagnostic checks) | ||
const isDeprecatedInput = isRoot(result); | ||
const context = isDeprecatedInput | ||
? { | ||
meta: { | ||
outputAst: result, | ||
rawAst: null as unknown as StylableMeta['rawAst'], | ||
diagnostics: null as unknown as StylableMeta['diagnostics'], | ||
transformDiagnostics: null as unknown as StylableMeta['transformDiagnostics'], | ||
}, | ||
} | ||
: result; | ||
// ToDo: support analyze mode | ||
const rootAst = context.meta.outputAst!; | ||
const expectedTestAmount = | ||
expectedTestInput ?? | ||
(rootAst.toString().match(new RegExp(`${testScopesRegex()}`, `gm`))?.length || 0); | ||
const checks: Test[] = []; | ||
const errors: string[] = []; | ||
// collect checks | ||
result.walkComments((comment) => { | ||
const checksInput = comment.text.split(`@check`); | ||
const rule = comment.next(); | ||
if (checksInput.length > 1 && rule) { | ||
if (rule.type === `rule`) { | ||
for (const checkInput of checksInput) { | ||
if (checkInput.trim()) { | ||
const check = createRuleCheck(rule, checkInput, errors); | ||
if (check) { | ||
checks.push(check); | ||
rootAst.walkComments((comment) => { | ||
const input = comment.text.split(/@/gm); | ||
const testCommentTarget = comment; | ||
const testCommentSrc = isDeprecatedInput | ||
? comment | ||
: getSourceComment(context.meta, comment) || comment; | ||
const nodeTarget = testCommentTarget.next() as AST; | ||
const nodeSrc = testCommentSrc.next() as AST; | ||
if (nodeTarget || nodeSrc) { | ||
while (input.length) { | ||
const next = `@` + input.shift()!; | ||
const testMatch = next.match(new RegExp(`^(${testScopesRegex()})`, `g`)); | ||
if (testMatch) { | ||
const testScope = testMatch[0] as TestScopes; | ||
let testInput = next.replace(testScope, ``); | ||
// collect expectation inner `@` fragments | ||
while ( | ||
input.length && | ||
!(`@` + input[0]).match(new RegExp(`^(${testScopesRegex()})`, `g`)) | ||
) { | ||
testInput += `@` + input.shift(); | ||
} | ||
if (testInput) { | ||
if ( | ||
isDeprecatedInput && | ||
(testScope === `@analyze` || testScope === `@transform`) | ||
) { | ||
// not possible with just AST root | ||
const result: Test = { | ||
type: testScope, | ||
expectation: testInput.trim(), | ||
errors: [ | ||
testInlineExpectsErrors.deprecatedRootInputNotSupported( | ||
testScope + testInput | ||
), | ||
], | ||
}; | ||
errors.push(...result.errors); | ||
checks.push(result); | ||
} else { | ||
const result = tests[testScope]( | ||
context, | ||
testInput.trim(), | ||
nodeTarget, | ||
nodeSrc | ||
); | ||
result.type = testScope; | ||
errors.push(...result.errors); | ||
checks.push(result); | ||
} | ||
@@ -76,49 +137,4 @@ } | ||
} | ||
if (rule.type === `atrule`) { | ||
if (checksInput.length > 2) { | ||
errors.push(testInlineExpectsErrors.atRuleMultiTest(comment.text)); | ||
} | ||
const check = createAtRuleCheck(rule, checksInput[1]); | ||
if (check) { | ||
checks.push(check); | ||
} | ||
} | ||
} | ||
}); | ||
// check | ||
checks.forEach((check) => { | ||
if (check.kind === `rule`) { | ||
const { msg, rule, expectedSelector, expectedDeclarations, declarationCheck } = check; | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (rule.selector !== expectedSelector) { | ||
errors.push( | ||
testInlineExpectsErrors.selector(expectedSelector, rule.selector, prefix) | ||
); | ||
} | ||
if (declarationCheck === `full`) { | ||
const actualDecl = rule.nodes.map((x) => x.toString()).join(`; `); | ||
const expectedDecl = expectedDeclarations | ||
.map(([prop, value]) => `${prop}: ${value}`) | ||
.join(`; `); | ||
if (actualDecl !== expectedDecl) { | ||
errors.push( | ||
testInlineExpectsErrors.declarations( | ||
expectedDecl, | ||
actualDecl, | ||
rule.selector, | ||
prefix | ||
) | ||
); | ||
} | ||
} | ||
} else if (check.kind === `atrule`) { | ||
const { msg, rule, expectedParams } = check; | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (rule.params !== expectedParams) { | ||
errors.push( | ||
testInlineExpectsErrors.atruleParams(expectedParams, rule.params, prefix) | ||
); | ||
} | ||
} | ||
}); | ||
// report errors | ||
@@ -128,67 +144,251 @@ if (errors.length) { | ||
} | ||
if (expectedTestsCount !== checks.length) { | ||
throw new Error(testInlineExpectsErrors.matchAmount(expectedTestsCount, checks.length)); | ||
if (expectedTestAmount !== checks.length) { | ||
throw new Error(testInlineExpectsErrors.matchAmount(expectedTestAmount, checks.length)); | ||
} | ||
} | ||
function createRuleCheck( | ||
rule: postcss.Rule, | ||
expectInput: string, | ||
errors: string[] | ||
): RuleCheck | undefined { | ||
const { msg, ruleIndex, expectedSelector, expectedBody } = expectInput.match( | ||
function checkTest(context: Context, expectation: string, targetNode: AST, srcNode: AST): Test { | ||
const type = targetNode?.type; | ||
switch (type) { | ||
case `rule`: { | ||
return tests[`@rule`](context, expectation, targetNode, srcNode); | ||
} | ||
case `atrule`: { | ||
return tests[`@atrule`](context, expectation, targetNode, srcNode); | ||
} | ||
default: | ||
return { | ||
type: `@check`, | ||
expectation, | ||
errors: [testInlineExpectsErrors.unsupportedNode(`@check`, type)], | ||
}; | ||
} | ||
} | ||
function ruleTest(context: Context, expectation: string, targetNode: AST, _srcNode: AST): Test { | ||
const result: Test = { | ||
type: `@rule`, | ||
expectation, | ||
errors: [], | ||
}; | ||
const { msg, ruleIndex, expectedSelector, expectedBody } = expectation.match( | ||
/(?<msg>\(.*\))*(\[(?<ruleIndex>\d+)\])*(?<expectedSelector>[^{}]*)\s*(?<expectedBody>.*)/s | ||
)!.groups!; | ||
const targetRule = ruleIndex ? getNextMixinRule(rule, Number(ruleIndex)) : rule; | ||
if (!targetRule) { | ||
errors.push(testInlineExpectsErrors.unfoundMixin(expectInput)); | ||
return; | ||
let testNode: AST = targetNode; | ||
// get mixed-in rule | ||
if (ruleIndex) { | ||
if (targetNode?.type !== `rule`) { | ||
result.errors.push( | ||
`mixed-in expectation is only supported for CSS Rule, not ${targetNode?.type}` | ||
); | ||
return result; | ||
} else { | ||
const actualTarget = getNextMixinRule(targetNode, Number(ruleIndex)); | ||
if (!actualTarget) { | ||
result.errors.push(testInlineExpectsErrors.unfoundMixin(expectation)); | ||
return result; | ||
} | ||
testNode = actualTarget as AST; | ||
} | ||
} | ||
const expectedDeclarations: RuleCheck[`expectedDeclarations`] = []; | ||
const declsInput = expectedBody.trim().match(/^{(.*)}$/s); | ||
const declarationCheck: RuleCheck[`declarationCheck`] = declsInput ? `full` : `none`; | ||
if (declsInput && declsInput[1]?.includes(`:`)) { | ||
for (const decl of declsInput[1].split(`;`)) { | ||
if (decl.trim() !== ``) { | ||
const [prop, value] = decl.split(':'); | ||
if (prop && value) { | ||
expectedDeclarations.push([prop.trim(), value.trim()]); | ||
} else { | ||
errors.push(testInlineExpectsErrors.malformedDecl(decl, expectInput)); | ||
// test by target node type | ||
const nodeType = testNode?.type; | ||
if (nodeType === `rule`) { | ||
const expectedDeclarations: [string, string][] = []; | ||
const declsInput = expectedBody.trim().match(/^{(.*)}$/s); | ||
const declarationCheck: 'full' | 'none' = declsInput ? `full` : `none`; | ||
if (declsInput && declsInput[1]?.includes(`:`)) { | ||
for (const decl of declsInput[1].split(`;`)) { | ||
if (decl.trim() !== ``) { | ||
const [prop, value] = decl.split(':'); | ||
if (prop && value) { | ||
expectedDeclarations.push([prop.trim(), value.trim()]); | ||
} else { | ||
result.errors.push( | ||
testInlineExpectsErrors.ruleMalformedDecl(decl, expectation) | ||
); | ||
} | ||
} | ||
} | ||
} | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (testNode.selector !== expectedSelector.trim()) { | ||
result.errors.push( | ||
testInlineExpectsErrors.selector(expectedSelector.trim(), testNode.selector, prefix) | ||
); | ||
} | ||
if (declarationCheck === `full`) { | ||
const actualDecl = testNode.nodes.map((x) => x.toString()).join(`; `); | ||
const expectedDecl = expectedDeclarations | ||
.map(([prop, value]) => `${prop}: ${value}`) | ||
.join(`; `); | ||
if (actualDecl !== expectedDecl) { | ||
result.errors.push( | ||
testInlineExpectsErrors.declarations( | ||
expectedDecl, | ||
actualDecl, | ||
testNode.selector, | ||
prefix | ||
) | ||
); | ||
} | ||
} | ||
} else if (nodeType === `atrule`) { | ||
// passing null to srcNode as atruleTest doesn't actually requires it. | ||
// if it would at some point, then its just a matter of searching the rawAst for it. | ||
return atRuleTest( | ||
context, | ||
expectation.replace(`[${ruleIndex}]`, ``), | ||
testNode, | ||
null as unknown as AST | ||
); | ||
} else { | ||
// unsupported mixed-in node test | ||
result.errors.push(testInlineExpectsErrors.unsupportedMixinNode(testNode.type)); | ||
} | ||
return { | ||
kind: `rule`, | ||
msg, | ||
rule: targetRule, | ||
expectedSelector: expectedSelector.trim(), | ||
expectedDeclarations, | ||
declarationCheck, | ||
return result; | ||
} | ||
function atRuleTest(_context: Context, expectation: string, targetNode: AST, _srcNode: AST): Test { | ||
const result: Test = { | ||
type: `@atrule`, | ||
expectation, | ||
errors: [], | ||
}; | ||
const { msg, expectedParams } = expectation.match(/(?<msg>\([^)]*\))*(?<expectedParams>.*)/)! | ||
.groups!; | ||
if (expectedParams.match(/^\[\d+\]/)) { | ||
result.errors.push(testInlineExpectsErrors.atRuleMultiTest(expectation)); | ||
return result; | ||
} | ||
const prefix = msg ? msg + `: ` : ``; | ||
if (targetNode.type === `atrule`) { | ||
if (targetNode.params !== expectedParams.trim()) { | ||
result.errors.push( | ||
testInlineExpectsErrors.atruleParams( | ||
expectedParams.trim(), | ||
targetNode.params, | ||
prefix | ||
) | ||
); | ||
} | ||
} else { | ||
result.errors.push(testInlineExpectsErrors.unsupportedNode(`@atrule`, targetNode.type)); | ||
} | ||
return result; | ||
} | ||
function createAtRuleCheck(rule: postcss.AtRule, expectInput: string): AtRuleCheck | undefined { | ||
const { msg, expectedParams } = expectInput.match(/(?<msg>\([^)]*\))*(?<expectedParams>.*)/)! | ||
.groups!; | ||
return { | ||
kind: `atrule`, | ||
msg, | ||
rule, | ||
expectedParams: expectedParams.trim(), | ||
function declTest(_context: Context, expectation: string, targetNode: AST, _srcNode: AST): Test { | ||
const result: Test = { | ||
type: `@decl`, | ||
expectation, | ||
errors: [], | ||
}; | ||
let { label, prop, value } = expectation.match( | ||
/(?<label>\([^)]*\))*(?<prop>[^:]*)\s*:?\s*(?<value>.*)/ | ||
)!.groups!; | ||
label = label ? label + `: ` : ``; | ||
prop = prop.trim(); | ||
value = value.trim(); | ||
if (!prop || !value) { | ||
result.errors.push(testInlineExpectsErrors.declMalformed(prop, value, label)); | ||
} else if (targetNode.type === `decl`) { | ||
if (targetNode.prop !== prop.trim() || targetNode.value !== value) { | ||
const expected = prop.trim() + `: ` + value.trim(); | ||
const actual = targetNode.prop + `: ` + targetNode.value; | ||
result.errors.push(testInlineExpectsErrors.decl(expected, actual, label)); | ||
} | ||
} else { | ||
result.errors.push( | ||
testInlineExpectsErrors.unsupportedNode(`@decl`, targetNode.type, label) | ||
); | ||
} | ||
return result; | ||
} | ||
function analyzeTest(context: Context, expectation: string, targetNode: AST, srcNode: AST): Test { | ||
return diagnosticTest(`analyze`, context, expectation, targetNode, srcNode); | ||
} | ||
function transformTest(context: Context, expectation: string, targetNode: AST, srcNode: AST): Test { | ||
return diagnosticTest(`transform`, context, expectation, targetNode, srcNode); | ||
} | ||
function diagnosticTest( | ||
type: `analyze` | `transform`, | ||
{ meta }: Context, | ||
expectation: string, | ||
_targetNode: AST, | ||
srcNode: AST | ||
): Test { | ||
const result: Test = { | ||
type: `@${type}`, | ||
expectation, | ||
errors: [], | ||
}; | ||
const matchResult = expectation.match( | ||
/-(?<severity>\w+)(?<label>\([^)]*\))?\s?(?:word\((?<word>[^)]*)\))?\s?(?<message>.*)/ | ||
); | ||
if (!matchResult) { | ||
result.errors.push(testInlineExpectsErrors.diagnosticsMalformed(type, expectation)); | ||
return result; | ||
} | ||
let { label, severity, message, word } = matchResult.groups!; | ||
label = label ? label + `: ` : ``; | ||
severity = severity?.trim() || ``; | ||
message = message?.trim() || ``; | ||
word = word?.trim() || ``; | ||
function getNextMixinRule(currentRule: postcss.Rule, count: number): postcss.Rule | undefined { | ||
while (currentRule && count > 0) { | ||
const next: postcss.ChildNode | undefined = currentRule.next(); | ||
// next must be a rule sense mixin can only add rules | ||
if (next?.type === `rule`) { | ||
currentRule = next; | ||
if (!message) { | ||
result.errors.push(testInlineExpectsErrors.diagnosticsMalformed(type, expectation, label)); | ||
return result; | ||
} | ||
// check for diagnostic | ||
const error = matchDiagnostic( | ||
type, | ||
meta, | ||
{ | ||
label, | ||
message, | ||
severity, | ||
location: { | ||
start: srcNode.source?.start, | ||
end: srcNode.source?.end, | ||
word, | ||
css: ``, | ||
}, | ||
}, | ||
{ | ||
diagnosticsNotFound: testInlineExpectsErrors.diagnosticsNotFound, | ||
unsupportedSeverity: testInlineExpectsErrors.diagnosticsUnsupportedSeverity, | ||
locationMismatch: testInlineExpectsErrors.diagnosticsLocationMismatch, | ||
wordMismatch: testInlineExpectsErrors.diagnosticsWordMismatch, | ||
severityMismatch: testInlineExpectsErrors.diagnosticsSeverityMismatch, | ||
expectedNotFound: testInlineExpectsErrors.diagnosticExpectedNotFound, | ||
} | ||
); | ||
if (error) { | ||
result.errors.push(error); | ||
} | ||
return result; | ||
} | ||
function getSourceComment(meta: Context['meta'], { source }: postcss.Comment) { | ||
let match: postcss.Comment | undefined = undefined; | ||
meta.rawAst.walkComments((srcComment) => { | ||
if ( | ||
srcComment.source?.start?.offset === source?.start?.offset && | ||
srcComment.source?.end?.offset === source?.end?.offset | ||
) { | ||
match = srcComment; | ||
return false; | ||
} | ||
return; | ||
}); | ||
return match; | ||
} | ||
function getNextMixinRule(originRule: postcss.Rule, count: number) { | ||
let current: postcss.Node | undefined = originRule; | ||
while (current && count > 0) { | ||
current = current.next(); | ||
if (current?.type !== `comment`) { | ||
count--; | ||
} else { | ||
return; | ||
} | ||
} | ||
return currentRule && count === 0 ? currentRule : undefined; | ||
return current && count === 0 ? current : undefined; | ||
} | ||
@@ -198,14 +398,50 @@ | ||
matchAmount: (expectedAmount: number, actualAmount: number) => | ||
`Expected ${expectedAmount} checks to run but there was ${actualAmount}`, | ||
`Expected "${expectedAmount}" checks to run but "${actualAmount}" were found`, | ||
unsupportedNode: (testType: string, nodeType: string, label = ``) => | ||
`${label}unsupported type "${testType}" for "${nodeType}"`, | ||
selector: (expectedSelector: string, actualSelector: string, label = ``) => | ||
`${label}expected ${actualSelector} to transform to ${expectedSelector}`, | ||
`${label}expected "${actualSelector}" to transform to "${expectedSelector}"`, | ||
declarations: (expectedDecl: string, actualDecl: string, selector: string, label = ``) => | ||
`${label}expected ${selector} to have declaration {${expectedDecl}}, but got {${actualDecl}}`, | ||
unfoundMixin: (expectInput: string) => `cannot locate mixed-in rule for "${expectInput}"`, | ||
malformedDecl: (decl: string, expectInput: string) => | ||
unsupportedMixinNode: (type: string) => `unsupported mixin expectation of type "${type}"`, | ||
ruleMalformedDecl: (decl: string, expectInput: string) => | ||
`error in expectation "${decl}" of "${expectInput}"`, | ||
atruleParams: (expectedParams: string, actualParams: string, label = ``) => | ||
`${label}expected ${actualParams} to transform to ${expectedParams}`, | ||
atRuleMultiTest: (comment: string) => `atrule multi test is not supported (${comment})`, | ||
`${label}expected "${actualParams}" to transform to ${expectedParams}`, | ||
atRuleMultiTest: (comment: string) => `atrule mixin is not supported: (${comment})`, | ||
decl: (expected: string, actual: string, label = ``) => | ||
`${label}expected "${actual}" to transform to "${expected}"`, | ||
declMalformed: (expectedProp: string, expectedLabel: string, label = ``) => { | ||
if (!expectedProp && !expectedLabel) { | ||
return `${label}malformed declaration expectation, format should be: "prop: value"`; | ||
} else if (!expectedProp) { | ||
return `${label}malformed declaration expectation missing prop: "???: ${expectedLabel}"`; | ||
} else { | ||
return `${label}malformed declaration expectation missing value: "${expectedProp}: ???"`; | ||
} | ||
}, | ||
deprecatedRootInputNotSupported: (expectation: string) => | ||
`"${expectation}" is not supported for with the used input, try calling testInlineExpects(generateStylableResults())`, | ||
diagnosticsMalformed: (type: string, expectation: string, label = ``) => | ||
`${label}malformed @${type} expectation "@${type}${expectation}". format should be: "@${type}-[severity] diagnostic message"`, | ||
diagnosticsNotFound: (type: string, message: string, label = ``) => | ||
`${label}${type} diagnostics not found for "${message}"`, | ||
diagnosticsUnsupportedSeverity: (type: string, severity: string, label = ``) => | ||
`${label}unsupported @${type}-[severity]: "${severity}"`, | ||
diagnosticsLocationMismatch: (type: string, message: string, label = ``) => | ||
`${label}expected "@${type}-[severity] "${message}" to be reported in this location, but got it somewhere else`, | ||
diagnosticsWordMismatch: (type: string, expectedWord: string, message: string, label = ``) => | ||
`${label}expected word in "@${type}-[severity] word(${expectedWord}) ${message}" was not found`, | ||
diagnosticsSeverityMismatch: ( | ||
type: string, | ||
expectedSeverity: string, | ||
actualSeverity: string, | ||
message: string, | ||
label = `` | ||
) => | ||
`${label}expected ${type} diagnostic "${message}" to be reported with "${expectedSeverity}", but it was reported with "${actualSeverity}"`, | ||
diagnosticExpectedNotFound: (type: string, message: string, label = ``) => | ||
`${label}no "${type}" diagnostic found for "${message}"`, | ||
combine: (errors: string[]) => `\n${errors.join(`\n`)}`, | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
128718
2037
129
Updated@stylable/core@^4.10.1