Skip to content

Commit

Permalink
refactor: extend inline testing (#2242)
Browse files Browse the repository at this point in the history
- add `@analyze` and @Transform diagnostics expectation
- add `@decl` single declaration expectation - 
- support for mixed-in at-rules
  • Loading branch information
idoros authored Jan 9, 2022
1 parent 7ff361d commit c23ce18
Show file tree
Hide file tree
Showing 4 changed files with 1,582 additions and 415 deletions.
171 changes: 90 additions & 81 deletions packages/core-test-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,117 +2,126 @@

[![npm version](https://img.shields.io/npm/v/@stylable/core-test-kit.svg)](https://www.npmjs.com/package/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

### Diagnostics tooling

A collection of tools used for testing Stylable diagnostics messages (warnings and errors).

- `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

### Testing infrastructure

Used for setting up Stylable instances (`processor`/`transformer`) and their infrastructure:
Selector - `@rule SELECTOR`
```css
/* @rule .entry__root::before */
.root::before {}
```

- `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
Declarations - `@rule SELECTOR { decl: val; }`
```css
/* @rule .entry__root { color: red } */
.root { color: red; }

### `testInlineExpects` utility
/* @rule .entry__root {
color: red;
background: green;
}*/
.root {
color: red;
background: green;
}
```

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.
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;
}
```

#### Supported checks:
Label - `@rule(LABEL) SELECTOR`
```css
/* @rule(expect 1) .entry__root */
.root {}

Rule checking (place just before rule) supporting multi-line declarations and multiple `@checks` statements
/* @rule(expect 2) .entry__part */
.part {}
```

##### 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
### `@atrule` - check at-rule transformation of params:

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;
}
/*
@check[1] .entry__container:hover {color: blue;}
*/
.container {
-st-mixin: my-mixin;
/* @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);
}
```

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) {}
```
#### 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.

```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 word(unknown) unknown pseudo element */
.root::unknown {}
```

### Match rules
Label - `@analyze(LABEL) MESSAGE` / `@transform(LABEL) MESSAGE`
```css
/* @analyze-warn(local keyframes) missing keyframes name */
@keyframes {}

Exposes two utility functions (`matchRuleAndDeclaration` and `matchAllRulesAndDeclarations`) used for testing Stylable generated AST representing CSS rules and declarations.
/* @transform-warn(imported keyframes) unresolved keyframes "unknown" */
@keyframes unknown {}
```

## License

Expand Down
111 changes: 111 additions & 0 deletions packages/core-test-kit/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,117 @@ export interface Location {
css: string;
}

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) {
let line = 1;
let column = 1;
Expand Down
Loading

0 comments on commit c23ce18

Please sign in to comment.