From b65976342fb4ab41a657b87bc26322cd97db1882 Mon Sep 17 00:00:00 2001 From: Ido Rosenthal Date: Tue, 15 Nov 2022 15:42:38 +0200 Subject: [PATCH] test: refactor existing tests into features - re-write tests to new inline expectation format - expose diagnostics messages internally for tests - remove unused `core-test-kit` matchers --- .../core-test-kit/src/matchers/results.ts | 84 - packages/core/src/features/st-custom-state.ts | 2 + packages/core/src/helpers/custom-state.ts | 2 +- .../core/test/extend-function-parser.spec.ts | 52 - packages/core/test/features/css-class.spec.ts | 296 +-- .../test/features/css-pseudo-class.spec.ts | 890 +++++++ .../test/features/st-custom-state.spec.ts | 278 +++ packages/core/test/pseudo-states.spec.ts | 2211 ----------------- 8 files changed, 1270 insertions(+), 2545 deletions(-) delete mode 100644 packages/core-test-kit/src/matchers/results.ts delete mode 100644 packages/core/test/extend-function-parser.spec.ts create mode 100644 packages/core/test/features/css-pseudo-class.spec.ts create mode 100644 packages/core/test/features/st-custom-state.spec.ts delete mode 100644 packages/core/test/pseudo-states.spec.ts diff --git a/packages/core-test-kit/src/matchers/results.ts b/packages/core-test-kit/src/matchers/results.ts deleted file mode 100644 index 2d9b174ded..0000000000 --- a/packages/core-test-kit/src/matchers/results.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { StylableResults } from '@stylable/core'; -import { expect } from 'chai'; -import type * as postcss from 'postcss'; - -export function mediaQuery(chai: Chai.ChaiStatic, util: Chai.ChaiUtils) { - const { flag } = util; - - chai.Assertion.addMethod('mediaQuery', function (index: number) { - const actual = flag(this, 'object') as StylableResults; - - if (!actual.meta || !actual.exports) { - throw new Error( - `expected Stylable result {meta, exports}, but got: {${Object.keys(actual).join( - ', ' - )}}` - ); - } - - const { targetAst } = actual.meta; - if (!targetAst) { - throw new Error(`expected result to be transformed - missing targetAst on meta`); - } - - const nodes = targetAst.nodes; - - if (!nodes) { - throw new Error(`no rules found for media`); - } - - const media = nodes[index]; - - if (!media || media.type !== 'atrule') { - throw new Error(`no media found at index #${index}`); - } - - flag(this, 'actualRule', media); - }); -} - -export function styleRules(chai: Chai.ChaiStatic, util: Chai.ChaiUtils) { - const { flag } = util; - - chai.Assertion.addMethod( - 'styleRules', - function (styleRules: string[] | { [key: number]: string }) { - const actual = flag(this, 'object') as StylableResults; - if (!actual.meta || !actual.exports) { - throw new Error( - `expected Stylable result {meta, exports}, but got: {${Object.keys(actual).join( - ', ' - )}}` - ); - } - - let scopeRule: postcss.Container | undefined = flag(this, 'actualRule'); - if (!scopeRule) { - const { targetAst } = actual.meta; - if (!targetAst) { - throw new Error( - `expected result to be transfromed - missing targetAst on meta` - ); - } else { - scopeRule = targetAst; - } - } - - if (Array.isArray(styleRules)) { - scopeRule.walkRules((rule, index) => { - const nextExpectedRule = styleRules.shift(); - const actualRule = rule.toString(); - expect(actualRule, `rule #${index}`).to.equal(nextExpectedRule); - }); - } else { - const nodes = scopeRule.nodes; - for (const expectedIndex in styleRules) { - expect(nodes, `rules exist`).to.not.equal(undefined); - expect(nodes && nodes[expectedIndex].toString()).to.equal( - styleRules[expectedIndex] - ); - } - } - } - ); -} diff --git a/packages/core/src/features/st-custom-state.ts b/packages/core/src/features/st-custom-state.ts index 97773dd94e..e47a2002a8 100644 --- a/packages/core/src/features/st-custom-state.ts +++ b/packages/core/src/features/st-custom-state.ts @@ -9,6 +9,7 @@ import { createBooleanStateClassName, createStateWithParamClassName, systemValidators, + validationErrors as sysValidationErrors, resolveStateParam, } from '../helpers/custom-state'; @@ -32,5 +33,6 @@ export { createBooleanStateClassName, createStateWithParamClassName, systemValidators, + sysValidationErrors, resolveStateParam, }; diff --git a/packages/core/src/helpers/custom-state.ts b/packages/core/src/helpers/custom-state.ts index b2e50bb026..df147c0662 100644 --- a/packages/core/src/helpers/custom-state.ts +++ b/packages/core/src/helpers/custom-state.ts @@ -251,7 +251,7 @@ export interface StateResult { errors: string[] | null; } -const validationErrors = { +export const validationErrors = { string: { STRING_TYPE_VALIDATION_FAILED: (actualParam: string) => `"${actualParam}" should be of type string`, diff --git a/packages/core/test/extend-function-parser.spec.ts b/packages/core/test/extend-function-parser.spec.ts deleted file mode 100644 index 5d829f229b..0000000000 --- a/packages/core/test/extend-function-parser.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect } from 'chai'; -import { CSSClass } from '@stylable/core/dist/features'; - -describe('CSS class -st-extends parsing', () => { - it('should parse type extends', () => { - expect(CSSClass.parseStExtends('Button').types).to.eql([ - { args: null, symbolName: 'Button' }, - ]); - }); - - it('should parse function extends with no arguments', () => { - expect(CSSClass.parseStExtends('Button()').types).to.eql([ - { args: [], symbolName: 'Button' }, - ]); - }); - - it('should parse type extends with value arguments separated by comma', () => { - expect(CSSClass.parseStExtends('Button(1px solid, red)').types).to.eql([ - { - args: [ - [ - { type: 'word', value: '1px' }, - { type: 'space', value: ' ' }, - { type: 'word', value: 'solid' }, - ], - [{ type: 'word', value: 'red' }], - ], - symbolName: 'Button', - }, - ]); - }); - - it('should parse multiple extends separated by space', () => { - expect(CSSClass.parseStExtends('Button(1px solid, red) Mixin').types).to.eql([ - { - args: [ - [ - { type: 'word', value: '1px' }, - { type: 'space', value: ' ' }, - { type: 'word', value: 'solid' }, - ], - [{ type: 'word', value: 'red' }], - ], - symbolName: 'Button', - }, - { - args: null, - symbolName: 'Mixin', - }, - ]); - }); -}); diff --git a/packages/core/test/features/css-class.spec.ts b/packages/core/test/features/css-class.spec.ts index 3e679a0787..94468856a1 100644 --- a/packages/core/test/features/css-class.spec.ts +++ b/packages/core/test/features/css-class.spec.ts @@ -1,4 +1,4 @@ -import { STImport, CSSClass, STSymbol } from '@stylable/core/dist/features'; +import { STImport, STCustomState, CSSClass, STSymbol } from '@stylable/core/dist/features'; import { testStylableCore, shouldReportNoDiagnostics, @@ -9,6 +9,7 @@ import type * as postcss from 'postcss'; const classDiagnostics = diagnosticBankReportToStrings(CSSClass.diagnostics); const stSymbolDiagnostics = diagnosticBankReportToStrings(STSymbol.diagnostics); +const stCustomStateDiagnostics = diagnosticBankReportToStrings(STCustomState.diagnostics); describe(`features/css-class`, () => { it(`should have root class`, () => { @@ -452,6 +453,103 @@ describe(`features/css-class`, () => { expect(actual).to.contain(expected); }); + describe('parsing (unit tests)', () => { + it('should parse type extends', () => { + expect(CSSClass.parseStExtends('Button').types).to.eql([ + { args: null, symbolName: 'Button' }, + ]); + }); + it('should parse function extends with no arguments', () => { + expect(CSSClass.parseStExtends('Button()').types).to.eql([ + { args: [], symbolName: 'Button' }, + ]); + }); + it('should parse type extends with value arguments separated by comma', () => { + expect(CSSClass.parseStExtends('Button(1px solid, red)').types).to.eql([ + { + args: [ + [ + { type: 'word', value: '1px' }, + { type: 'space', value: ' ' }, + { type: 'word', value: 'solid' }, + ], + [{ type: 'word', value: 'red' }], + ], + symbolName: 'Button', + }, + ]); + }); + it('should parse multiple extends separated by space', () => { + expect(CSSClass.parseStExtends('Button(1px solid, red) Mixin').types).to.eql([ + { + args: [ + [ + { type: 'word', value: '1px' }, + { type: 'space', value: ' ' }, + { type: 'word', value: 'solid' }, + ], + [{ type: 'word', value: 'red' }], + ], + symbolName: 'Button', + }, + { + args: null, + symbolName: 'Mixin', + }, + ]); + }); + }); + }); + describe('st-custom-state', () => { + it('should define state on class symbol', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: bool, str(string) def-val, opt(enum(a, b)); + } + `); + + const { meta } = sheets['/entry.st.css']; + expect(meta.getClass('root')!['-st-states']).to.eql({ + bool: null, + str: { type: 'string', arguments: [], defaultValue: 'def-val' }, + opt: { type: 'enum', arguments: ['a', 'b'], defaultValue: '' }, + }); + }); + it('should report state definition in complex or type selector', () => { + testStylableCore(` + .a.b { + /* @analyze-error ${classDiagnostics.STATE_DEFINITION_IN_COMPLEX()} */ + -st-states: some-state; + } + + div { + /* @analyze-error ${classDiagnostics.STATE_DEFINITION_IN_ELEMENT()} */ + -st-states: some-state; + } + `); + }); + it('should report overridden states', () => { + testStylableCore(` + .a { + -st-states: some-state; + } + + .a { + /* @analyze-warn ${classDiagnostics.OVERRIDE_TYPED_RULE('-st-states', 'a')} */ + -st-states: some-state; + } + `); + }); + it('should report parsing diagnostics on -st-states decl', () => { + testStylableCore(` + .a { + /* @analyze-warn ${stCustomStateDiagnostics.NO_STATE_TYPE_GIVEN( + 'state-with-param' + )} */ + -st-states: state-with-param(); + } + `); + }); }); describe(`st-import`, () => { it(`should resolve imported classes`, () => { @@ -896,202 +994,6 @@ describe(`features/css-class`, () => { }); }); }); - describe(`css-pseudo-class`, () => { - // ToDo: move to css-pseudo-class spec once feature is created - describe(`st-var`, () => { - it('should unsupported value() within var definition / call', () => { - const { sheets } = testStylableCore(` - :vars { - optionA: a; - optionB: b; - optionC: c; - } - - .root { - -st-states: - option(enum( - value(optionA), - value(optionB) - )) value(optionB); - } - - /* @rule(default) .entry__root.entry---option-1-b */ - .root:option {} - - /* @rule(target value) .entry__root.entry---option-1-a */ - .root:option(value(optionA)) {} - - /* - @x-transform-error(target invalid) invalid optionC - @rule(target invalid) .entry__root.entry---option-1-c - */ - .root:option(value(optionC)) {} - `); - - const { meta } = sheets['/entry.st.css']; - - shouldReportNoDiagnostics(meta); // ToDo: `target invalid` should report - }); - }); - describe(`st-mixin`, () => { - it.skip('should override value() within var definition / call', () => { - // mixins could be able to gain more power by overriding st-var in state definitions and selectors - const { sheets } = testStylableCore(` - :vars { - optionA: a; - optionB: b; - optionC: c; - optionD: c; - } - - .mix { - -st-states: - option(enum( - value(optionA), - value(optionB) - )) value(optionB); - } - .mix:option {} - .mix:option(value(optionA)) {} - - /* - @rule[1](default) .entry__into.entry---option-1-d - @rule[2](target value) .entry__into.entry---option-1-c - */ - .into { - -st-mixin: mix( - optionA value(optionC), - optionB value(optionD) - ); - } - `); - - const { meta } = sheets['/entry.st.css']; - - shouldReportNoDiagnostics(meta); - }); - it(`should mix custom state`, () => { - const { sheets } = testStylableCore({ - '/base.st.css': ` - .root { - -st-states: toggled; - } - .root:toggled { - value: from base; - } - `, - '/extend.st.css': ` - @st-import Base from './base.st.css'; - Base {} - .root { - -st-extends: Base; - } - .root:toggled { - value: from extend; - } - `, - '/entry.st.css': ` - @st-import Extend, [Base] from './extend.st.css'; - - /* @rule[1] - .entry__a.base--toggled { - value: from base; - } */ - .a { - -st-mixin: Base; - } - - /* - ToDo: change to 1 once empty AST is filtered - @rule[2] - .entry__a.base--toggled { - value: from extend; - } */ - .a { - -st-mixin: Extend; - } - `, - }); - - const { meta } = sheets['/entry.st.css']; - - shouldReportNoDiagnostics(meta); - }); - it(`should mix imported class with custom-pseudo-state`, () => { - // ToDo: fix case where extend.st.css has .root between mix rules: https://shorturl.at/cwBMP - const { sheets } = testStylableCore({ - '/base.st.css': ` - .root { - /* not going to be mixed through -st-extends */ - id: base-root; - -st-states: state; - } - `, - '/extend.st.css': ` - @st-import Base from './base.st.css'; - .root { - -st-extends: Base; - } - .mix { - -st-extends: Base; - id: extend-mix; - } - .mix:state { - id: extend-mix-state; - }; - .root:state { - id: extend-root-state; - } - - `, - '/enrich.st.css': ` - @st-import MixRoot, [mix as mixClass] from './extend.st.css'; - MixRoot { - id: enrich-MixRoot; - } - MixRoot:state { - id: enrich-MixRoot-state; - } - .mixClass { - id: enrich-mixClass; - } - .mixClass:state { - id: enrich-mixClass-state; - } - `, - '/entry.st.css': ` - @st-import [MixRoot, mixClass] from './enrich.st.css'; - - /* - @rule[0] .entry__a { -st-extends: Base; id: extend-mix; } - @rule[1] .entry__a.base--state { id: extend-mix-state; } - @rule[2] .entry__a { id: enrich-mixClass; } - @rule[3] .entry__a.base--state { id: enrich-mixClass-state; } - */ - .a { - -st-mixin: mixClass; - } - - /* - @rule[0] .entry__a { -st-extends: Base; } - @rule[1] .entry__a .extend__mix { -st-extends: Base; id: extend-mix; } - @rule[2] .entry__a .extend__mix.base--state { id: extend-mix-state; } - @rule[3] .entry__a.base--state { id: extend-root-state; } - @rule[4] .entry__a { id: enrich-MixRoot; } - @rule[5] .entry__a.base--state { id: enrich-MixRoot-state; } - */ - .a { - -st-mixin: MixRoot; - } - `, - }); - - const { meta } = sheets['/entry.st.css']; - - shouldReportNoDiagnostics(meta); - }); - }); - }); describe(`css-pseudo-element`, () => { // ToDo: move to css-pseudo-element spec once feature is created describe(`st-mixin`, () => { diff --git a/packages/core/test/features/css-pseudo-class.spec.ts b/packages/core/test/features/css-pseudo-class.spec.ts new file mode 100644 index 0000000000..dee9d69858 --- /dev/null +++ b/packages/core/test/features/css-pseudo-class.spec.ts @@ -0,0 +1,890 @@ +import { STCustomState, CSSPseudoClass } from '@stylable/core/dist/features'; +import { nativePseudoClasses } from '@stylable/core/dist/index-internal'; +import { + testStylableCore, + shouldReportNoDiagnostics, + diagnosticBankReportToStrings, +} from '@stylable/core-test-kit'; +import { expect } from 'chai'; + +const cssPseudoClassDiagnostics = diagnosticBankReportToStrings(CSSPseudoClass.diagnostics); +const stCustomStateDiagnostics = diagnosticBankReportToStrings(STCustomState.diagnostics); + +describe('features/css-pseudo-class', () => { + it('should preserve native pseudo classes', () => { + const src = nativePseudoClasses.map((name) => `.root:${name}{}`).join('\n'); + + const { sheets } = testStylableCore(src); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + expect(meta.targetAst!.toString()).to.eql(src.replace(/\.root/g, '.entry__root')); + }); + it('should report unknown pseudo-class', () => { + testStylableCore(` + /* + @transform-error ${cssPseudoClassDiagnostics.UNKNOWN_STATE_USAGE( + 'unknown-p-class' + )} + @rule .entry__root:unknown-p-class + */ + .root:unknown-p-class {} + `); + }); + describe('st-custom-state', () => { + it('should transform boolean state', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: bool, + exBool(boolean); + } + + /* @rule(boolean) .entry__root.entry--bool */ + .root:bool {} + + /* @rule(explicit boolean) .entry__root.entry--exBool */ + .root:exBool {} + + /* @rule(nested) .entry__root:not(.entry--bool) */ + .root:not(:bool) {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + describe('string parameter', () => { + it('should transform string state', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: str(string), + strWithDef(string) defVal, + strWithDefSpace(string) def val; + } + + /* @rule(base) .entry__root.entry---str-3-val */ + .root:str(val) {} + + /* @rule(strip quotation) .entry__root.entry---str-3-val */ + .root:str("val") {} + + /* @rule(strip quotation single) .entry__root.entry---str-3-val */ + .root:str('val') {} + + /* @rule(space in value) .entry__root.entry---str-7-one_two */ + .root:str(one two) {} + + /* @rule(with default) .entry__root.entry---strWithDef-6-defVal */ + .root:strWithDef {} + + /* @rule(with default) .entry__root.entry---strWithDefSpace-7-def_val */ + .root:strWithDefSpace {} + + /* @rule(escape param) .entry__root.entry---str-2-\\.x */ + .root:str(.x) {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should transform with no diagnostics for valid values', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: rgx(string(regex('^user'))), + cont(string(contains(abc))), + multi(string(minLength(2), maxLength(6))); + } + + /* @rule(regex) .entry__root.entry---rgx-10-user-first */ + .root:rgx(user-first) {} + + /* @rule(contains) .entry__root.entry---cont-9-123abc456 */ + .root:cont(123abc456) {} + + /* @rule(multi-min/max) .entry__root.entry---multi-4-1234 */ + .root:multi(1234) {} + + /* @rule(eql min) .entry__root.entry---multi-2-12 */ + .root:multi(12) {} + + /* @rule(eql max) .entry__root.entry---multi-6-123456 */ + .root:multi(123456) {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should report validation errors', () => { + testStylableCore(` + .root { + -st-states: rgx(string(regex('^user'))), + cont(string(contains(abc))), + multi(string(minLength(10), maxLength(2))); + } + + /* + @transform-error(no param) ${stCustomStateDiagnostics.NO_STATE_ARGUMENT_GIVEN( + 'rgx', + 'string' + )} + @rule(no param) .entry__root.entry---rgx-0- + */ + .root:rgx {} + + /* + @transform-error(no param with parans) ${stCustomStateDiagnostics.NO_STATE_ARGUMENT_GIVEN( + 'rgx', + 'string' + )} + @rule(no param with parans) .entry__root.entry---rgx-0- + */ + .root:rgx() {} + + /* + @transform-error(regex) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'rgx', + 'robot', + [ + STCustomState.sysValidationErrors.string.REGEX_VALIDATION_FAILED( + '^user', + 'robot' + ), + ] + )} + @rule(regex) .entry__root.entry---rgx-5-robot + */ + .root:rgx(robot) {} + + /* + @transform-error(contains) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'cont', + 'xyz', + [ + STCustomState.sysValidationErrors.string.CONTAINS_VALIDATION_FAILED( + 'abc', + 'xyz' + ), + ] + )} + @rule(contains) .entry__root.entry---cont-3-xyz + */ + .root:cont(xyz) {} + + /* + @transform-error(multi) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'multi', + '12345', + [ + STCustomState.sysValidationErrors.string.MIN_LENGTH_VALIDATION_FAILED( + '10', + '12345' + ), + STCustomState.sysValidationErrors.string.MAX_LENGTH_VALIDATION_FAILED( + '2', + '12345' + ), + ] + )} + @rule(multi) .entry__root.entry---multi-5-12345 + */ + .root:multi(12345) {} + `); + }); + }); + describe('number parameter', () => { + it('should transform number state', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: num(number), + numWithDef(number) 42; + } + + /* @rule(base) .entry__root.entry---num-1-5 */ + .root:num(5) {} + + /* @rule(with default) .entry__root.entry---numWithDef-2-42 */ + .root:numWithDef {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should transform with no diagnostics for valid values', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: base(number), + minFive(number(min(5))), + maxFive(number(max(5))), + multipleOfThree(number(multipleOf(3))), + multi(number(min(10), multipleOf(2))); + } + + /* @rule(base) .entry__root.entry---base-2-42*/ + .root:base(42) {} + + /* @rule(min) .entry__root.entry---minFive-1-9 */ + .root:minFive(9) {} + + /* @rule(equal min) .entry__root.entry---minFive-1-5 */ + .root:minFive(5) {} + + /* @rule(max) .entry__root.entry---maxFive-1-2 */ + .root:maxFive(2) {} + + /* @rule(equal max) .entry__root.entry---maxFive-1-5 */ + .root:maxFive(5) {} + + /* @rule(multipleOf) .entry__root.entry---multipleOfThree-2-12 */ + .root:multipleOfThree(12) {} + + /* @rule(multi validations) .entry__root.entry---multi-2-14 */ + .root:multi(14) {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should report validation errors', () => { + testStylableCore(` + .root { + -st-states: base(number), + minFive(number(min(5))), + maxFive(number(max(5))), + multipleOfThree(number(multipleOf(3))), + multi(number(min(10), multipleOf(2))); + } + + /* + @transform-error(no param) ${stCustomStateDiagnostics.NO_STATE_ARGUMENT_GIVEN( + 'base', + 'number' + )} + @rule(no param) .entry__root.entry---base-0- + */ + .root:base {} + + /* + @transform-error(no param with parans) ${stCustomStateDiagnostics.NO_STATE_ARGUMENT_GIVEN( + 'base', + 'number' + )} + @rule(no param with parans) .entry__root.entry---base-0- + */ + .root:base() {} + + /* + @transform-error(base) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'base', + 'text', + [ + STCustomState.sysValidationErrors.number.NUMBER_TYPE_VALIDATION_FAILED( + 'text' + ), + ] + )} + @rule(base) .entry__root.entry---base-4-text + */ + .root:base(text) {} + + /* + @transform-error(min) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'minFive', + '4', + [ + STCustomState.sysValidationErrors.number.MIN_VALIDATION_FAILED( + '4', + '5' + ), + ] + )} + @rule(min) .entry__root.entry---minFive-1-4 + */ + .root:minFive(4) {} + + /* + @transform-error(max) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'maxFive', + '6', + [ + STCustomState.sysValidationErrors.number.MAX_VALIDATION_FAILED( + '6', + '5' + ), + ] + )} + @rule(max) .entry__root.entry---maxFive-1-6 + */ + .root:maxFive(6) {} + + /* + @transform-error(multipleOf) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'multipleOfThree', + '4', + [ + STCustomState.sysValidationErrors.number.MULTIPLE_OF_VALIDATION_FAILED( + '4', + '3' + ), + ] + )} + @rule(multipleOf) .entry__root.entry---multipleOfThree-1-4 + */ + .root:multipleOfThree(4) {} + + /* + @transform-error(multi validations) ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'multi', + '7', + [ + STCustomState.sysValidationErrors.number.MIN_VALIDATION_FAILED( + '7', + '10' + ), + STCustomState.sysValidationErrors.number.MULTIPLE_OF_VALIDATION_FAILED( + '7', + '2' + ), + ] + )} + @rule(multi validations) .entry__root.entry---multi-1-7 + */ + .root:multi(7) {} + `); + }); + }); + describe('enum parameter', () => { + it('should transform enum state', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: opts(enum(small, large)), + optsWithDef(enum(one, two, three)) three; + } + + /* @rule(base) .entry__root.entry---opts-5-small */ + .root:opts(small) {} + + /* @rule(with default) .entry__root.entry---optsWithDef-5-three */ + .root:optsWithDef {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should report validation errors', () => { + testStylableCore(` + .root { + -st-states: size(enum(small, large)); + } + + /* + @transform-error(no param) ${stCustomStateDiagnostics.NO_STATE_ARGUMENT_GIVEN( + 'size', + 'enum' + )} + @rule(no param) .entry__root.entry---size-0- + */ + .root:size {} + + /* + @transform-error(no param with parans) ${stCustomStateDiagnostics.NO_STATE_ARGUMENT_GIVEN( + 'size', + 'enum' + )} + @rule(no param with parans) .entry__root.entry---size-0- + */ + .root:size() {} + + /* + @transform-error ${stCustomStateDiagnostics.FAILED_STATE_VALIDATION( + 'size', + 'huge', + [ + STCustomState.sysValidationErrors.enum.ENUM_TYPE_VALIDATION_FAILED( + 'huge', + ['small', 'large'] + ), + ] + )} + @rule .entry__root.entry---size-4-huge + */ + .root:size(huge) {} + `); + }); + }); + describe('custom mapped parameter', () => { + it('should transform mapped state (quoted)', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: cls(".x"), + escapedAndTrimmed(" .y[data-z=\\"value\\"] "), + nested(":not(:focus-within):not(:hover)"), + valueAsGlobal(":cls"); + } + + /* @rule(base) .entry__root.x */ + .root:cls {} + + /* @rule(escaped and trimmed) .entry__root.y[data-z="value"] */ + .root:escapedAndTrimmed {} + + /* @rule(nested) .entry__root:not(:focus-within):not(:hover) */ + .root:nested {} + + /* @rule(take value as global) .entry__root:cls */ + .root:valueAsGlobal {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + }); + it('should handle escaped characters', () => { + const { sheets } = testStylableCore( + ` + .root { + -st-states: bool\\.ean, + str\\.ing(string), + num\\.ber(number), + en\\.um(enum(x, y)), + map\\.ped(".x"); + } + + /* @rule(escaped) .entry\\.__root.entry\\.--bool\\.ean */ + .root:bool\\.ean {} + + /* @rule(string) .entry\\.__root.entry\\.---str\\.ing-3-abc */ + .root:str\\.ing(abc) {} + + /* @rule(number) .entry\\.__root.entry\\.---num\\.ber-1-5 */ + .root:num\\.ber(5) {} + + /* @rule(enum) .entry\\.__root.entry\\.---en\\.um-1-y */ + .root:en\\.um(y) {} + + /* @rule(mapped) .entry\\.__root.x */ + .root:map\\.ped {} + `, + { + stylableConfig: { + resolveNamespace(namespace) { + return namespace + '.'; + }, + }, + } + ); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + }); + describe(`st-var`, () => { + /* ToDo: consider dropping support for this */ + it('should support value() concatenate value into default', () => { + const { sheets } = testStylableCore(` + :vars { + idPrefix: id-; + } + + .root { + -st-states: s1(string) value(idPrefix)default; + } + + /* @rule(default) .entry__root.entry---s1-10-id-default */ + .root:s1 {} + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it('should support value() within string validation config', () => { + const { sheets } = testStylableCore(` + :vars { + validPrefix: user; + } + + .a { + -st-states: state1(string(contains(value(validPrefix)))); + } + + /* @rule .entry__a.entry---state1-8-userName */ + .a:state1(userName) {} + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it('should support value() within enum definition / call', () => { + const { sheets } = testStylableCore(` + :vars { + optionA: a; + optionB: b; + optionC: c; + } + + .root { + -st-states: + option(enum( + value(optionA), + value(optionB) + )) value(optionB); + } + + /* @rule(default) .entry__root.entry---option-1-b */ + .root:option {} + + /* @rule(target value) .entry__root.entry---option-1-a */ + .root:option(value(optionA)) {} + + /* + @x-transform-error(target invalid) invalid optionC + @rule(target invalid) .entry__root.entry---option-1-c + */ + .root:option(value(optionC)) {} + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); // ToDo: `target invalid` should report + }); + }); + describe(`st-mixin`, () => { + it.skip('should override value() within var definition / call', () => { + // mixins could be able to gain more power by overriding st-var in state definitions and selectors + const { sheets } = testStylableCore(` + :vars { + optionA: a; + optionB: b; + optionC: c; + optionD: c; + } + + .mix { + -st-states: + option(enum( + value(optionA), + value(optionB) + )) value(optionB); + } + .mix:option {} + .mix:option(value(optionA)) {} + + /* + @rule[1](default) .entry__into.entry---option-1-d + @rule[2](target value) .entry__into.entry---option-1-c + */ + .into { + -st-mixin: mix( + optionA value(optionC), + optionB value(optionD) + ); + } + `); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix custom state`, () => { + const { sheets } = testStylableCore({ + '/base.st.css': ` + .root { + -st-states: toggled; + } + .root:toggled { + value: from base; + } + `, + '/extend.st.css': ` + @st-import Base from './base.st.css'; + Base {} + .root { + -st-extends: Base; + } + .root:toggled { + value: from extend; + } + `, + '/entry.st.css': ` + @st-import Extend, [Base] from './extend.st.css'; + + /* @rule[1] + .entry__a.base--toggled { + value: from base; + } */ + .a { + -st-mixin: Base; + } + + /* + ToDo: change to 1 once empty AST is filtered + @rule[2] + .entry__a.base--toggled { + value: from extend; + } */ + .a { + -st-mixin: Extend; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + it(`should mix imported class with custom-pseudo-state`, () => { + // ToDo: fix case where extend.st.css has .root between mix rules: https://shorturl.at/cwBMP + const { sheets } = testStylableCore({ + '/base.st.css': ` + .root { + /* not going to be mixed through -st-extends */ + id: base-root; + -st-states: state; + } + `, + '/extend.st.css': ` + @st-import Base from './base.st.css'; + .root { + -st-extends: Base; + } + .mix { + -st-extends: Base; + id: extend-mix; + } + .mix:state { + id: extend-mix-state; + }; + .root:state { + id: extend-root-state; + } + + `, + '/enrich.st.css': ` + @st-import MixRoot, [mix as mixClass] from './extend.st.css'; + MixRoot { + id: enrich-MixRoot; + } + MixRoot:state { + id: enrich-MixRoot-state; + } + .mixClass { + id: enrich-mixClass; + } + .mixClass:state { + id: enrich-mixClass-state; + } + `, + '/entry.st.css': ` + @st-import [MixRoot, mixClass] from './enrich.st.css'; + + /* + @rule[0] .entry__a { -st-extends: Base; id: extend-mix; } + @rule[1] .entry__a.base--state { id: extend-mix-state; } + @rule[2] .entry__a { id: enrich-mixClass; } + @rule[3] .entry__a.base--state { id: enrich-mixClass-state; } + */ + .a { + -st-mixin: mixClass; + } + + /* + @rule[0] .entry__a { -st-extends: Base; } + @rule[1] .entry__a .extend__mix { -st-extends: Base; id: extend-mix; } + @rule[2] .entry__a .extend__mix.base--state { id: extend-mix-state; } + @rule[3] .entry__a.base--state { id: extend-root-state; } + @rule[4] .entry__a { id: enrich-MixRoot; } + @rule[5] .entry__a.base--state { id: enrich-MixRoot-state; } + */ + .a { + -st-mixin: MixRoot; + } + `, + }); + + const { meta } = sheets['/entry.st.css']; + + shouldReportNoDiagnostics(meta); + }); + }); + describe('css-class (inheritance)', () => { + it('should resolve state from a local extended classes', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: s1; + } + .class { + -st-states: s2; + } + + .fromRoot { + -st-extends: root; + } + .fromClass { + -st-extends: class; + } + + /* @rule .entry__fromRoot.entry--s1 */ + .fromRoot:s1 {} + + /* @rule .entry__fromClass.entry--s2 */ + .fromClass:s2 {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should resolve extended state', () => { + const { sheets } = testStylableCore({ + '/comp.st.css': ` + .root { + -st-states: x; + } + `, + '/entry.st.css': ` + @st-import Comp from './comp.st.css'; + + .local { + -st-extends: Comp; + } + + /* @rule .entry__local.comp--x */ + .local:x {} + `, + }); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should resolve 2nd level inherited state', () => { + const { sheets } = testStylableCore({ + '/comp.st.css': ` + .root { + -st-states: x; + } + `, + '/proxy.st.css': ` + @st-import Comp from './comp.st.css'; + .root { + -st-extends: Comp + } + `, + '/entry.st.css': ` + @st-import Proxy from './proxy.st.css'; + + .local { + -st-extends: Proxy; + } + + /* @rule .entry__local.comp--x */ + .local:x {} + `, + }); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + }); + describe('css-type', () => { + it('should resolve state from element type', () => { + const { sheets } = testStylableCore({ + '/comp.st.css': ` + .root { + -st-states: x; + } + `, + '/pass-through.st.css': ` + @st-import Comp from './comp.st.css'; + .root Comp {} + `, + '/entry.st.css': ` + @st-import [Comp] from './pass-through.st.css'; + + /* @rule .entry__root .comp__root.comp--x */ + .root Comp:x {} + `, + }); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + }); + describe('css-pseudo-element', () => { + it('should resolve state from inherited part', () => { + const { sheets } = testStylableCore({ + '/comp.st.css': ` + .part { + -st-states: x; + } + `, + '/entry.st.css': ` + @st-import Comp from './comp.st.css'; + + /* @rule(from element) .entry__root .comp__root .comp__part.comp--x */ + .root Comp::part:x {} + + .local { + -st-extends: Comp; + } + + /* @rule(from extend) .entry__local .comp__part.comp--x */ + .local::part:x {} + `, + }); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + it('should resolve state from inherited part that inherits the state', () => { + const { sheets } = testStylableCore({ + '/comp.st.css': ` + .root { + -st-states: x, c; + } + `, + '/extend.st.css': ` + @st-import Comp from './comp.st.css'; + .part { + -st-extends: Comp; + -st-states: y, c; + } + `, + '/entry.st.css': ` + @st-import Extend from './extend.st.css'; + + .local { + -st-extends: Extend; + } + + /* @rule(from base) .entry__local .extend__part.comp--x */ + .local::part:x {} + + /* @rule(from extend) .entry__local .extend__part.extend--y */ + .local::part:y {} + + /* @rule(override from extend) .entry__local .extend__part.extend--c */ + .local::part:c {} + `, + }); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + }); + describe('css-media', () => { + it('handle scoping inside media queries', () => { + const { sheets } = testStylableCore(` + @media (max-width: 300px) { + .a { + -st-states: x; + } + + /* @rule(boolean) .entry__a.entry--x */ + .a:x {} + } + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + }); +}); diff --git a/packages/core/test/features/st-custom-state.spec.ts b/packages/core/test/features/st-custom-state.spec.ts new file mode 100644 index 0000000000..bb51a97026 --- /dev/null +++ b/packages/core/test/features/st-custom-state.spec.ts @@ -0,0 +1,278 @@ +import { STCustomState } from '@stylable/core/dist/features'; +import { + testStylableCore, + shouldReportNoDiagnostics, + diagnosticBankReportToStrings, +} from '@stylable/core-test-kit'; +import { reservedFunctionalPseudoClasses } from '@stylable/core/dist/native-reserved-lists'; +import { expect } from 'chai'; + +const stCustomStateDiagnostics = diagnosticBankReportToStrings(STCustomState.diagnostics); + +describe('features/st-custom-state', () => { + /** + * This feature has no direct API atm + * - definition is only integrated by -st-states in the css-class feature + * - usage is in the transformation of css-pseudo-class + * + * In the future we might want to add standalone states + * that can be defined, imported, and referenced + */ + it('basic integration', () => { + const { sheets } = testStylableCore(` + .root { + -st-states: a; + } + + /* @rule .entry__root.entry--a */ + .root:a {} + `); + + const { meta } = sheets['/entry.st.css']; + shouldReportNoDiagnostics(meta); + }); + describe('parsing/analyzing', () => { + /* parsing is tested through the integration for now + as there is not direct user API to define states */ + it('should collect states on classes', () => { + const { sheets } = testStylableCore(` + .bool { + -st-states: b1, b2(boolean); + } + .enum { + -st-states: e1(enum(small, medium, large)), + e2(enum(red, green, blue)) green; + } + .num { + -st-states: n1(number), + n2(number()), + n3(number(min(2), max(6), multipleOf(2))), + n4(number) 4; + } + .str { + -st-states: s1(string), + s2(string()), + s3(string(minLength(2), maxLength(5), contains(abc), regex("^user"))), + s4(string) def val; + } + .map { + -st-states: m1("[some-attr]"), m2('.global-cls'); + } + `); + + const { meta } = sheets['/entry.st.css']; + const classes = meta.getAllClasses(); + shouldReportNoDiagnostics(meta); + expect(classes.bool['-st-states'], 'boolean states').to.eql({ + b1: null, + b2: null, + }); + expect(classes.enum['-st-states'], 'enum states').to.eql({ + e1: { + type: 'enum', + arguments: ['small', 'medium', 'large'], + defaultValue: '', + }, + e2: { + type: 'enum', + arguments: ['red', 'green', 'blue'], + defaultValue: 'green', + }, + }); + expect(classes.num['-st-states'], 'number states').to.eql({ + n1: { type: 'number', arguments: [], defaultValue: '' }, + n2: { type: 'number', arguments: [], defaultValue: '' }, + n3: { + type: 'number', + arguments: [ + { + name: 'min', + args: ['2'], + }, + { + name: 'max', + args: ['6'], + }, + { + name: 'multipleOf', + args: ['2'], + }, + ], + defaultValue: '', + }, + n4: { type: 'number', arguments: [], defaultValue: '4' }, + }); + expect(classes.str['-st-states'], 'string states').to.eql({ + s1: { type: 'string', arguments: [], defaultValue: '' }, + s2: { type: 'string', arguments: [], defaultValue: '' }, + s3: { + type: 'string', + arguments: [ + { + name: 'minLength', + args: ['2'], + }, + { + name: 'maxLength', + args: ['5'], + }, + { + name: 'contains', + args: ['abc'], + }, + { + name: 'regex', + args: ['^user'], + }, + ], + defaultValue: '', + }, + s4: { type: 'string', arguments: [], defaultValue: 'def val' }, + }); + expect(classes.map['-st-states'], 'mapped states').to.eql({ + m1: '[some-attr]', + m2: '.global-cls', + }); + }); + it('should report missing state type', () => { + const { sheets } = testStylableCore(` + .a { + /* @analyze-warn ${stCustomStateDiagnostics.NO_STATE_TYPE_GIVEN('s1')} */ + -st-states: s1(); + } + `); + + const { meta } = sheets['/entry.st.css']; + const classes = meta.getAllClasses(); + expect(classes.a['-st-states'], 'state collected as boolean').to.eql({ + s1: null, + }); + }); + it('should report unknown state type', () => { + const { sheets } = testStylableCore(` + .a { + /* @analyze-error ${stCustomStateDiagnostics.UNKNOWN_STATE_TYPE( + 's1', + 'unknown' + )} */ + -st-states: s1(unknown); + } + `); + + const { meta } = sheets['/entry.st.css']; + const classes = meta.getAllClasses(); + expect(classes.a['-st-states'], 'state not collected').to.eql({}); + }); + it('should report on enum type with no options', () => { + /* ToDo(tech-debt): move to analyze phase + - An issue while build-vars are supported to define default + */ + testStylableCore(` + .a { + /* @transform-error ${stCustomStateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION( + 'e1', + '', + [STCustomState.sysValidationErrors.enum.NO_OPTIONS_DEFINED()] + )} */ + -st-states: e1(enum()); + } + `); + }); + it('should report on validator definition issues', () => { + /* ToDo(tech-debt): move "unknown validator" to analyze phase + - An issue while build-vars are supported to define default + */ + testStylableCore(` + .a { + /* @analyze-error(multi types) ${stCustomStateDiagnostics.TOO_MANY_STATE_TYPES( + 's1', + ['string', 'number'] + )} */ + -st-states: s1(string, number); + } + .b { + /* @analyze-error(multi validation args) ${stCustomStateDiagnostics.TOO_MANY_ARGS_IN_VALIDATOR( + 's2', + 'contains', + ['x', 'y'] + )} */ + -st-states: s2(string(contains(x, y))); + } + .c { + /* @transform-error(unknown str validator) ${stCustomStateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION( + 's3', + '', + [STCustomState.sysValidationErrors.string.UKNOWN_VALIDATOR('unknown')] + )} */ + -st-states: s3(string(unknown())); + } + .d { + /* @transform-error(unknown num validator) ${stCustomStateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION( + 's4', + '', + [STCustomState.sysValidationErrors.number.UKNOWN_VALIDATOR('unknown')] + )} */ + -st-states: s4(number(unknown())); + } + `); + }); + it('should report on non invalid default value', () => { + /* ToDo(tech-debt): move to analyze phase + - An issue while build-vars are supported to define default + */ + testStylableCore(` + .a { + /* @transform-error(multi types) ${stCustomStateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION( + 'n1', + 'abc', + [ + STCustomState.sysValidationErrors.number.NUMBER_TYPE_VALIDATION_FAILED( + 'abc' + ), + ] + )} */ + -st-states: n1(number) abc; + } + .b { + /* @transform-error(multi types) ${stCustomStateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION( + 'e1', + 'huge', + [ + STCustomState.sysValidationErrors.enum.ENUM_TYPE_VALIDATION_FAILED( + 'huge', + ['small', 'large'] + ), + ] + )} */ + -st-states: e1(enum(small, large)) huge; + } + `); + }); + it('should report state that start with a dash (-)', () => { + // ToDo: consider removing this restriction https://github.com/wix/stylable/issues/2625 + testStylableCore(` + .a { + /* @analyze-error ${stCustomStateDiagnostics.STATE_STARTS_WITH_HYPHEN( + '-some-state' + )} */ + -st-states: -some-state; + } + `); + }); + it('should not allow reserved pseudo classes as names', () => { + // prettier-ignore + testStylableCore( + reservedFunctionalPseudoClasses.map((name) => ` + .${name} { + /* @analyze-warn ${stCustomStateDiagnostics.RESERVED_NATIVE_STATE(name)} */ + -st-states: ${name}; + }` + ).join('\n') + ); + }); + }); + + // it('should check for state name collision in the same definition', () => {}); + + // it('should check for type collision in states with the same name', () => {}); +}); diff --git a/packages/core/test/pseudo-states.spec.ts b/packages/core/test/pseudo-states.spec.ts deleted file mode 100644 index fe02dbb0c4..0000000000 --- a/packages/core/test/pseudo-states.spec.ts +++ /dev/null @@ -1,2211 +0,0 @@ -import chai, { expect } from 'chai'; -import chaiSubset from 'chai-subset'; -import { - flatMatch, - mediaQuery, - styleRules, - expectAnalyzeDiagnostics, - expectTransformDiagnostics, - generateStylableResult, - processSource, - testInlineExpects, - diagnosticBankReportToStrings, -} from '@stylable/core-test-kit'; -import { nativePseudoClasses } from '@stylable/core/dist/index-internal'; -import { reservedFunctionalPseudoClasses } from '@stylable/core/dist/native-reserved-lists'; -import { STCustomState, CSSClass, CSSType, CSSPseudoClass } from '@stylable/core/dist/features'; - -chai.use(chaiSubset); // move all of these to a central place -chai.use(styleRules); -chai.use(mediaQuery); -chai.use(flatMatch); - -// testing concerns for feature -// - states belonging to an extended class (multi level) -// - lookup order - -const stateStringDiagnostics = diagnosticBankReportToStrings(STCustomState.diagnostics); -const cssTypeDiagnostics = diagnosticBankReportToStrings(CSSType.diagnostics); -const cssClassDiagnostics = diagnosticBankReportToStrings(CSSClass.diagnostics); -const CSSPseudoClassDiagnostics = diagnosticBankReportToStrings(CSSPseudoClass.diagnostics); - -describe('pseudo-states', () => { - describe('process', () => { - // What does it do? - // Works in the scope of a single file, collecting state definitions for later usage - describe(`reserved pseudo classes`, () => { - reservedFunctionalPseudoClasses.forEach((name) => { - it(`should NOT collect "${name}"`, () => { - const meta = processSource( - ` - .root { - -st-states: custom-only, ${name}; - }`, - { from: 'path/to/style.css' } - ); - expect(meta.getAllClasses()).to.flatMatch({ - root: { - '-st-states': { - 'custom-only': null, - }, - }, - }); - expectAnalyzeDiagnostics( - `.root{ - |-st-states: $${name}$|; - }`, - [ - { - message: stateStringDiagnostics.RESERVED_NATIVE_STATE(name), - file: 'main.css', - }, - ] - ); - }); - }); - }); - describe('boolean', () => { - it('should collect state definitions as null (for boolean)', () => { - const meta = processSource( - ` - .root { - -st-states: state1, state2; - } - `, - { from: 'path/to/style.css' } - ); - - expect(meta.diagnostics.reports.length, 'no reports').to.eql(0); - expect(meta.getAllClasses()).to.flatMatch({ - root: { - '-st-states': { - state1: null, - state2: null, - }, - }, - }); - }); - - it('should support explicit boolean state definition', () => { - const res = processSource( - ` - .root { - -st-states: state1(boolean); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: null, - }, - }, - }); - }); - }); - - describe('advanced type', () => { - it('should warn when a state receives more than a single state type', () => { - expectAnalyzeDiagnostics( - ` - .root{ - |-st-states: $state1(string, number(x))$|; - } - `, - [ - { - message: stateStringDiagnostics.TOO_MANY_STATE_TYPES('state1', [ - 'string', - 'number(x)', - ]), - file: 'main.css', - }, - ] - ); - }); - - it('should warn when a state function receives no arguments', () => { - expectAnalyzeDiagnostics( - ` - .root{ - |-st-states: $state1()$|; - } - `, - [ - { - message: stateStringDiagnostics.NO_STATE_TYPE_GIVEN('state1'), - file: 'main.css', - }, - ] - ); - }); - - it('should warn when a validator function receives more than a single argument', () => { - expectAnalyzeDiagnostics( - ` - .my-class { - |-st-states: $state1( string( contains(one, two) ) )$|; - } - `, - [ - { - message: stateStringDiagnostics.TOO_MANY_ARGS_IN_VALIDATOR( - 'state1', - 'contains', - ['one', 'two'] - ), - file: 'main.css', - }, - ] - ); - }); - - it('should warn when encountering an unknown type', () => { - expectAnalyzeDiagnostics( - ` - .my-class { - |-st-states: state1( $unknown$ )|; - } - `, - [ - { - message: stateStringDiagnostics.UNKNOWN_STATE_TYPE('state1', 'unknown'), - file: 'main.css', - }, - ] - ); - }); - - describe('string', () => { - it('as a simple validator', () => { - const res = processSource( - ` - .root { - -st-states: state1(string); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'string', - }, - }, - }, - }); - }); - - it('as a validation type with no nested validations', () => { - const res = processSource( - ` - .root { - -st-states: state1(string()); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'string', - }, - }, - }, - }); - }); - - it('including a default value', () => { - const res = processSource( - ` - .root { - -st-states: state1(string) some Default String; - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - defaultValue: 'some Default String', - type: 'string', - }, - }, - }, - }); - }); - - it('with a regex validator', () => { - const res = processSource( - ` - .root { - -st-states: state1( string( regex("^user") )); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'string', - arguments: [ - { - name: 'regex', - args: ['^user'], - }, - ], - }, - }, - }, - }); - }); - - it('with a single nested validator', () => { - const res = processSource( - ` - .root { - -st-states: state1(string(minLength(2))); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'string', - arguments: [ - { - name: 'minLength', - args: ['2'], - }, - ], - }, - }, - }, - }); - }); - - it('with multiple validators', () => { - // this test also shows that all validator params are treated as strings - const res = processSource( - ` - .root { - -st-states: state1(string(minLength(2), maxLength("7"))); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'string', - arguments: [ - { - name: 'minLength', - args: ['2'], - }, - { - name: 'maxLength', - args: ['7'], - }, - ], - }, - }, - }, - }); - }); - - it('with a nested validator and a regex validator', () => { - const res = processSource( - ` - .root { - -st-states: state1(string( regex("^user"), contains(user) )); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'string', - arguments: [ - { - name: 'regex', - args: ['^user'], - }, - { - name: 'contains', - args: ['user'], - }, - ], - }, - }, - }, - }); - }); - }); - - describe('number', () => { - it('as a simple validator', () => { - const res = processSource( - ` - .root { - -st-states: state1(number), state2(number()); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - type: 'number', - }, - state2: { - type: 'number', - }, - }, - }, - }); - }); - - it('including a default value', () => { - const res = processSource( - ` - .root { - -st-states: state1(number) 7; - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - state1: { - defaultValue: '7', - type: 'number', - }, - }, - }, - }); - }); - }); - - describe('enum', () => { - it('as a simple validator', () => { - const res = processSource( - ` - .root { - -st-states: size(enum(small, medium, large)), color(enum(red, green, blue)); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - size: { - type: 'enum', - arguments: ['small', 'medium', 'large'], - }, - color: { - type: 'enum', - arguments: ['red', 'green', 'blue'], - }, - }, - }, - }); - }); - - it('including a default value', () => { - const res = processSource( - ` - .root { - -st-states: size(enum(small, large)) small; - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - expect(res.getAllClasses()).to.containSubset({ - root: { - '-st-states': { - size: { - defaultValue: 'small', - type: 'enum', - arguments: ['small', 'large'], - }, - }, - }, - }); - }); - }); - }); - - describe('custom mapping', () => { - it('collect typed classes with mapping states', () => { - const res = processSource( - ` - .root { - -st-states: state1, state2("[data-mapped]"); - } - `, - { from: 'path/to/style.css' } - ); - - expect(res.diagnostics.reports.length, 'no reports').to.eql(0); - expect(res.getAllClasses()).to.flatMatch({ - root: { - '-st-states': { - state1: null, // boolean - state2: '[data-mapped]', - }, - }, - }); - }); - }); - }); - - describe('transform', () => { - // What does it do? - // Replaces all custom state definitions (based on information gather during processing) - // with their final selector string - - describe('native', () => { - nativePseudoClasses.forEach((nativeClass: string) => { - it(`should keep native ${nativeClass} pseudo-class`, () => { - const res = generateStylableResult({ - entry: '/entry.css', - files: { - '/entry.css': { - namespace: 'entry', - content: `.root:${nativeClass}{}`, - }, - }, - }); - - expect(res).to.have.styleRules([`.entry__root:${nativeClass}{}`]); - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - }); - }); - }); - - describe('boolean', () => { - it('should resolve boolean pseudo-state', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1; - } - .my-class:state1 {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, []); - - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry--state1 {}', - }); - }); - - it('should resolve nested pseudo-states', () => { - const res = generateStylableResult({ - entry: '/entry.st.css', - usedFiles: ['/entry.st.css'], - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .root { - -st-states: state1; - } - .root:not(:state1) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__root:not(.entry--state1) {}', - }); - }); - - it('should support explicitly defined boolean state type', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(boolean); - } - .my-class:state1 {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry--state1 {}', - }); - }); - - it('should accept escaped state', () => { - const res = generateStylableResult({ - entry: '/entry.st.css', - usedFiles: ['/entry.st.css'], - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .root { - -st-states: state1\\.; - } - .root:state1\\. {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__root.entry--state1\\. {}', - }); - }); - - it('should escape namespace', () => { - const res = generateStylableResult({ - entry: '/entry.st.css', - usedFiles: ['/entry.st.css'], - files: { - '/entry.st.css': { - namespace: 'entr.y', - content: ` - .root { - -st-states: state1; - } - .root:state1 {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entr\\.y__root.entr\\.y--state1 {}', - }); - }); - }); - - describe('advanced type / validation', () => { - it('should strip quotation marks when transform any state parameter', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string); - } - .my-class:state1("someString") {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-10-someString {}', - }); - }); - - it('should use an escaped class selector for illegal param syntax (and replaces spaces with underscores)', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .root { - -st-states: state( string()); - } - .root:state(user name) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__root.entry---state-9-user_name {}', - }); - }); - - it('should support default values when invoked', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: stateWithDefault(string) aDefaultValue; - } - .my-class:stateWithDefault() {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---stateWithDefault-13-aDefaultValue {}', - }); - }); - - describe('string', () => { - it('should transform string type', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string); - } - .my-class:state1(someString) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-10-someString {}', - }); - }); - - it('should support default values for string type', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: stateWithDefault(string) myDefault String; - } - .my-class:stateWithDefault {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---stateWithDefault-16-myDefault_String {}', - }); - }); - - it('should support default values through a variable', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - myID: user; - } - - .my-class { - -st-states: stateWithDefault(string) value(myID)name; - } - .my-class:stateWithDefault {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---stateWithDefault-8-username {}', - }); - }); - - it('should accept escaped name', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state\\.1(string); - } - .my-class:state\\.1(someString) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state\\.1-10-someString {}', - }); - }); - - it('should escape namespace', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entr.y', - content: ` - .my-class { - -st-states: state1(string); - } - .my-class:state1(someString) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entr\\.y__my-class.entr\\.y---state1-10-someString {}', - }); - }); - - describe('specific validators', () => { - it('should transform string using a valid regex validation', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1( string( regex("^user") )); - } - .my-class:state1(userName) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-8-userName {}', - }); - }); - - it('should warn when using an invalid regex validation', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1( string( regex("^user") )); - } - |.my-class:state1(failingParameter)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "failingParameter" failed validation:', - 'expected "failingParameter" to match regex "^user"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-16-failingParameter {}', - }); - }); - - it('should transform string using a valid contains validator', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string(contains(user))); - } - .my-class:state1(userName) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-8-userName {}', - }); - }); - - it('should transform string using a contains validator with a variable', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - validPrefix: user; - } - - .my-class { - -st-states: state1(string(contains(value(validPrefix)))); - } - .my-class:state1(userName) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-8-userName {}', - }); - }); - - it('should transform string using an invalid contains validator (maintains passed values)', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string(contains(user))); - } - |.my-class:state1($wrongState$)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "wrongState" failed validation:', - 'expected "wrongState" to contain string "user"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-10-wrongState {}', - }); - }); - - it('should transform using multiple validators (regex, minLength, maxLength)', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1( string( regex("^user"), minLength(3), maxLength(5) )); - } - .my-class:state1(user) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-4-user {}', - }); - }); - - it('should transform and warn when passing an invalid value to a minLength validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string(minLength(7))); - } - |.my-class:state1($user$)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "user" failed validation:', - 'expected "user" to be of length longer than or equal to 7', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-4-user {}', - }); - }); - - it('should not warn when passing a value whose length equals minLength validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string(minLength(7))); - } - |.my-class:state1(hello!!)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, []); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-7-hello\\!\\! {}', - }); - }); - - it('should transform and warn when passing an invalid value to a maxLength validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string(maxLength(3))); - } - |.my-class:state1($user$)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "user" failed validation:', - 'expected "user" to be of length shorter than or equal to 3', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-4-user {}', - }); - }); - - it('should not warn when passing a value whose length equals maxLength validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string(maxLength(3))); - } - |.my-class:state1(abc)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, []); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-3-abc {}', - }); - }); - - it('should transform and warn when passing an invalid value to a multiple validators', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1( string( maxLength(3), regex("^case") )); - } - |.my-class:state1($user$)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "user" failed validation:', - 'expected "user" to be of length shorter than or equal to 3', - 'expected "user" to match regex "^case"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-4-user {}', - }); - }); - - it('should warn when trying to use an unknown string validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - |-st-states: $state1(string(missing()))$|; - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" default value "" failed validation:', - 'encountered unknown string validator "missing"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - }); - }); - }); - - describe('number', () => { - it('should transform number validator', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(number); - } - .my-class:state1(42) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-2-42 {}', - }); - }); - - it('should warn when a non-number default value is invoked', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - |-st-states: $state1(number) defaultBlah$|; - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" default value "defaultBlah" failed validation:', - 'expected "defaultBlah" to be of type number', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - }); - - it('should warn on an non-number value passed', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number); - } - |.my-class:state1(blah)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "blah" failed validation:', - 'expected "blah" to be of type number', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-4-blah {}', - }); - }); - - it('should warn when trying to use an unknown number validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - |-st-states: $state1( number( missing() ))$|; - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" default value "" failed validation:', - 'encountered unknown number validator "missing"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - }); - - it('should transform state param ignoring possible selector symbols', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .abc {} - .my-class { - -st-states: state1(string); - } - /* @check .entry__my-class.entry---state1-4-\\.abc */ - .my-class:state1(.abc) {} - `, - }, - }, - }); - - testInlineExpects(res.meta.targetAst!); - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - }); - - describe('specific validators', () => { - it('should warn on invalid min validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number(min(3))); - } - |.my-class:state1(1)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "1" failed validation:', - 'expected "1" to be larger than or equal to 3', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-1-1 {}', - }); - }); - - it('should warn on invalid max validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number(max(5))); - } - |.my-class:state1(42)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "42" failed validation:', - 'expected "42" to be lesser then or equal to 5', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-2-42 {}', - }); - }); - - it('should warn on invalid multipleOf validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number(multipleOf(5))); - } - |.my-class:state1(42)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "state1" with parameter "42" failed validation:', - 'expected "42" to be a multiple of 5', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-2-42 {}', - }); - }); - - it('should not warn on valid min/max/multipleOf validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number(min(3), max(100), multipleOf(5))); - } - |.my-class:state1(40)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, []); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-2-40 {}', - }); - }); - - it('should not warn when value equals to min validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number(min(3))); - } - |.my-class:state1(3)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, []); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-1-3 {}', - }); - }); - - it('should not warn when value equals to max validator', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class{ - -st-states: state1(number(max(3))); - } - |.my-class:state1(3)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, []); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-1-3 {}', - }); - }); - }); - }); - - describe('enum', () => { - describe('definition', () => { - it('should warn when an enum is defined with no options', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - |-st-states: size( enum() )|; - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "size" default value "" failed validation:', - 'expected enum to be defined with one option or more', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - }); - - it('should warn when a default value does not equal one of the options provided', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - |-st-states: $size( enum(small, large)) huge$|; - } - `, - }, - }, - }; - - expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "size" default value "huge" failed validation:', - 'expected "huge" to be one of the options: "small, large"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - }); - }); - - it('should transform enum validator', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: size( enum(small, large) ); - } - .my-class:size(small) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---size-5-small {}', - }); - }); - - it('should transform enum validator with variables in definition and usage', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :vars { - small: small; - large: large; - } - .my-class { - -st-states: size( enum( value(small), value(large) ) ); - } - .my-class:size(value(small)) {} - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for native states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---size-5-small {}', - }); - }); - - it('should warn when a value does not match any of the enum options', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: size( enum(small, large) ); - } - |.my-class:size(huge)| {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: [ - 'pseudo-state "size" with parameter "huge" failed validation:', - 'expected "huge" to be one of the options: "small, large"', - ].join('\n'), - file: '/entry.st.css', - }, - ]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---size-4-huge {}', - }); - }); - }); - }); - - describe('custom mapping', () => { - it('should transform any quoted string (trimmed)', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: my-state('.X'), my-other-state(" .y[data-z=\\"value\\"] "); - } - .my-class:my-state {} - .my-class:my-other-state {} - `, - }, - }, - }); - - expect(res).to.have.styleRules({ - 1: '.entry__my-class.X {}', - 2: '.entry__my-class.y[data-z="value"] {}', - }); - }); - - it('should not transform any internal state look-alike', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .root { - -st-states: open(":not(:focus-within):not(:hover)"); - } - .root:open {} - `, - }, - }, - }); - - expect(res).to.have.styleRules({ - 1: '.entry__root:not(:focus-within):not(:hover) {}', - }); - }); - }); - - describe('inheritance', () => { - it('should resolve extended type state', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./inner.st.css"; - -st-default: Inner; - } - .my-class { - -st-extends: Inner; - } - .my-class:my-state {} - `, - }, - '/inner.st.css': { - namespace: 'inner', - content: ` - .root { - -st-states: my-state; - } - `, - }, - }, - }); - - expect( - res.meta.diagnostics.reports, - 'no diagnostics reported for imported states' - ).to.eql([]); - expect(res).to.have.styleRules({ - 1: '.entry__my-class.inner--my-state {}', - }); - }); - - it('should resolve override type state', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./extended-state.st.css"; - -st-default: ExtendedState; - } - :import { - -st-from: "./proxy-state.st.css"; - -st-default: ProxyState; - } - .direct { - -st-extends: ExtendedState; - -st-states: my-state; - } - .proxy { - -st-extends: ProxyState; - -st-states: my-state; - } - .direct:my-state {} - .proxy:my-state {} - `, - }, - '/proxy-state.st.css': { - namespace: 'proxyState', - content: ` - :import { - -st-from: "./inner.st.css"; - -st-default: ExtendedState; - } - .root { - -st-extends: ExtendedState; - } - `, - }, - '/extended-state.st.css': { - namespace: 'extendedState', - content: ` - .root { - -st-states: my-state; - } - `, - }, - }, - }); - - expect(res).to.have.styleRules({ - 2: '.entry__direct.entry--my-state {}', - 3: '.entry__proxy.entry--my-state {}', - }); - }); - - it('state lookup when exported as element', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import{ - -st-from: "./index.st.css"; - -st-named: Element; - } - .root Element:disabled{} - `, - }, - '/index.st.css': { - namespace: 'index', - content: ` - :import{ - -st-from: "./element.st.css"; - -st-default: Element; - } - .root Element{} - `, - }, - '/element.st.css': { - namespace: 'element', - content: ` - .root { - -st-states: disabled; - } - `, - }, - }, - }); - - expect( - result.meta.diagnostics.reports, - 'no diagnostics reported for imported states' - ).to.eql([]); - expect(result).to.have.styleRules({ - 0: '.entry__root .element__root.element--disabled{}', - }); - }); - - it('should resolve state of pseudo-element', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./imported.st.css"; - -st-default: Imported; - } - .local { - -st-extends: Imported; - } - .local::inner:my-state {} - Imported::inner:my-state {} - `, - }, - '/imported.st.css': { - namespace: 'imported', - content: ` - .inner { - -st-states: my-state; - } - `, - }, - }, - }); - - expect(res).to.have.styleRules({ - 1: '.entry__local .imported__inner.imported--my-state {}', - 2: '.imported__root .imported__inner.imported--my-state {}', - }); - }); - - it('should resolve state from pseudo-element that inherits the state ', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import{ - -st-from: "./type.st.css"; - -st-default: Type; - } - .my-class { - -st-extends: Type; - } - .my-class::element:my-state {} - `, - }, - '/type.st.css': { - namespace: 'type', - content: ` - :import { - -st-from: "./with-state.st.css"; - -st-default: WithState; - } - .element { - -st-extends: WithState; - } - `, - }, - '/with-state.st.css': { - namespace: 'withState', - content: ` - .root { - -st-states: my-state; - } - `, - }, - }, - }); - - expect(res).to.have.styleRules({ - 1: '.entry__my-class .type__element.withState--my-state {}', - }); - }); - }); - - describe('@media', () => { - it('handle scoping inside media queries', () => { - const res = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - @media (max-width: 300px) { - .my-class { - -st-states: my-state; - } - .my-class:my-state {} - } - `, - }, - }, - }); - - expect(res).to.have.mediaQuery(0).with.styleRules({ - 1: '.entry__my-class.entry--my-state {}', - }); - }); - }); - - describe('extends local root with states', () => { - it('resolve states from extended local root', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .root { - -st-states: disabled; - } - - .x { - -st-extends: root; - } - - .x:disabled {} - `, - }, - }, - }); - - expect( - result.meta.diagnostics.reports, - 'no diagnostics reported for imported states' - ).to.eql([]); - expect(result).to.have.styleRules({ - 2: '.entry__x.entry--disabled {}', - }); - }); - - it('resolve states from extended local class', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .y { - -st-states: disabled; - } - - .x { - -st-extends: y; - } - - .x:disabled {} - `, - }, - }, - }); - - expect( - result.meta.diagnostics.reports, - 'no diagnostics reported for imported states' - ).to.eql([]); - expect(result).to.have.styleRules({ - 2: '.entry__x.entry--disabled {}', - }); - }); - }); - - describe('state after pseudo-element', () => { - it('transform states after pseudo-element that extends states', () => { - const result = generateStylableResult({ - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - :import { - -st-from: "./menu.st.css"; - -st-default: Menu; - } - - .menu1 { - -st-extends: Menu; - } - - .menu1::button:state {} /*TEST_SUBJECT*/ - `, - }, - '/menu.st.css': { - namespace: 'menu', - content: ` - :import { - -st-from: "./button.st.css"; - -st-default: Button; - } - .button { - -st-extends: Button; - -st-states: state; - } - `, - }, - '/button.st.css': { - namespace: 'button', - content: ``, - }, - }, - }); - - expect( - result.meta.diagnostics.reports, - 'no diagnostics reported for imported states' - ).to.eql([]); - expect(result).to.have.styleRules({ - 1: '.entry__menu1 .menu__button.menu--state {}', - }); - }); - }); - }); - - describe('diagnostics', () => { - // TODO: Add warning implementation - xit('should return warning for state without selector', () => { - expectAnalyzeDiagnostics( - ` - |:hover|{ - - } - `, - [ - { - message: 'global states are not supported, use .root:hover instead', - file: 'main.css', - }, - ] - ); - }); - - it('should warn when pseudo-class expects params but none was given (no default)', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string); - } - |.my-class:$state1$ {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: stateStringDiagnostics.NO_STATE_ARGUMENT_GIVEN('state1', 'string'), - file: '/entry.st.css', - severity: 'error', - }, - ]); - - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-0- {}', - }); - }); - - it('should warn when pseudo-class invoked and expects params but none was given', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: ` - .my-class { - -st-states: state1(string); - } - |.my-class:$state1$() {} - `, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: stateStringDiagnostics.NO_STATE_ARGUMENT_GIVEN('state1', 'string'), - file: '/entry.st.css', - severity: 'error', - }, - ]); - - expect(res).to.have.styleRules({ - 1: '.entry__my-class.entry---state1-0- {}', - }); - }); - - it('should trigger a warning when trying to target an unknown state and keep the state', () => { - const config = { - entry: `/entry.st.css`, - files: { - '/entry.st.css': { - namespace: 'entry', - content: `|.root:$unknownState$|{}`, - }, - }, - }; - - const res = expectTransformDiagnostics(config, [ - { - message: CSSPseudoClassDiagnostics.UNKNOWN_STATE_USAGE('unknownState'), - file: '/entry.st.css', - }, - ]); - expect(res, 'keep unknown state').to.have.styleRules([`.entry__root:unknownState{}`]); - }); - - it('should warn when defining states in complex selector', () => { - // TODO: add more complex scenarios - expectAnalyzeDiagnostics( - ` - .gaga:hover { - |-st-states|:shmover; - } - `, - [ - { - message: 'cannot define pseudo states inside complex selectors', - file: 'main.css', - }, - ] - ); - }); - - it('should warn when defining a state inside a type selector', () => { - expectAnalyzeDiagnostics( - ` - MyElement { - |-st-states|:shmover; - } - `, - [ - // skipping root scoping warning - { - message: cssTypeDiagnostics.UNSCOPED_TYPE_SELECTOR('MyElement'), - file: 'main.css', - skip: true, - }, - { - message: cssClassDiagnostics.STATE_DEFINITION_IN_ELEMENT(), - file: 'main.css', - }, - ] - ); - }); - - it('should warn when overriding class states', () => { - expectAnalyzeDiagnostics( - ` - .root { - -st-states: mystate; - } - .root { - |-st-states: mystate2;| - } - `, - [ - { - message: cssClassDiagnostics.OVERRIDE_TYPED_RULE('-st-states', 'root'), - file: 'main.css', - }, - ] - ); - }); - - it('should warn when defining a state starting with a "-"', () => { - expectAnalyzeDiagnostics( - ` - .root { - |-st-states: $-someState$|; - } - `, - [ - { - message: stateStringDiagnostics.STATE_STARTS_WITH_HYPHEN('-someState'), - file: 'main.css', - severity: 'error', - }, - ] - ); - }); - - // TODO: test for case insensitivity in validators - - // it('should check for state name collision in the same definition', () => {}); - - // it('should check for type collision in states with the same name', () => {}); - }); -});