import { Template } from "@opticss/template-api" ; import { assert } from "chai" ; import { skip , suite , test } from "mocha-typescript" ; import { postcss } from "opticss" ; import { BlockFactory } from "../../src/BlockParser" ; import { AttrValue , Block , BlockClass , isAttrValue , isBlockClass } from "../../src/BlockTree" ; import { Options , resolveConfiguration } from "../../src/configuration" ; import { CssBlockError , TemplateAnalysisError } from "../../src/errors" ; import { assertParseError } from "../util/assertError" ; import { indented } from "../util/indented" ; import { MockImportRegistry } from "../util/MockImportRegistry" ; import { TestAnalyzer } from "../util/TestAnalyzer" ; type BlockAndRoot = [ Block , postcss . Container ] ; @ suite ( "Property Conflict Validator" ) export class TemplateAnalysisTests { private parseBlock ( css , filename , opts ? , blockName = "analysis" ) { let config = resolveConfiguration ( opts ) ; let factory = new BlockFactory ( config , postcss ) ; let root = postcss . parse ( css , { from : filename } ) ; return factory . parseRootFaultTolerant ( root , filename , blockName ) . then ( ( block ) => { return < BlockAndRoot > [ block , root ] ; } ) ; } @ test "properties of the same value, defined in the same order, do not throw an error" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; color: blue; background-color: yellow; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "properties with mismatched count of defs throw an error" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: red; color: blue; background: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; color: blue; background-color: yellow; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a (blocks/foo.block.css:3:37)
            block-b (blocks/b.block.css:1:31)
            block-b (blocks/b.block.css:1:43)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "all properties of the same type must match, in order, for a conflict to not be thrown" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; color: blue; color: yellow; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "if properties of the same type do not match, in order, a conflict is thrown" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: red; color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; color: blue; color: yellow; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a (blocks/foo.block.css:3:37)
            block-a (blocks/foo.block.css:3:50)
            block-b (blocks/b.block.css:1:31)
            block-b (blocks/b.block.css:1:43)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "properties that have potential conflicts in alternate rulesets throw an error" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; }
      .klass { color: blue; background: yellow; }
      :scope[colorful] .klass { color: red; }
    ` ) ; let css = `
    @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass { color: blue; background-color: yellow; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.klass (blocks/foo.block.css:4:16)
            block-b.klass (blocks/b.block.css:4:33)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) ) ; } @ test "properties that have potential conflicts in alternate rulesets pass when resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; }
      .klass { color: blue; background: yellow; }
      :scope[colorful] .klass { color: red; }
    ` ) ; let css = `
    @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass { color: resolve('b.klass'); color: blue; background-color: yellow; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "static roots throw error when a property is unresolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , `The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

  color:
    block-a (blocks/foo.block.css:3:37)
    block-b (blocks/b.block.css:1:31)

  background-color:
    block-a (blocks/foo.block.css:3:49)
    block-b (blocks/b.block.css:1:44)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; } ) ) ; } @ test "static roots pass error when a property is explicitly resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope {
        block-name: block-a;
        color: resolve('b');
        color: red;
        background-color: red;
        background-color: resolve('b');
      }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; assert . equal ( 1 , 1 ) ; } ) ; } @ test "static classes throw error when a property is unresolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; }
       .foo  { color: blue; background-color: yellow; }
      ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .bar  { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.bar (blocks/foo.block.css:4:15)
            block-b.foo (blocks/b.block.css:2:16)

          background-color:
            block-a.bar (blocks/foo.block.css:4:27)
            block-b.foo (blocks/b.block.css:2:29)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".bar" , "b.foo" ) . end ( ) ; } ) ) ; } @ test "static classes pass when a property is explicitly resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; }
       .foo  { color: blue; background-color: yellow; }
      ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .bar  {
        color: resolve('b.foo');
        color: red;
        background-color: red;
        background-color: resolve('b.foo');
      }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".bar" , "b.foo" ) . end ( ) ; assert . equal ( 1 , 1 ) ; } ) ; } @ test "static root and class throw error when a property is unresolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.foo (blocks/foo.block.css:4:15)
            block-b (blocks/b.block.css:1:31)

          background-color:
            block-a.foo (blocks/foo.block.css:4:27)
            block-b (blocks/b.block.css:1:44)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".foo" , "b" ) . end ( ) ; } ) ) ; } @ test "static root and class pass when properties are explicitly unresolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  {
        color: resolve('b');
        color: red;
        background-color: red;
        background-color: resolve('b');
      }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".foo" , "b" ) . end ( ) ; assert . equal ( 1 , 1 ) ; } ) ; } @ test "mixed static classes and states do not throw when on the same block" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; let css = `
      .foo { color: red; }
      .foo[bar]  { color: blue;}
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".foo" , ".foo[bar]" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) . then ( ( ) => { assert . ok ( 1 , "no error thrown" ) ; } ) ; } @ test "dynamic classes do not throw when on the same block" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; let css = `
      .foo { color: red; }
      .foo[bar]  { color: blue;}
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block ) . addDynamic ( [ ".foo" ] ) . addDynamic ( ".foo[bar]" ) . end ( ) ; } ) . then ( ( ) => { assert . ok ( 1 , "no error thrown" ) ; } ) ; } @ test "dynamic root and class throw error when a property is unresolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.foo (blocks/foo.block.css:4:15)
            block-b (blocks/b.block.css:1:31)

          background-color:
            block-a.foo (blocks/foo.block.css:4:27)
            block-b (blocks/b.block.css:1:44)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block ) . addDynamic ( [ ".foo" ] ) . addDynamic ( [ "b" ] ) . end ( ) ; } ) ) ; } @ test "dynamic root and class do not throw error when properties are resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  {
        color: resolve('b');
        color: red;
        background-color: red;
        background-color: resolve('b');
      }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block ) . addDynamic ( [ ".foo" ] ) . addDynamic ( [ "b" ] ) . end ( ) ; assert . equal ( 1 , 1 ) ; } ) ; } @ test "dynamic root and class throw error when a on same side of ternary" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.foo (blocks/foo.block.css:4:15)
            block-b (blocks/b.block.css:1:31)

          background-color:
            block-a.foo (blocks/foo.block.css:4:27)
            block-b (blocks/b.block.css:1:44)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block ) . addDynamic ( [ ".foo" , "b" ] ) . end ( ) ; } ) ) ; } @ test "dynamic root and class do not throw error when a on same side of ternary and explicitly resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  {
        color: resolve('b');
        color: red;
        background-color: red;
        background-color: resolve('b');
      }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block ) . addDynamic ( [ ".foo" , "b" ] ) . end ( ) ; assert . equal ( 1 , 1 ) ; } ) ; } @ test "dynamic root and class pass when on opposite sides of ternary" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .foo  { color: red; background: red; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block ) . addDynamic ( [ ".foo" ] , [ "b" ] ) . end ( ) ; } ) . then ( ( ) => { assert . ok ( 1 , "does not throw" ) ; } ) ; } @ test "conflicting classes and dynamic states throw" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo] { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-b (blocks/b.block.css:1:31)
            block-a[foo] (blocks/foo.block.css:4:21)

          background-color:
            block-b (blocks/b.block.css:1:44)
            block-a[foo] (blocks/foo.block.css:4:33)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . addDynamic ( "[foo]" ) . end ( ) ; } ) . then ( ( ) => { assert . ok ( 1 , "does not throw" ) ; } ) ) ; } @ test "conflicting classes and dynamic states do not throw when all properties are resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo] {
        color: resolve('b');
        color: red;
        background-color: red;
        background-color: resolve('b');
      }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . addDynamic ( "[foo]" ) . end ( ) ; assert . equal ( 1 , 1 ) ; } ) ; } @ test "conflicting static states throw when a property is unresolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; }
      .klass {}
      .klass[foo] { color: blue; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass {}
      .klass[foo] { color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.klass[foo] (blocks/foo.block.css:5:21)
            block-b.klass[foo] (blocks/b.block.css:4:21)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" , ".klass[foo]" , "b.klass[foo]" ) . end ( ) ; } ) ) ; } @ test "conflicting static states pass when a property is explicitly resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      .klass {}
      .klass[foo] { color: blue; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      .klass {}
      .klass[foo] { color: resolve('b.klass[foo]'); color: red; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" , ".klass[foo]" , "b.klass[foo]" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "conflicting dynamic states throw" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; }
      :scope[bar] { color: blue; background-color: yellow; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo] { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a[foo] (blocks/foo.block.css:4:21)
            block-b[bar] (blocks/b.block.css:3:21)

          background-color:
            block-a[foo] (blocks/foo.block.css:4:33)
            block-b[bar] (blocks/b.block.css:3:34)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . addDynamic ( "[foo]" ) . addDynamic ( "b:scope[bar]" ) . end ( ) ; } ) . then ( ( ) => { assert . ok ( 1 , "does not throw" ) ; } ) ) ; } @ test "conflicting dynamic states do not throw when all properties are resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; }
      :scope[bar] { color: blue; background-color: yellow; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo] {
        color: resolve('b:scope[bar]');
        color: red;
        background-color: red;
        background-color: resolve('b:scope[bar]'); }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b:scope" ) . addDynamic ( ":scope[foo]" ) . addDynamic ( "b:scope[bar]" ) . end ( ) ; } ) . then ( ( ) => { assert . equal ( 1 , 1 ) ; } ) ; } @ test "conflicting classes and dynamic state groups throw" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; color: blue; background-color: yellow; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo=one] { color: red; background-color: red; }
      :scope[foo=two] { text-decoration: underline; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-b (blocks/b.block.css:2:37)
            block-a[foo=one] (blocks/foo.block.css:4:25)

          background-color:
            block-b (blocks/b.block.css:2:50)
            block-a[foo=one] (blocks/foo.block.css:4:37)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . addStateGroup ( ":scope" , "[foo]" ) . end ( ) ; } ) . then ( ( ) => { assert . ok ( 1 , "does not throw" ) ; } ) ) ; } @ test "conflicting classes and dynamic state groups do not throw when all properties are resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; color: blue; background-color: yellow; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo=one] { color: resolve('b'); color: red; background-color: red; background-color: resolve('b');  }
      :scope[foo=two] { text-decoration: underline; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . addStateGroup ( ":scope" , "[foo]" ) . end ( ) ; } ) . then ( ( ) => { assert . equal ( 1 , 1 ) ; } ) ; } @ test "conflicting dynamic state groups throw" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; color: blue; background-color: yellow; }
      :scope[bar=one] { color: red; background-color: red; }
      :scope[bar=two] { color: yellow; background-color: purple; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      :scope[foo=one] { color: orange; background-color: green; }
      :scope[foo=two] { text-decoration: underline; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-b (blocks/b.block.css:2:37)
            block-a[foo=one] (blocks/foo.block.css:4:25)
            block-b[bar=one] (blocks/b.block.css:3:25)
            block-b[bar=two] (blocks/b.block.css:4:25)

          background-color:
            block-b (blocks/b.block.css:2:50)
            block-a[foo=one] (blocks/foo.block.css:4:40)
            block-b[bar=one] (blocks/b.block.css:3:37)
            block-b[bar=two] (blocks/b.block.css:4:40)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . addStateGroup ( ":scope" , "[foo]" ) . addStateGroup ( "b" , "[bar]" ) . end ( ) ; } ) . then ( ( ) => { assert . ok ( 1 , "does not throw" ) ; } ) ) ; } @ test "multiple conflicts on same property display correct error" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; background-color: yellow; }` ) ; imports . registerSource ( "blocks/c.block.css" , `
      :scope { block-name: block-c; }
      .bar { color: green; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      @block c from "./c.block.css";
      :scope { block-name: block-a; }
      .foo  { color: red; background-color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-b (blocks/b.block.css:1:31)
            block-c.bar (blocks/c.block.css:3:14)
            block-a.foo (blocks/foo.block.css:5:15)

          background-color:
            block-b (blocks/b.block.css:1:44)
            block-a.foo (blocks/foo.block.css:5:27)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , "b" , "c.bar" ) . addDynamic ( [ ".foo" ] ) . end ( ) ; } ) ) ; } @ test "conflicting roots pass when a property is explicitly resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; color: blue; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; color: resolve('b'); color: red; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ":scope" , "b" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test @ skip "multiple states on a compound selector do not throw if only one state is used" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
        :scope { block-name: block-a; }
        .klass-1 { color: blue; }
      ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass-2[one][two] { color: red; color: }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".klass-2" , "b.klass-1" ) . addStateGroup ( ".klass-2" , "[one]" ) . end ( ) ; } ) ; } @ test "conflicting classes pass when a property is explicitly resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `.klass { color: blue; }` ) ; let css = `
      @block b from "./b.block.css";
      .klass { color: resolve('b.klass'); color: red; }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "Block references in resolve statements that can't be resolved throw helpful error" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; let css = `
      :scope { block-name: block-a; }
      .klass:before { color: resolve('b.klass'); color: red; }
      .klass { color: red; }
    ` ; return assertParseError ( CssBlockError , `No Block named "b" found in scope. (blocks/foo.block.css:3:23)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "Style paths in resolve statements that can't be resolved throw helpful error" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; }` ) ; let css = `
      @block b from './b.block.css';
      :scope { block-name: block-a; }
      .klass:before { color: resolve('b.klass'); color: red; }
      .klass { color: red; }
    ` ; return assertParseError ( CssBlockError , `No Style ".klass" found on Block "block-b". (blocks/foo.block.css:4:23)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "resolutions don't leak out of pseudos" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; }
       .klass:before { color: yellow; }
       .klass { color: blue; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass:before { color: resolve('b.klass'); color: green; }
      .klass { color: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          color:
            block-a.klass (blocks/foo.block.css:5:16)
            block-b.klass (blocks/b.block.css:3:17)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "conflict validator expands shorthands" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `.klass { background-color: blue; border: 1px solid red; }` ) ; let css = `
      @block b from "./b.block.css";
      .klass { background: red; border-color: yellow; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          background-color:
            analysis.klass (blocks/foo.block.css:3:16)
            b.klass (blocks/b.block.css:1:10)

          border-color:
            analysis.klass (blocks/foo.block.css:3:33)
            b.klass (blocks/b.block.css:1:34)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "conflict validator expands shorthands and manages longhand re-declarations" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `.klass { border: 1px solid red; }` ) ; let css = `
      @block b from "./b.block.css";
      .klass { border-color: green; border-left-color: yellow }
    ` ; return assertParseError ( TemplateAnalysisError , `The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

  border-color:
    analysis.klass (blocks/foo.block.css:3:16)
    b.klass (blocks/b.block.css:1:10)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "conflict validator expands shorthands and uses lowest common property" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `.klass { background-color: blue; border: 1px solid red; }` ) ; let css = `
      @block b from "./b.block.css";
      .klass { background: red; border-left-color: yellow }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          background-color:
            analysis.klass (blocks/foo.block.css:3:16)
            b.klass (blocks/b.block.css:1:10)

          border-left-color:
            analysis.klass (blocks/foo.block.css:3:33)
            b.klass (blocks/b.block.css:1:34)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "conflict validator expands shorthands and manages multiple longhand conflicts" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `.klass { border: 1px solid red; }` ) ; let css = `
      @block b from "./b.block.css";
      .klass { border-right-color: green; border-left-color: yellow }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          border-left-color:
            analysis.klass (blocks/foo.block.css:3:43)
            b.klass (blocks/b.block.css:1:10)

          border-right-color:
            analysis.klass (blocks/foo.block.css:3:16)
            b.klass (blocks/b.block.css:1:10)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "conflicts may be resolved by a shorthand" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: b; }
      .klass { background-color: blue; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: a; }
      .klass { background-color: red; background: resolve('b.klass'); }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "shorthand conflicts cannot be resolved by a longhand" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: b-block; }
      .klass { border-color: blue; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: a-block; }
      .klass { border-color: red; border-left-color: resolve('b.klass'); }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          border-color:
            a-block.klass (blocks/foo.block.css:4:16)
            b-block.klass (blocks/b.block.css:3:16)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "conflict validation errors are thrown on custom properties" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `
      :scope { block-name: block-b; }
      .klass { custom-prop: blue; }
    ` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass { custom-prop: red; }
    ` ; return assertParseError ( TemplateAnalysisError , indented `
        The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

          custom-prop:
            block-a.klass (blocks/foo.block.css:4:16)
            block-b.klass (blocks/b.block.css:3:16)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "custom properties may be resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/b.block.css" , `:scope { block-name: block-b; }
       .klass { custom-prop: blue; }` ) ; let css = `
      @block b from "./b.block.css";
      :scope { block-name: block-a; }
      .klass { custom-prop: red; custom-prop: resolve('b.klass'); }
    ` ; return this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".klass" , "b.klass" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ; } @ test "property conflicts that arise from in-stylesheet composition must be resolved" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "blocks/biz.block.css" , `.baz { color: red; }` ) ; let css = `@block biz from "./biz.block.css";
               .bar { composes: biz.baz; color: green; }` ; return assertParseError ( TemplateAnalysisError , indented `
      The following property conflicts must be resolved for these composed Styles: (templates/my-template.hbs:10:32)

        color:
          analysis.bar (blocks/foo.block.css:2:42)
          biz.baz (blocks/biz.block.css:1:8)` , this . parseBlock ( css , "blocks/foo.block.css" , options ) . then ( ( [ block , _ ] ) => { constructElement ( block , ".bar" ) . end ( ) ; assert . deepEqual ( 1 , 1 ) ; } ) ) ; } @ test "property conflicts don't arise internal to in-stylesheet composed block inheritance trees" ( ) { let imports = new MockImportRegistry ( ) ; let options = { importer : imports . importer ( ) } ; imports . registerSource ( "base.block.css" , `.baz { color: red; }` ) ; imports . registerSource ( "biz.block.css" , `
        @block base from "./base.block.css";
        :scope { extends: base; }
        .baz { color: blue; }
      ` ) ; let css = `@block biz from "./biz.block.css";
               .bar { composes: biz.baz; }` ; return this . parseBlock ( css , "./foo.block.css" , options ) . then ( ( [ block , _ ] ) => { return constructElement ( block , ".bar" ) . end ( ) ; } ) . then ( ( ) => { assert . deepEqual ( 1 , 1 ) ; } ) ; } } type TernaryCondition = boolean ; function constructElement ( block , ... styles ) { let info = new Template ( "templates/my-template.hbs" ) ; let analyzer = new TestAnalyzer ( new BlockFactory ( { } ) ) ; let analysis = analyzer . newAnalysis ( info ) ; analysis . addBlock ( ":scope" , block ) ; block . eachBlockExport ( ( name , ref ) => { analysis . addBlock ( name , ref ) ; } ) ; let element = analysis . startElement < boolean , string , TernaryCondition > ( { line : 10 , column : 32 } ) ; for ( let path of styles ) { let style = block . lookup ( path ) ; if ( ! style ) { throw Error ( `Error looking up Style ${ path } for test.` ) ; } if ( isBlockClass ( style ) ) { element . addStaticClass ( style ) ; } else if ( isAttrValue ( style ) ) { element . addStaticAttr ( style . blockClass , style ) ; } } return { addDynamic ( truthy , falsy ? ) { if ( typeof truthy === "string" ) { let state = block . lookup ( truthy ) as AttrValue ; element . addDynamicAttr ( state . blockClass , state , true ) ; return this ; } let truthyStyles = truthy . map ( ( s ) => block . lookup ( s ) as BlockClass ) ; let falsyStyles = falsy ? falsy . map ( ( s ) => block . lookup ( s ) as BlockClass ) : undefined ; element . addDynamicClasses ( { condition : true , whenTrue : truthyStyles , whenFalse : falsyStyles , } ) ; return this ; } , addStateGroup ( base , groupName ) { let baseStyle = block . lookup ( base ) as BlockClass ; let group = baseStyle . resolveAttribute ( groupName ) ; if ( ! group ) throw new Error ( `Error resolving group ${ groupName }` ) ; element . addDynamicGroup ( baseStyle , group , "" ) ; return this ; } , end ( ) { analysis . endElement ( element ) ; return analysis ; } , } ; }