import { describe, it, assert } from 'vitest' import * as Style from '../../src/style/index.js' import { CSS } from '@sitecore-feaas/sdk' import { getSelectorWeight, getSelectorWithWeight } from '../../src/index.js' describe('Styles', () => { describe('AST', () => { var context = {} it('should compile simple expressions', () => { assert.equal(CSS.process([]), '') assert.deepEqual(CSS.process([['length', 1]]), ['1px']) assert.deepEqual(CSS.process([['length', 0]]), ['0px']) assert.deepEqual(CSS.process([['length', null, 'none']]), ['none']) assert.deepEqual(CSS.process([['property', 'test', 'value']]), ['test: value;']) assert.deepEqual(CSS.process(CSS.reify([['property', 'test', () => 1]])), ['test: 1;']) assert.deepEqual(CSS.process(CSS.reify([['property', 'test', ({}) => 2]], { test: 2 })), ['test: 2;']) assert.deepEqual(CSS.process(CSS.reify([['property', 'test', () => [['length', 1]]]])), ['test: 1px;']) assert.deepEqual(CSS.process([['property', 'test', 'value', 'another']]), ['test: value another;']) }) it('should compile blocks', () => { assert.deepEqual(CSS.process([['rule', 'abc', '']]), ['abc {', [''], '}']) assert.deepEqual(CSS.process([['comment']]), []) assert.deepEqual(CSS.process([['comment', 1]]), ['/*', [1], '*/']) assert.deepEqual(CSS.process([['comment', 1, 2]]), ['/*', [1, 2], '*/']) assert.deepEqual(CSS.process(CSS.unnest([['comment', 1, 2]])), ['/*', [1, 2], '*/']) assert.deepEqual(CSS.process([['comment', '123']]), ['/*', ['123'], '*/']) assert.deepEqual(CSS.process([['comment', '123', '321']]), ['/*', ['123', '321'], '*/']) assert.deepEqual(CSS.process([['rule', 'selector']]), ['selector {', [], '}']) assert.deepEqual(CSS.process([['rule', 'selector', 'content']]), ['selector {', ['content'], '}']) assert.deepEqual(CSS.process([['media', 'selector', 'content']]), ['@media selector {', ['content'], '}']) }) it('should generate weights', () => { assert.deepEqual(getSelectorWeight(0, 0, 0), '') assert.deepEqual(getSelectorWeight(1, 0, 0), ':not(#_)') assert.deepEqual(getSelectorWeight(1, 1, 0), ':not(._#_)') assert.deepEqual(getSelectorWeight(1, 1, 1), ':not(x._#_)') assert.deepEqual(getSelectorWeight(2, 0, 0), ':not(#_#_)') assert.deepEqual(getSelectorWeight(2, 1, 0), ':not(._#_#_)') assert.deepEqual(getSelectorWeight(2, 1, 1), ':not(x._#_#_)') assert.deepEqual(getSelectorWeight(2, 1, 0), ':not(._#_#_)') assert.deepEqual(getSelectorWeight(2, 2, 0), ':not(._._#_#_)') assert.deepEqual(getSelectorWeight(2, 2, 1), ':not(x._._#_#_)') assert.deepEqual(getSelectorWeight(2, 1, 1), ':not(x._#_#_)') assert.deepEqual(getSelectorWeight(2, 2, 1), ':not(x._._#_#_)') assert.deepEqual(getSelectorWeight(2, 2, 2), ':not(x):not(x._._#_#_)') assert.deepEqual(getSelectorWeight(0, 0, 2), ':not(x):not(x)') assert.deepEqual(getSelectorWeight(0, 1, 2), ':not(x):not(x._)') }) it('should adjust weights', () => { assert.deepEqual(getSelectorWithWeight('div:weight(0.0.0)'), 'div') assert.deepEqual(getSelectorWithWeight('div:weight(0.-1.0)'), 'div') assert.deepEqual(getSelectorWithWeight('.div:weight(0.-1.0)'), '') assert.deepEqual(getSelectorWithWeight('.theme .div:weight(0.-1.0)'), '.div') assert.deepEqual(getSelectorWithWeight('#test .theme .div:weight(0.-1.0)'), '#test .div') assert.deepEqual(getSelectorWithWeight('div:before:weight(1.0.0)'), 'div:not(#_):before') assert.deepEqual(getSelectorWithWeight('div:weight(1.0.0):before'), 'div:not(#_):before') assert.deepEqual(getSelectorWithWeight('div:weight(1.0.0)'), 'div:not(#_)') assert.deepEqual(getSelectorWithWeight('div:weight(1.1.0)'), 'div:not(._#_)') assert.deepEqual(getSelectorWithWeight('div:weight(1.1.1)'), 'div:not(x._#_)') assert.deepEqual(getSelectorWithWeight('div:weight(0.1.1)'), 'div:not(x._)') assert.deepEqual(getSelectorWithWeight('div:weight(0.0.1)'), 'div:not(x)') assert.deepEqual(getSelectorWithWeight('div:weight(0.0.0)'), 'div') assert.deepEqual(getSelectorWithWeight('div:weight(1.0.0) a:weight(1.1.1)'), 'div a:not(x._#_#_)') assert.deepEqual(getSelectorWithWeight('div:weight(1.1.0) a:weight(1.1.1)'), 'div a:not(x._._#_#_)') assert.deepEqual(getSelectorWithWeight('div:weight(1.1.1) a:weight(1.1.1)'), 'div a:not(x):not(x._._#_#_)') assert.deepEqual(getSelectorWithWeight('div:weight(0.1.1) a:weight(1.1.1)'), 'div a:not(x):not(x._._#_)') assert.deepEqual(getSelectorWithWeight('div:weight(0.0.1) a:weight(1.1.1)'), 'div a:not(x):not(x._#_)') }) it('should stirngify empty rule', () => { assert.deepEqual(CSS.reify(['rule', 'abc', '']), ['rule', 'abc', '']) assert.deepEqual(CSS.unnest(['rule', 'abc', '']), [['rule', 'abc', '']]) assert.deepEqual(CSS.simplify([['rule', 'abc', '']]), [['rule', ['abc'], '']]) assert.deepEqual(CSS.process([['rule', ['abc'], '']]), ['abc {', [''], '}']) assert.deepEqual(CSS.concat(['abc {', [''], '}']), `abc {\n \n}`) assert.deepEqual(CSS.stringify(['rule', 'abc', '']), `abc {\n \n}`) assert.deepEqual(CSS.stringify(['', '']), `\n`) }) it('should compile extends', () => { assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector1'], ['extend', 'selector2']], ['rule', 'selector1', 'Hello'], ['rule', 'selector2', 'Bye'] ]) ) ), ['selector1,\nalias {', ['Hello'], '}', 'selector2,\nalias {', ['Bye'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector1']], ['rule', 'selector1-1', 'Hello'] ]) ) ), ['selector1-1 {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector1']], ['rule', 'selector1+1', 'Hello'] ]) ) ), ['selector1+1,\nalias+1 {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector1']], ['rule', 'selector1:not(1)', 'Hello'] ]) ) ), ['selector1:not(1),\nalias:not(1) {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'wrapper alias', ['extend', '.target']], ['rule', 'a + .target h3', 'Hello'], ['rule', 'a .target', 'Bye'] ]) ) ), ['a + .target h3,\nwrapper a + alias h3 {', ['Hello'], '}', 'a .target,\nwrapper a alias {', ['Bye'], '}'] ) assert.deepEqual(CSS.process(CSS.simplify(CSS.unnest(['rule', 'alias', ['extend', 'selector']]))), []) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector']], ['rule', 'selector', 'Hello'] ]) ) ), ['selector,\nalias {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector']], ['rule', 'selector', 'Hello'] ]) ) ), ['selector,\nalias {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', ['alias1', 'alias2'], ['extend', 'selector']], ['rule', 'selector', 'Hello'] ]) ) ), ['selector,\nalias1,\nalias2 {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', ['alias1', 'alias2'], ['extend', 'selector1']], ['rule', ['selector1', 'selector2'], 'Hello'] ]) ) ), ['selector1,\nselector2,\nalias1,\nalias2 {', ['Hello'], '}'] ) assert.deepEqual( CSS.process( CSS.simplify( CSS.unnest([ ['rule', ['wrapper'], ['rule', ['alias1', 'alias2'], ['extend', 'selector1']]], ['rule', ['selector1', 'selector2'], 'Hello'] ]) ) ), ['selector1,\nselector2,\nwrapper alias1,\nwrapper alias2 {', ['Hello'], '}'] ) assert.deepEqual( CSS.stringify( CSS.process( CSS.simplify( CSS.unnest([ ['rule', 'alias', ['extend', 'selector1'], ['extend', 'selector2']], ['media', 'query', ['rule', 'selector1', 'Hello']], ['rule', 'selector2', 'Bye'] ]) ) ) ), `@media query { selector1, alias { Hello } } selector2, alias { Bye }` ) }) it('should compile nested blocks', () => { assert.deepEqual(CSS.process(CSS.unnest(['rule', 'selector', ['rule', 'nested1', 'Hello']])), [ 'selector nested1 {', ['Hello'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['rule', 'selector', ['rule', ['nested1', 'nested2'], 'Hello']])), [ 'selector nested1,\nselector nested2 {', ['Hello'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['rule', 'selector', ['rule', ['&:nested1', 'nested2 &'], 'Hello']])), [ 'selector:nested1,\nnested2 selector {', ['Hello'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['media', 'query', ['rule', 'nested1', 'Hello']])), [ '@media query {', ['nested1 {', ['Hello'], '}'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['rule', 'nested1', ['media', 'query', 'Hello']])), [ '@media query {', ['nested1 {', ['Hello'], '}'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['media', 'query', ['media', 'nested1', 'Hello']])), [ '@media query and nested1 {', ['Hello'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['rule', 'selector', ['rule', 'nested1', 'Hello']])), [ 'selector nested1 {', ['Hello'], '}' ]) assert.deepEqual( CSS.process(CSS.unnest(['rule', 'selector', ['media', 'query', ['rule', 'nested1', 'Hello']]])), ['@media query {', ['selector nested1 {', ['Hello'], '}'], '}'] ) assert.deepEqual(CSS.process(CSS.unnest(['media', 'q1', ['rule', 's1', ['media', 'q2', 'Hello']]])), [ '@media q1 and q2 {', ['s1 {', ['Hello'], '}'], '}' ]) assert.deepEqual(CSS.process(CSS.unnest(['rule', 'selector', 'before', ['rule', 'nested1', 'Hello'], 'after'])), [ 'selector {', ['before', 'after'], '}', 'selector nested1 {', ['Hello'], '}' ]) assert.deepEqual( CSS.process( CSS.unnest([ 'rule', 'selector', ['rule', 'nested1', 'Hello', ['rule', 'nested2', 'Hello2']], ['rule', 'nested3', 'Hello3'] ]) ), [ 'selector nested1 {', ['Hello'], '}', 'selector nested1 nested2 {', ['Hello2'], '}', 'selector nested3 {', ['Hello3'], '}' ] ) assert.deepEqual( CSS.process( CSS.unnest([ 'rule', 'S1', ['property', 'P1.1', 'V1.1'], ['property', 'P1.2', 'V1.2'], ['rule', 'S1.3', '1.3'], ['property', 'P1.4', 'V1.4'], ['media', '(Q1.4)', '1.4'], ['property', 'P1.5', 'V1.5'], [ 'media', '(Q1.6)', [ 'media', '(Q1.6.1)', '1.6.1.1', '1.6.1.2', ['rule', 'S1.6.1.3', '1.6.1.3.1'], ['media', '(Q1.6.1.4)', '1.6.1.4.1', '1.6.1.4.2'] ], ['rule', 'S1.6.2', '1.6.2.1', '1.6.2.2'] ] ]) ), [ 'S1 {', ['P1.1: V1.1;', 'P1.2: V1.2;', 'P1.4: V1.4;', 'P1.5: V1.5;'], '}', 'S1 S1.3 {', ['1.3'], '}', '@media (Q1.4) {', ['S1 {', ['1.4'], '}'], '}', '@media (Q1.6) and (Q1.6.1) {', ['S1 {', ['1.6.1.1', '1.6.1.2'], '}'], '}', '@media (Q1.6) and (Q1.6.1) {', ['S1 S1.6.1.3 {', ['1.6.1.3.1'], '}'], '}', '@media (Q1.6) and (Q1.6.1) and (Q1.6.1.4) {', ['S1 {', ['1.6.1.4.1', '1.6.1.4.2'], '}'], '}', '@media (Q1.6) {', ['S1 S1.6.2 {', ['1.6.2.1', '1.6.2.2'], '}'], '}' ] ) assert.deepEqual( CSS.stringify([ 'rule', 'S1', ['property', 'P1.1', 'V1.1'], ['property', 'P1.2', 'V1.2'], ['rule', 'S1.3', '1.3'], ['property', 'P1.4', 'V1.4'], ['media', '(Q1.4)', '1.4'], ['property', 'P1.5', 'V1.5'], [ 'media', '(Q1.6)', [ 'media', '(Q1.6.1)', '1.6.1.1', '1.6.1.2', ['rule', 'S1.6.1.3', '1.6.1.3.1'], ['media', '(Q1.6.1.4)', '1.6.1.4.1', '1.6.1.4.2'] ], ['rule', 'S1.6.2', '1.6.2.1', '1.6.2.2'] ] ]), `S1 { P1.1: V1.1; P1.2: V1.2; P1.4: V1.4; P1.5: V1.5; } S1 S1.3 { 1.3 } @media (Q1.4) { S1 { 1.4 } } @media (Q1.6) and (Q1.6.1) { S1 { 1.6.1.1 1.6.1.2 } S1 S1.6.1.3 { 1.6.1.3.1 } } @media (Q1.6) and (Q1.6.1) and (Q1.6.1.4) { S1 { 1.6.1.4.1 1.6.1.4.2 } } @media (Q1.6) { S1 S1.6.2 { 1.6.2.1 1.6.2.2 } }` ) assert.deepEqual( CSS.process( CSS.unnest([ 'rule', 'S1', ['property', 'P1.1', 'V1.1'], ['property', 'P1.2', 'V1.2'], ['rule', 'S1.3', '1.3'], ['property', 'P1.4', 'V1.4'], ['media', '(Q1.4)', '1.4'], ['property', 'P1.5', 'V1.5'], [ 'media', '(Q1.6)', [ 'media', '(Q1.6.1)', '1.6.1.1', '1.6.1.2', ['rule', 'S1.6.1.3', '1.6.1.3.1'], ['media', '(Q1.6.1.4)', '1.6.1.4.1', '1.6.1.4.2'] ], ['rule', 'S1.6.2', '1.6.2.1', '1.6.2.2'] ] ]) ), [ 'S1 {', ['P1.1: V1.1;', 'P1.2: V1.2;', 'P1.4: V1.4;', 'P1.5: V1.5;'], '}', 'S1 S1.3 {', ['1.3'], '}', '@media (Q1.4) {', ['S1 {', ['1.4'], '}'], '}', '@media (Q1.6) and (Q1.6.1) {', ['S1 {', ['1.6.1.1', '1.6.1.2'], '}'], '}', '@media (Q1.6) and (Q1.6.1) {', ['S1 S1.6.1.3 {', ['1.6.1.3.1'], '}'], '}', '@media (Q1.6) and (Q1.6.1) and (Q1.6.1.4) {', ['S1 {', ['1.6.1.4.1', '1.6.1.4.2'], '}'], '}', '@media (Q1.6) {', ['S1 S1.6.2 {', ['1.6.2.1', '1.6.2.2'], '}'], '}' ] ) }) }) describe('Rule', () => { describe('Details', () => { it('should stringify Details', () => { assert.deepEqual( CSS.produceDetails('Something', { id: '007', title: 'Hello', description: 'The metadata must begin at the very top of the document - no blank lines can precede it. There can optionally be a -- on the line before and after the metadata. The line after the metadata can also be .... This is to provide better compatibility with YAML, though MultiMarkdown doesn’t support all YAML metadata.', exampleContent: 'Example text' }), [ 'Type: Something', 'Id: 007', 'Title: Hello', 'Description: The metadata must begin at the very top of the document - no blank lines can ', ' precede it. There can optionally be a -- on the line before and after the ', ' metadata. The line after the metadata can also be .... This is to provide ', ' better compatibility with YAML, though MultiMarkdown doesn’t support all YAML ', ' metadata.', 'Example Content: Example text' ] ) assert.deepEqual( CSS.produceDetails('Something', { id: '007', title: 'Hello', description: 'World' }), ['Type: Something', 'Id: 007', 'Title: Hello', 'Description: World'] ) }) }) }) describe('Basics', () => { describe('Fonts', () => { it('should stringify font', () => { assert.deepEqual( CSS.process( CSS.reify( CSS.produceFontProps( Style.Font.Props({ platform: 'google', familyName: 'Lobster', variants: [ { name: '300', weight: 300, style: 'normal', familyName: null }, { name: '400', weight: 400, style: 'normal', familyName: null } ] }) ) ) ), ['@import url("https://fonts.googleapis.com/css?display=swap&family=Lobster:300,400");'] ) }) it('should stringify font rule', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'font', details: { id: 'test' }, props: { platform: 'google', familyName: 'Lobster', variants: [ { name: '300', weight: 300, style: 'normal', familyName: null }, { name: '400', weight: 400, style: 'normal', familyName: null } ] } }) ) ), `@import url("https://fonts.googleapis.com/css?display=swap&family=Lobster:300,400"); /* Type: font Id: test Title: Lobster */ ` ) }) it('should stringify font version', () => { assert.deepEqual( CSS.stringify( CSS.produceFontVariantProps({ id: 'f1', name: '400' }), { rules: [ Style.Rule({ type: 'font', details: { id: 'f1', title: 'Lobster' }, props: { familyName: 'Lobster', platform: 'google', variants: [ { name: '400', weight: 400, style: 'normal', familyName: null } ] } }) ] } ), `font-family: "Lobster"; font-weight: 400; font-style: normal;` ) }) it('should stringify resolved font v', () => { assert.deepEqual( CSS.stringify( [ 'fontVariant', { id: 'f1', familyName: 'Lobster', style: 'italic', weight: '400' } ], {} ), `font-family: "Lobster"; font-weight: 400; font-style: italic;` ) }) }) describe('Colors', () => { it('should stringify ColorWithName', () => { assert.equal( CSS.process([ 'color', { red: 255, green: 0, blue: 0, alpha: 0.1 } ])[0], 'rgba(255, 0, 0, 0.1)' ) assert.equal( CSS.process([ 'color', { red: 255, green: 0, blue: 0, alpha: 0 } ])[0], 'rgba(255, 0, 0, 0)' ) assert.equal( CSS.process([ 'color', { red: 0, green: 0, blue: 0, alpha: 0 } ])[0], 'rgba(0, 0, 0, 0)' ) assert.equal(CSS.process(['color', null])[0], undefined) }) // it.skip('should stringify BasicColorProps', () => { // assert.deepEqual( // CSS.process( // CSS.reify( // CSS.produceBasicColorProps({ // colors: [ // { // name: 'Primary 100', // color: { // red: 255, // green: 0, // blue: 0, // alpha: 0 // } // }, // { // name: 'crème brulée', // color: { // red: 0, // green: 255, // blue: 0, // alpha: 1 // } // } // ] // }) // ) // ), // ['---color--primary-100: rgba(255, 0, 0, 0);', '---color--creme-brulee: rgba(0, 255, 0, 1);'] // ) // }) // it.skip('should stringify BasicRule with BasicColor', () => { // assert.deepEqual( // CSS.stringify( // CSS.produceStyle({ // details: { // id: 'a', // title: 'Example rule', // description: '' // }, // props: { // colors: [ // { // name: 'Primary.100', // color: { // red: 255, // green: 0, // blue: 0, // alpha: 1 // } // }, // { // name: 'crème brulée', // color: { // red: 0, // green: 255, // blue: 0, // alpha: 1 // } // } // ] // }, // type: 'color' // }) // ), // `/* // Type: color // Id: a // Title: Example rule // */ // :root { // ---color--primary-100: rgba(255, 0, 0, 1); // ---color--creme-brulee: rgba(0, 255, 0, 1); // }` // ) // }) }) }) describe('Reusables', () => { describe('Media', () => { describe('empty rule', () => { it('should normalize media', () => { assert.deepEqual(Style.Media.Props({}), { objectFit: 'cover' as Style.Media.Fit, objectPositionX: { unit: '%', value: 50 }, objectPositionY: { unit: '%', value: 50 }, width: null, height: null }) }) it('should serialize media', () => { assert.deepEqual( CSS.stringify(CSS.produceMediaProps(Style.Media.Props({}))), `&[class*="-block--media"] { width: auto; } > picture { position: relative; overflow: hidden; height: auto; } > picture > img { object-fit: cover; object-position: 50% 50%; }` ) }) }) describe('fixed height rule', () => { it('should disable flex-grow', () => { assert.deepEqual(Style.Media.Props({ height: { unit: 'px', value: 100 } }), { objectFit: 'cover' as Style.Media.Fit, objectPositionX: { unit: '%', value: 50 }, objectPositionY: { unit: '%', value: 50 }, width: null, height: { unit: 'px', value: 100 } }) }) it('should serialize media', () => { assert.deepEqual( CSS.stringify(CSS.produceMediaProps(Style.Media.Props({ height: { unit: 'px', value: 100 } }))), `&[class*="-block--media"] { width: auto; flex-grow: 0 !important; flex-shrink: 0 !important; } > picture { position: relative; overflow: hidden; height: 100px; } > picture > img { object-fit: cover; object-position: 50% 50%; }` ) }) }) describe('given rule', () => { it('should normalize media', () => { assert.deepEqual( Style.Media.Props({ placement: 'background', objectFit: 'cover', objectPositionX: '33px', objectPositionY: 'right', width: '100%', height: '1:2' }), { objectFit: 'cover' as Style.Media.Fit, objectPositionX: { unit: 'px', value: 33 }, objectPositionY: { unit: '%', value: 100 }, width: { value: 100, unit: '%' }, height: { value: 0.5, unit: 'ratio' } } ) }) it('should serialize media', () => { assert.deepEqual( CSS.stringify( CSS.produceMediaProps( Style.Media.Props({ objectFit: 'cover', objectPositionX: '33px', objectPositionY: 'right', width: '100%', height: '2.5:1.5' }) ) ), `&[class*="-block--media"] { width: 100%; flex-grow: 0 !important; flex-shrink: 0 !important; } > picture { position: relative; overflow: hidden; } > picture > img { object-fit: cover; object-position: 33px 100%; position: absolute; top: 0; left: 0; right: 0; bottom: 0; height: 100%; width: 100%; } > picture:before { content: ""; display: block; padding-top: 60%; }` ) }) }) }) describe('Decoration', () => { it('should stringify', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ details: { id: '123', title: 'My rule', description: '' }, props: { borderTopLeftRadius: 1, borderBottomWidth: 2, borderStyle: 'dashed', borderColor: { red: 0, green: 255, blue: 0, alpha: 1 }, boxShadowColor: { red: 255, green: 0, blue: 0, alpha: 1 }, boxShadowOffsetX: 1, boxShadowInset: true }, type: 'decoration' }) ) ), `/* Type: decoration Id: 123 Title: My rule *\/ .-decoration--my-rule:not(._._._._._._#_) { box-shadow: inset 1px 0px 0px 0px rgba(255, 0, 0, 1); border-radius: 1px 0px 0px 0px; border-width: 0px 0px 2px 0px; border-style: dashed; border-color: rgba(0, 255, 0, 1); }` ) }) it('should not stringify box shadow without color', () => { assert.deepEqual( CSS.process( CSS.reify( CSS.produceDecorationProps( Style.Decoration.Props({ boxShadowOffsetX: 1, boxShadowOffsetY: 2, boxShadowBlurRadius: 3, boxShadowSpreadRadius: 4 }) ) ) ), [ 'box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0);', 'border-radius: 0px 0px 0px 0px;', 'border-width: 0px 0px 0px 0px;', 'border-style: solid;', 'border-color: rgba(0, 0, 0, 0);' ] ) }) it('should stringify box shadow with all properties', () => { assert.deepEqual( CSS.process( CSS.reify( CSS.produceDecorationProps( Style.Decoration.Props({ boxShadowOffsetX: 1, boxShadowOffsetY: 2, boxShadowBlurRadius: 3, boxShadowSpreadRadius: 4, boxShadowColor: { red: 1, green: 2, blue: 3, alpha: 0.4 } }) ) ) ), [ 'box-shadow: 1px 2px 3px 4px rgba(1, 2, 3, 0.4);', 'border-radius: 0px 0px 0px 0px;', 'border-width: 0px 0px 0px 0px;', 'border-style: solid;', 'border-color: rgba(0, 0, 0, 0);' ] ) }) it('should not stringify shadow with only color', () => { assert.deepEqual( CSS.process( CSS.reify( CSS.produceDecorationProps( Style.Decoration.Props({ boxShadowOffsetX: 0, boxShadowOffsetY: 0, boxShadowBlurRadius: 0, boxShadowSpreadRadius: 0, boxShadowColor: { red: 1, green: 2, blue: 3, alpha: 4 } }) ) ) ), [ 'box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0);', 'border-radius: 0px 0px 0px 0px;', 'border-width: 0px 0px 0px 0px;', 'border-style: solid;', 'border-color: rgba(0, 0, 0, 0);' ] ) }) it('should stringify shadow with color and offset', () => { assert.deepEqual( CSS.process( CSS.reify( CSS.produceDecorationProps( Style.Decoration.Props({ boxShadowOffsetX: 1, boxShadowOffsetY: 0, boxShadowBlurRadius: 0, boxShadowSpreadRadius: 0, boxShadowColor: { red: 1, green: 2, blue: 3, alpha: 0.4 } }) ) ) ), [ 'box-shadow: 1px 0px 0px 0px rgba(1, 2, 3, 0.4);', 'border-radius: 0px 0px 0px 0px;', 'border-width: 0px 0px 0px 0px;', 'border-style: solid;', 'border-color: rgba(0, 0, 0, 0);' ] ) }) }) describe('Breakpoint', () => { it('should normalize empty', () => { assert.deepEqual(Style.Breakpoint.Props({}), { minWidth: 0, maxWidth: Infinity, query: '(min-width: 0px)' }) }) it('should normalize ', () => { assert.deepEqual( Style.Breakpoint.Props({ minWidth: 1, maxWidth: 2 }), { minWidth: 1, maxWidth: 2, query: '(min-width: 1px) and (max-width: 2.98px)' } ) assert.deepEqual( Style.Breakpoint.Props({ minWidth: 1 }), { minWidth: 1, maxWidth: Infinity, query: '(min-width: 1px)' } ) assert.deepEqual( Style.Breakpoint.Props({ maxWidth: Infinity, query: 'test' }), { minWidth: 0, maxWidth: Infinity, query: 'test' } ) }) it('should serialize', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'breakpoint', details: { id: 'test', slug: 'my-breakpoint' }, props: { minWidth: 1, maxWidth: 2, query: 'abc' } }) ) ), `/* Type: breakpoint Id: test */ @media abc { [class*="-breakpoint--"]:not(.-breakpoint--my-breakpoint):not(._._._._._._#_) { display: none; } }` ) }) it('should serialize query', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'breakpoint', details: { id: 'test', slug: 'my-breakpoint' }, props: { minWidth: 1, maxWidth: 2 } }) ) ), `/* Type: breakpoint Id: test */ @media (min-width: 1px) and (max-width: 2.98px) { [class*="-breakpoint--"]:not(.-breakpoint--my-breakpoint):not(._._._._._._#_) { display: none; } }` ) }) it('should serialize query', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'breakpoint', details: { id: 'test', slug: 'my-breakpoint' }, props: { minWidth: 1, maxWidth: Infinity } }) ) ), `/* Type: breakpoint Id: test */ @media (min-width: 1px) { [class*="-breakpoint--"]:not(.-breakpoint--my-breakpoint):not(._._._._._._#_) { display: none; } }` ) }) }) describe('Lines', () => { it('should normalize empty', () => { assert.deepEqual(Style.Lines.Props({}), { textAlign: 'left' as Style.Lines.TextAlign, lines: null, ellipsis: false, wordBreak: 'normal' as Style.Lines.WordBreak }) }) it('should normalize incorrect', () => { assert.deepEqual( Style.Lines.Props({ textAlign: '1', lines: 'fff', ellipsis: 'ff', wordBreak: 'bre22ak-all' }), { textAlign: 'left' as Style.Lines.TextAlign, lines: null, ellipsis: false, wordBreak: 'normal' as Style.Lines.WordBreak } ) }) it('should normalize non-empty', () => { assert.deepEqual( Style.Lines.Props({ textAlign: 'right', lines: '2', ellipsis: 'true', wordBreak: 'break-all' }), { textAlign: 'right' as Style.Lines.TextAlign, lines: 2, ellipsis: true, wordBreak: 'break-all' as Style.Lines.WordBreak } ) }) it('should stringify empty', () => { assert.deepEqual( CSS.stringify(CSS.process(CSS.reify(CSS.produceLinesProps(Style.Lines.Props({}))))), `text-align: left; word-break: normal; overflow-wrap: normal;` ) }) it('should stringify 1 line', () => { assert.deepEqual( CSS.stringify( CSS.process( CSS.reify( CSS.produceLinesProps( Style.Lines.Props({ lines: 1 }) ) ) ) ), `text-align: left; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; word-break: normal; overflow-wrap: normal;` ) }) it('should stringify word break property', () => { assert.deepEqual( CSS.stringify( CSS.process( CSS.reify( CSS.produceLinesProps( Style.Lines.Props({ wordBreak: 'break-word' }) ) ) ) ), `text-align: left; word-break: break-word; overflow-wrap: break-word;` ) }) it('should stringify 1 line with no overflow', () => { assert.deepEqual( CSS.stringify( CSS.process( CSS.reify( CSS.produceLinesProps( Style.Lines.Props({ lines: 1, ellipsis: false }) ) ) ) ), `text-align: left; white-space: nowrap; word-break: normal; overflow-wrap: normal;` ) }) it('should stringify 2 lines', () => { assert.deepEqual( CSS.stringify( CSS.process( CSS.reify( CSS.produceLinesProps( Style.Lines.Props({ lines: 2 }) ) ) ) ), `text-align: left; ---self--display: -webkit-box; display: var(---self--display); -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: normal; overflow-wrap: normal;` ) }) }) it('should stringify 2 lines', () => { assert.deepEqual( CSS.stringify( CSS.process( CSS.reify( CSS.produceLinesProps( Style.Lines.Props({ wordBreak: 'hyphens' }) ) ) ) ), `text-align: left; word-break: normal; overflow-wrap: normal; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto;` ) }) describe('Spacing', () => { it('should serialize', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'spacing', props: { paddingTop: 1, paddingBottom: 2, paddingLeft: 3, paddingRight: 4, columnGap: 5, rowGap: 6 }, details: { id: 'id', title: 'title', description: 'test' } }) ) ), `/* Type: spacing Id: id Title: title Description: test */ .-spacing--title:not(._._._._._._#_) { padding-top: 1px; padding-right: 4px; padding-bottom: 2px; padding-left: 3px; ---spacing--row-gap: 6px; ---spacing--column-gap: 5px; } .-spacing--title > *:not(._._._._._._#_) { ---layout--row-gap: 6px; ---layout--column-gap: 5px; }` ) }) it('should serialize empty', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'spacing', props: {}, details: { id: 'id', title: 'title', description: 'test' } }) ) ), `/* Type: spacing Id: id Title: title Description: test */ .-spacing--title:not(._._._._._._#_) { padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ---spacing--row-gap: 0px; ---spacing--column-gap: 0px; } .-spacing--title > *:not(._._._._._._#_) { ---layout--row-gap: 0px; ---layout--column-gap: 0px; }` ) }) }) describe('Layout', () => { it('should allow null as columns count', () => { assert.deepEqual( Style.Layout.Props({ columnCount: null, weights: [1, 2, 3] }), { alignItems: 'stretch' as any, columnCount: null, flexWrap: false, justifyContent: 'stretch' as any, weights: [] } ) }) it('should generate 1 column by default', () => { assert.deepEqual( Style.Layout.Props({ columnCount: undefined, weights: [1, 2, 3] }), { alignItems: 'stretch' as any, columnCount: 1, flexWrap: false, justifyContent: 'flex-start' as any, weights: [1] } ) }) it('should serialize', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'layout', props: { columnCount: 3, weights: [1, 2, 3] }, details: { id: 'id', title: 'title', description: 'test' } }) ) ), `/* Type: layout Id: id Title: title Description: test */ .-layout--title:not(._._._._._._#_) { ---self--display: flex; display: var(---self--display); flex-wrap: nowrap; flex-direction: row; justify-content: space-between; align-items: stretch; row-gap: calc(var(---supports--flex-gap, 0) * var(---spacing--row-gap)); column-gap: calc(var(---supports--flex-gap, 0) * var(---spacing--column-gap)); max-width: min(var(---self--max-available-width, 100%), var(---self--max-width, 100%)); } .-layout--title > *:not(._._._._._._#_) { ---self--vertical-layout: 0; ---layout--available-width: calc(100% - var(---layout--column-gap, 0px) * 2); ---self--paragraph-spacing: initial; margin-top: calc( var(---supports--flex-gap, 0) * (var(---self--paragraph-spacing, var(---self--row-gap, 0px)) - var(---self--row-gap, 0px)) + (1 - var(---supports--flex-gap, 0)) * var(---self--paragraph-spacing, var(---self--row-gap, 0px)) ); margin-right: calc((1 - var(---supports--flex-gap, 0)) * var(---self--column-gap, 0px)); flex-basis: var(---self--column-width); flex-grow: 1; ---self--row-gap: initial; ---self--column-gap: var(---layout--column-gap, 0px); } .-layout--title > :last-child:not(._._._._._._#_) { ---self--column-gap: initial; } .-layout--title > :nth-child(3n + 1):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 6 * 1); } .-layout--title > :nth-child(3n + 2):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 6 * 2); } .-layout--title > :nth-child(3n + 3):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 6 * 3); }` ) }) it('should serialize', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'layout', props: { columnCount: 3, weights: [1, 2, 3], alignItems: 'flex-start', justifyContent: 'space-between' }, details: { id: 'id', title: 'title', description: 'test' } }) ) ), `/* Type: layout Id: id Title: title Description: test */ .-layout--title:not(._._._._._._#_) { ---self--display: flex; display: var(---self--display); flex-wrap: nowrap; flex-direction: row; justify-content: space-between; align-items: flex-start; row-gap: calc(var(---supports--flex-gap, 0) * var(---spacing--row-gap)); column-gap: calc(var(---supports--flex-gap, 0) * var(---spacing--column-gap)); max-width: min(var(---self--max-available-width, 100%), var(---self--max-width, 100%)); } .-layout--title > *:not(._._._._._._#_) { ---self--vertical-layout: 0; ---self--max-available-width: var(---self--column-width); ---layout--available-width: calc(100% - var(---layout--column-gap, 0px) * 2); ---self--paragraph-spacing: initial; margin-top: calc( var(---supports--flex-gap, 0) * (var(---self--paragraph-spacing, var(---self--row-gap, 0px)) - var(---self--row-gap, 0px)) + (1 - var(---supports--flex-gap, 0)) * var(---self--paragraph-spacing, var(---self--row-gap, 0px)) ); margin-right: calc((1 - var(---supports--flex-gap, 0)) * var(---self--column-gap, 0px)); flex-basis: var(---self--column-width); flex-grow: 0; ---self--row-gap: initial; ---self--column-gap: var(---layout--column-gap, 0px); } .-layout--title > :last-child:not(._._._._._._#_) { ---self--column-gap: initial; } .-layout--title > :nth-child(3n + 1):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 6 * 1); } .-layout--title > :nth-child(3n + 2):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 6 * 2); } .-layout--title > :nth-child(3n + 3):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 6 * 3); }` ) }) it('should serialize empty', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'layout', props: {}, details: { id: 'id', title: 'title', description: 'test' } }) ) ), `/* Type: layout Id: id Title: title Description: test */ .-layout--title:not(._._._._._._#_) { ---self--display: flex; display: var(---self--display); flex-wrap: nowrap; flex-direction: column; justify-content: flex-start; align-items: stretch; row-gap: calc(var(---supports--flex-gap, 0) * var(---spacing--row-gap)); column-gap: calc(var(---supports--flex-gap, 0) * var(---spacing--column-gap)); max-width: min(var(---self--max-available-width, 100%), var(---self--max-width, 100%)); } .-layout--title > *:not(._._._._._._#_) { ---self--vertical-layout: 1; margin-top: calc( var(---supports--flex-gap, 0) * (var(---self--paragraph-spacing, var(---self--row-gap, 0px)) - var(---self--row-gap, 0px)) + (1 - var(---supports--flex-gap, 0)) * var(---self--paragraph-spacing, var(---self--row-gap, 0px)) ); flex-grow: 0; ---self--row-gap: initial; ---self--column-gap: var(---layout--column-gap, 0px); } .-layout--title > :not(:nth-child(-n+1)):not(._._._._._._#_) { ---self--row-gap: var(---layout--row-gap, 0px); } .-layout--title > :last-child:not(._._._._._._#_) { ---self--column-gap: initial; } .-layout--title > :nth-child(1n + 1):not(._._._._._._#_) { ---self--column-width: calc(var(---layout--available-width, 100%) / 1 * 1); }` ) }) }) describe('Dimensions', () => { it('should normalize dimensions', () => { assert.deepEqual(Style.Dimensions.Props({}), { minWidth: null, minHeight: null, maxWidth: null, maxHeight: null }) assert.deepEqual( Style.Dimensions.Props({ minWidth: 1, maxHeight: '100%' }), { minWidth: { unit: 'px', value: 1 }, minHeight: null, maxWidth: null, maxHeight: { unit: '%', value: 100 } } ) }) }) describe('Typography', () => { it('should serialize', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'typography', props: { textTransform: 'capitalize', base: { fontSize: 1, letterSpacing: 3 }, overrides: { mobile: { fontSize: 11, letterSpacing: 13, paragraphSpacing: 14, iconSize: 25 }, desktop: { lineHeight: 21 } } }, details: { id: 'id', title: 'title', description: 'test' } }) ).concat([['rule', 'test', ['extend', '.-typography--title']]]) ), `/* Type: typography Id: id Title: title Description: test Example Content: Example */ .-typography--title:not(._._._._._._#_), test { text-transform: capitalize; line-height: var(---typography--line-height, inherit); font-family: inherit; font-weight: inherit; font-style: inherit; font-size: 1px; letter-spacing: 3px; ---typography--line-height: initial; ---typography--icon-size: initial; } @media mobile { .-typography--title:not(._._._._._._#_), test { font-size: 11px; letter-spacing: 13px; ---typography--icon-size: 25px; ---typography--paragraph-spacing: 14px; } } .-emulate--mobile .-typography--title:not(._._._._._._#_), .-emulate--mobile test { font-size: 11px; letter-spacing: 13px; ---typography--icon-size: 25px; ---typography--paragraph-spacing: 14px; } @media desktop { .-typography--title:not(._._._._._._#_), test { ---typography--line-height: 21px; } } .-emulate--desktop .-typography--title:not(._._._._._._#_), .-emulate--desktop test { ---typography--line-height: 21px; }` ) }) }) describe('Palette', () => { it('should stringify', () => { assert.deepEqual( CSS.stringify([ 'rule', '.test', ...CSS.producePaletteProps( Style.Palette.Props({ textColor: { red: 0, green: 1, blue: 2, alpha: 0.3 }, linkDecoration: 'dotted' }) ) ]), `.test { color: rgba(0, 1, 2, 0.3); ---typography--text-decoration: dotted; }` ) assert.deepEqual( CSS.stringify(['rule', '.test', ...CSS.producePaletteProps(Style.Palette.Props({}))]), `.test { color: inherit; ---typography--text-decoration: underline; }` ) }) }) describe('Fill', () => { it('should stringify effects', () => { assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ blur: 12 }) ) ), `background: none; backdrop-filter: blur(12px); opacity: 1;` ) assert.deepEqual( CSS.stringify(CSS.produceFillProps(Style.Fill.Props({}))), `background: none; backdrop-filter: none; opacity: 1;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ opacity: 1 }) ) ), `background: none; backdrop-filter: none; opacity: 1;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ opacity: 0.5 }) ) ), `background: none; backdrop-filter: none; opacity: 0.5;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ opacity: 0.5, blur: 1 }) ) ), `background: none; backdrop-filter: blur(1px); opacity: 0.5;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ opacity: 0.5, blur: 0 }) ) ), `background: none; backdrop-filter: none; opacity: 0.5;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ opacity: 1, blur: 0 }) ) ), `background: none; backdrop-filter: none; opacity: 1;` ) }) it('should stringify', () => { assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ backgroundColor: { red: 0, green: 1, blue: 2, alpha: 0.3 } }) ) ), `background: rgba(0, 1, 2, 0.3); backdrop-filter: none; opacity: 1;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ backgroundGradient: { angle: 90, stops: [ { start: 0, color: { red: 0, green: 1, blue: 2, alpha: 0.3 } }, { start: { unit: '%', value: 100 }, color: { red: 4, green: 5, blue: 6, alpha: 0.7 } } ] } }) ) ), `background: linear-gradient(90deg, rgba(0, 1, 2, 0.3) 0px, rgba(4, 5, 6, 0.7) 100%); backdrop-filter: none; opacity: 1;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ backgroundColor: { red: 8, green: 9, blue: 10, alpha: 0.11 }, backgroundGradient: { angle: 90, stops: [ { start: 0, color: { red: 0, green: 1, blue: 2, alpha: 0.3 } }, { start: 100, color: { red: 4, green: 5, blue: 6, alpha: 0.7 } } ] } }) ) ), `background: linear-gradient(90deg, rgba(0, 1, 2, 0.3) 0px, rgba(4, 5, 6, 0.7) 100px), rgba(8, 9, 10, 0.11); backdrop-filter: none; opacity: 1;` ) assert.deepEqual( CSS.stringify( CSS.produceFillProps( Style.Fill.Props({ backgroundColor: { red: 8, green: 9, blue: 10, alpha: 0.11 }, backgroundImage: 'test', backgroundPositionY: 'right', backgroundSize: 'cover', backgroundGradient: { angle: 90, stops: [ { color: { red: 0, green: 1, blue: 2, alpha: 0.3 } }, { start: 100, color: { red: 4, green: 5, blue: 6, alpha: 0.7 } } ] } }) ) ), `background: url("test") repeat 50% 100% / cover, linear-gradient(90deg, rgba(0, 1, 2, 0.3), rgba(4, 5, 6, 0.7) 100px), rgba(8, 9, 10, 0.11); backdrop-filter: none; opacity: 1;` ) }) }) }) describe('Elements', () => { describe('Button', () => { it('should stringify', () => { const styles: Style.Rule[] = [ { type: 'color', details: { id: 'colors1', title: 'Color set 1' }, props: { red: 1, green: 2, blue: 3, alpha: 0.9 } }, Style.Rule({ type: 'breakpoint', details: { id: 'mobile', title: 'Breakpoint Mobile' }, props: { query: '(max-width: 899px)' } }), Style.Rule({ type: 'breakpoint', details: { id: 'desktop', title: 'Breakpoint Desktop' }, props: { query: '(min-width: 900px)' } }), Style.Rule({ type: 'decoration', details: { id: '1', title: 'Cool decoration 1' }, props: { borderStyle: 'solid' } }), Style.Rule({ type: 'decoration', details: { id: '2', title: 'Cool decoration 2' }, props: { borderStyle: 'dashed', boxShadowBlurRadius: 3, boxShadowInset: true, boxShadowColor: { id: 'colors1', alpha: 0.3 } } }), Style.Rule({ type: 'fill', details: { id: '4', title: 'Cool fill' }, props: { backgroundImage: 'abc.jpg', backgroundSize: 'cover', backgroundGradient: { angle: 30, stops: [ { start: { unit: '%', value: 10 }, color: { id: 'colors1', alpha: 0.1 } }, { color: { red: 255, green: 0, blue: 0, alpha: 0.1 } } ] } } }), Style.Rule({ type: 'typography', details: { id: '3', title: 'Cool typography 3' }, props: { base: { fontSize: 1 }, overrides: { mobile: { fontSize: 2, lineHeight: { unit: '%', value: 121 } }, desktop: { letterSpacing: 3, paragraphSpacing: 4, iconSize: 5 } } } }), Style.Rule({ type: 'spacing', props: { paddingTop: 1, paddingBottom: 2, paddingLeft: 3, paddingRight: 4, rowGap: 5, columnGap: 6 }, details: { id: '5', title: 'title', description: 'test' } }) ] assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ details: { id: '123', slug: 'button--my-cool-card', title: 'My cool card', description: 'Hello', collectionId: 'button' }, props: { decorationIds: ['2', '1'], typographyIds: ['3'], fillIds: ['4'], spacingIds: [] }, type: 'inline' }) ), { rules: styles, bundle: true } ), `/* Type: inline Id: 123 Title: My cool card Description: Hello */ .-button--my-cool-card:not(._._._._#_) { /* Typography Ids: 3 Decoration Ids: 2, 1 Fill Ids: 4 */ box-shadow: inset 0px 0px 3px 0px rgba(1, 2, 3, 0.3); border-radius: 0px 0px 0px 0px; border-width: 0px 0px 0px 0px; border-style: solid; border-color: rgba(0, 0, 0, 0); line-height: var(---typography--line-height, inherit); font-family: inherit; font-weight: inherit; font-style: inherit; font-size: 1px; letter-spacing: inherit; ---typography--line-height: initial; ---typography--icon-size: initial; color: inherit; ---typography--text-decoration: underline; background: url("abc.jpg") repeat 50% 50% / cover, linear-gradient(30deg, rgba(1, 2, 3, 0.1) 10%, rgba(255, 0, 0, 0.1)); backdrop-filter: none; opacity: 1; padding-top: 1px; padding-right: 4px; padding-bottom: 2px; padding-left: 3px; ---spacing--row-gap: 5px; ---spacing--column-gap: 6px; } @media (max-width: 899px) { .-button--my-cool-card:not(._._._._#_) { font-size: 2px; ---typography--line-height: 1.21em; } } .-emulate--mobile .-button--my-cool-card:not(._._._._#_) { font-size: 2px; ---typography--line-height: 1.21em; } @media (min-width: 900px) { .-button--my-cool-card:not(._._._._#_) { letter-spacing: 3px; ---typography--icon-size: 5px; ---typography--paragraph-spacing: 4px; } } .-emulate--desktop .-button--my-cool-card:not(._._._._#_) { letter-spacing: 3px; ---typography--icon-size: 5px; ---typography--paragraph-spacing: 4px; } .-button--my-cool-card > *:not(._._._._#_) { ---layout--row-gap: 5px; ---layout--column-gap: 6px; }` ) }) }) }) describe('Themes', () => { const styles: Style.Rule[] = [ Style.Rule({ type: 'block', details: { id: 'my-card', collectionId: 'card', title: 'My card' }, props: {} }), Style.Rule({ type: 'fill', details: { id: 'fill1', title: 'Cool fill 1' }, props: { backgroundImage: 'fill1.jpg' } }), Style.Rule({ type: 'fill', details: { id: 'fill2', title: 'Cool fill 2' }, props: { backgroundImage: 'fill2.jpg' } }), Style.Rule({ type: 'decoration', details: { id: 'decoration1', title: 'Fancy decoration' }, props: { boxShadowBlurRadius: 3, boxShadowInset: true, boxShadowColor: { id: 'colors1', alpha: 0.3 } } }) ] it('should produce props', () => { assert.deepEqual( CSS.unnest( CSS.reify( CSS.produceStyle( Style.Rule({ type: 'theme', details: { id: 'default-theme', title: 'My theme' }, props: { blocks: [ { fillId: 'fill1', decorationId: 'decoration1', refId: 'my-card', isDefault: true, id: 'b' }, { fillId: 'fill2', decorationId: 'decoration1', refId: 'my-card', id: 'a' } ] } }) ), { rules: styles } ) ).slice(1), [ [ 'rule', [ '.-theme--my-theme:weight(1.0.0) [class^="-card"]', '.-theme--my-theme:weight(1.0.0).-use--b [class^="-card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) [class*=" -card"]', '.-theme--my-theme:weight(1.0.0).-use--b [class*=" -card"]:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-component', //'.-theme--my-theme:weight(1.0.0).-use--b .-component:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-embed', //'.-theme--my-theme:weight(1.0.0).-use--b .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--b:weight(0.4.0)' ], ['extend', ['.-my-card']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0) [class^="-card"]', '.-theme--my-theme:weight(1.0.0).-use--b [class^="-card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) [class*=" -card"]', '.-theme--my-theme:weight(1.0.0).-use--b [class*=" -card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--b:weight(0.4.0)' ], ['extend', ['.-decoration--fancy-decoration']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0) [class^="-card"]', '.-theme--my-theme:weight(1.0.0).-use--b [class^="-card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) [class*=" -card"]', '.-theme--my-theme:weight(1.0.0).-use--b [class*=" -card"]:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-component', //'.-theme--my-theme:weight(1.0.0).-use--b .-component:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-embed', //'.-theme--my-theme:weight(1.0.0).-use--b .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--b:weight(0.4.0)' ], ['extend', ['.-fill--cool-fill-1']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0).-use--a [class^="-card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0).-use--a [class*=" -card"]:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0).-use--a .-component:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0).-use--a .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--a:weight(0.4.0)' ], ['extend', ['.-my-card']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0).-use--a [class^="-card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0).-use--a [class*=" -card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--a:weight(0.4.0)' ], ['extend', ['.-decoration--fancy-decoration']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0).-use--a [class^="-card"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0).-use--a [class*=" -card"]:weight(0.-1.2)', // '.-theme--my-theme:weight(1.0.0).-use--a .-component:weight(0.-1.2)', // '.-theme--my-theme:weight(1.0.0).-use--a .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--a:weight(0.4.0)' ], ['extend', ['.-fill--cool-fill-2']] ] ] ) }) it('should produce props for custom elements', () => { var stylesWithCustom: Style.Rule[] = [ Style.Rule({ details: { id: 'EYEBROW-ID', slug: 'eyebrow', title: 'Eyebrow' }, type: 'collection', props: { type: 'inline' } }), Style.Rule({ type: 'inline', details: { collectionId: 'EYEBROW-ID', id: 'eyebrow-element', title: 'My eyebrow' }, props: { fillIds: ['fill2'], decorationIds: ['decoration1'] } }), ...styles ] assert.deepEqual( CSS.unnest( CSS.reify( CSS.produceStyle( Style.Rule({ type: 'theme', details: { id: 'default-theme', title: 'My theme' }, props: { inlines: [ { fillId: 'fill1', decorationId: 'decoration1', refId: 'eyebrow-element', isDefault: true, id: 'b' }, { fillId: 'fill2', decorationId: 'decoration1', refId: 'eyebrow-element', id: 'a' } ] } }) ), { rules: stylesWithCustom } ) ).slice(1), [ [ 'rule', [ '.-theme--my-theme:weight(1.0.0) [class^="-inline--eyebrow"]', '.-theme--my-theme:weight(1.0.0).-use--b [class^="-inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) [class*=" -inline--eyebrow"]', '.-theme--my-theme:weight(1.0.0).-use--b [class*=" -inline--eyebrow"]:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-component', //'.-theme--my-theme:weight(1.0.0).-use--b .-component:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-embed', //'.-theme--my-theme:weight(1.0.0).-use--b .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--b:weight(0.4.0)' ], ['extend', ['.-inline--my-eyebrow']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0) [class^="-inline--eyebrow"]', '.-theme--my-theme:weight(1.0.0).-use--b [class^="-inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) [class*=" -inline--eyebrow"]', '.-theme--my-theme:weight(1.0.0).-use--b [class*=" -inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--b:weight(0.4.0)' ], ['extend', ['.-decoration--fancy-decoration']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0) [class^="-inline--eyebrow"]', '.-theme--my-theme:weight(1.0.0).-use--b [class^="-inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) [class*=" -inline--eyebrow"]', '.-theme--my-theme:weight(1.0.0).-use--b [class*=" -inline--eyebrow"]:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-component', //'.-theme--my-theme:weight(1.0.0).-use--b .-component:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0) .-embed', //'.-theme--my-theme:weight(1.0.0).-use--b .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--b:weight(0.4.0)' ], ['extend', ['.-fill--cool-fill-1']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0).-use--a [class^="-inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0).-use--a [class*=" -inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--a:weight(0.4.0)' ], ['extend', ['.-inline--my-eyebrow']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0).-use--a [class^="-inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0).-use--a [class*=" -inline--eyebrow"]:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0).-use--a .-component:weight(0.-1.2)', //'.-theme--my-theme:weight(1.0.0).-use--a .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--a:weight(0.4.0)' ], ['extend', ['.-decoration--fancy-decoration']] ], [ 'rule', [ '.-theme--my-theme:weight(1.0.0).-use--a [class^="-inline--eyebrow"]:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0).-use--a [class*=" -inline--eyebrow"]:weight(0.-1.2)', // '.-theme--my-theme:weight(1.0.0).-use--a .-component:weight(0.-1.2)', // '.-theme--my-theme:weight(1.0.0).-use--a .-embed:weight(0.-1.2)', '.-theme--my-theme:weight(1.0.0) .--a:weight(0.4.0)' ], ['extend', ['.-fill--cool-fill-2']] ] ] ) }) it('should serialize', () => { assert.deepEqual( CSS.stringify( [ ...CSS.produceStyle( Style.Rule({ type: 'theme', details: { id: 'default-theme', title: 'My theme' }, props: { blocks: [ { fillId: 'fill1', decorationId: 'decoration1', refId: 'my-card' }, { fillId: 'fill2', decorationId: 'decoration1', refId: 'my-card' } ] } }) ), ...CSS.produceStyle(styles[1]), ...CSS.produceStyle(styles[2]), ...CSS.produceStyle(styles[3]) ], { rules: styles } ), `/* Type: theme Id: default-theme Title: My theme */ .-theme--my-theme:not(#_) { } /* Type: fill Id: fill1 Title: Cool fill 1 */ .-fill--cool-fill-1:not(._._._._._._#_) { background: url("fill1.jpg") repeat 50% 50% / cover; backdrop-filter: none; opacity: 1; } /* Type: fill Id: fill2 Title: Cool fill 2 */ .-fill--cool-fill-2:not(._._._._._._#_) { background: url("fill2.jpg") repeat 50% 50% / cover; backdrop-filter: none; opacity: 1; } /* Type: decoration Id: decoration1 Title: Fancy decoration */ .-decoration--fancy-decoration:not(._._._._._._#_) { box-shadow: inset 0px 0px 3px 0px rgba(0, 0, 0, 0.3); border-radius: 0px 0px 0px 0px; border-width: 0px 0px 0px 0px; border-style: solid; border-color: rgba(0, 0, 0, 0); }` ) }) }) describe('Blocks', () => { it('should normalize blocks', () => { assert.deepEqual(Style.Block.Props({}), { decorationIds: [], fillIds: [], gridIds: [], layoutIds: [], paletteIds: [], spacingIds: [] } as any) assert.deepEqual( Style.Block.Props({ layoutIds: ['abc'] }), { decorationIds: [], fillIds: [], gridIds: [], layoutIds: ['abc'], paletteIds: [], spacingIds: [] } ) }) }) describe('Themes', () => { it('should normalize empty', () => { assert.deepEqual(Style.Theme.Props({}), { blocks: [], inlines: [], texts: [] }) }) it('should normalize correct', () => { assert.deepEqual( Style.Theme.Props({ blocks: [ { id: 'test', refId: 'test', fillId: 'test', decorationId: 'test', spacingId: 'test', isDefault: false } ], inlines: [ { id: 'test', refId: 'test', fillId: 'test', decorationId: 'test', spacingId: 'test', typographyId: 'test', paletteId: 'test', isDefault: false } ], texts: [ { id: 'test', refId: 'test', typographyId: 'test', paletteId: 'test', isDefault: false } ] }), { blocks: [ { id: 'test', refId: 'test', fillId: 'test', decorationId: 'test', spacingId: 'test', typographyId: null, paletteId: null, isDefault: false } ], inlines: [ { id: 'test', refId: 'test', fillId: 'test', decorationId: 'test', spacingId: 'test', typographyId: 'test', paletteId: 'test', isDefault: false } ], texts: [ { id: 'test', refId: 'test', typographyId: 'test', paletteId: 'test', fillId: null, decorationId: null, spacingId: null, isDefault: false } ] } ) }) it('should normalize incorrect', () => { assert.deepEqual( Style.Theme.Props({ blocks: [ { id: 'test', refId: 'test', fillId: 'test', decorationId: 'test', spacingId: 'test', isDefault: false }, 23, '3123', { hello: 'world' }, [33], Symbol('test') ], inlines: [], texts: [] }), { blocks: [ { id: 'test', refId: 'test', fillId: 'test', decorationId: 'test', spacingId: 'test', typographyId: null, paletteId: null, isDefault: false } ], inlines: [], texts: [] } ) }) }) describe('Overrides', () => { it('should serialize extra css in context', () => { assert.deepEqual( CSS.stringify( CSS.produceStyle( Style.Rule({ type: 'spacing', props: {}, details: { id: 'id', title: 'title', description: 'test', override: `span { test-property: 1px; } h1& { test-property: top left; }` } }) ) ), `/* Type: spacing Id: id Title: title Description: test */ .-spacing--title:not(._._._._._._#_) { padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ---spacing--row-gap: 0px; ---spacing--column-gap: 0px; } .-spacing--title > *:not(._._._._._._#_) { ---layout--row-gap: 0px; ---layout--column-gap: 0px; } .-spacing--title span:not(._._._._._._#_) { test-property: 1px; } h1.-spacing--title:not(._._._._._._#_) { test-property: top left; }` ) }) }) })