├── .gitignore ├── unit-test.sh ├── run-galen-tests.sh ├── run-galen-tests-for-bad-website.sh ├── rules-tests ├── specs │ ├── amount.gspec │ ├── text-multiple.mock.js │ ├── ratio.gspec │ ├── tags.gspec │ ├── general-rules.gspec │ ├── init.gspec │ ├── sides.mock.js │ ├── stretch.gspec │ ├── squared.gspec │ ├── text-multiple.gspec │ ├── stretch-vertically.gspec │ ├── sides.gspec │ ├── table-layout.mock.js │ ├── table-layout.gspec │ ├── one-liners.gspec │ ├── conditional-rules.gspec │ ├── conditional-rules.mock.js │ ├── alignment.gspec │ ├── mock.gspec.js │ └── location.gspec ├── text-multiple.test.js ├── conditional-rules.test.js ├── stretch.test.js ├── general-rules.test.js ├── spec-one-liners.test.js ├── stretch-vertically.test.js ├── sides.test.js ├── alignment.test.js ├── table-layout.test.js ├── location.test.js ├── rule.test.js └── init.js ├── galen.test ├── box-component.gspec ├── examples.gspec ├── galen-extras ├── galen-extras-rules.js └── galen-extras-rules.gspec ├── LICENSE-2.0.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | reports/ 2 | *.swp 3 | 4 | -------------------------------------------------------------------------------- /unit-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd rules-tests 4 | galen test . 5 | -------------------------------------------------------------------------------- /run-galen-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | galen test galen.test --htmlreport reports -DwebsiteFolder="website" 4 | -------------------------------------------------------------------------------- /run-galen-tests-for-bad-website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | galen test galen.test --htmlreport reports -DwebsiteFolder="website-bad" 4 | -------------------------------------------------------------------------------- /rules-tests/specs/amount.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | = Main = 4 | |amount of visible icon-* should be 2 5 | |amount of absent logo-* should be < 2 6 | -------------------------------------------------------------------------------- /rules-tests/specs/text-multiple.mock.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | mockFunction("findAll") 4 | .thenReturn([ 5 | {name: "label-1"}, 6 | {name: "label-2"}, 7 | {name: "label-3"} 8 | ]); 9 | -------------------------------------------------------------------------------- /rules-tests/specs/ratio.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | = Main = 4 | |icon-* should have ~ 33% width/height ratio 5 | 6 | icon-3: 7 | |has 10% width/height ratio 8 | -------------------------------------------------------------------------------- /rules-tests/specs/tags.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | 4 | = Main = 5 | |login_button, cancel_button should be visible on desktop, tablet but absent on mobile 6 | 7 | |login_button should be absent on mobile but on desktop, tablet: 8 | inside screen 0px top left 9 | -------------------------------------------------------------------------------- /rules-tests/specs/general-rules.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | 4 | = Main = 5 | button: 6 | |is more or less readable 7 | 8 | |icon-* should be more or less readable 9 | 10 | 11 | login_button: 12 | |is tapable 13 | 14 | |cancel_button,register_button should be tapable 15 | -------------------------------------------------------------------------------- /rules-tests/specs/init.gspec: -------------------------------------------------------------------------------- 1 | @import ../../galen-extras/galen-extras-rules.gspec 2 | @script mock.gspec.js 3 | 4 | @objects 5 | icon-1 #icon1 6 | icon-2 #icon2 7 | 8 | menu_icon #menu_icon 9 | 10 | box-1 #box1 11 | box-2 #box2 12 | box-3 #box3 13 | 14 | -------------------------------------------------------------------------------- /rules-tests/specs/sides.mock.js: -------------------------------------------------------------------------------- 1 | 2 | var boxes = [ 3 | {name: "box-1"}, 4 | {name: "box-2"}, 5 | {name: "box-3"} 6 | ]; 7 | 8 | mockFunction("findAll") 9 | .thenReturn(boxes) 10 | .thenReturn(boxes) 11 | .thenReturn(boxes) 12 | .thenReturn(boxes) 13 | .thenReturn(boxes) 14 | .thenReturn(boxes) 15 | .thenReturn(boxes) 16 | -------------------------------------------------------------------------------- /rules-tests/specs/stretch.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | 4 | = Main = 5 | some_panel: 6 | | stretches to main_container with 10px margin 7 | | stretches to main_container 8 | 9 | |login_panel, register_panel should stretch to main_container with 10px margin 10 | |login_panel, register_panel should stretch to main_container 11 | -------------------------------------------------------------------------------- /rules-tests/specs/squared.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | = Main = 4 | |icon-* should be squared 5 | 6 | |icon-* should be almost squared 7 | 8 | icon-1: 9 | |squared with ~ 50px size 10 | 11 | icon-2: 12 | |squared 13 | 14 | icon-3: 15 | |almost squared 16 | 17 | |icon-* should be squared with 23 to 30 px size 18 | -------------------------------------------------------------------------------- /rules-tests/specs/text-multiple.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | @script text-multiple.mock.js 4 | 5 | @objects 6 | label-1 #label1 7 | label-2 #label2 8 | label-3 #label3 9 | 10 | @groups 11 | labels label-# 12 | 13 | = Main = 14 | |text of all &labels should be ["text 1", "text 2", "text 3 with \"double quotes\""] 15 | 16 | 17 | -------------------------------------------------------------------------------- /galen.test: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @@ table devices 6 | | tag | size | 7 | | desktop | 1024x768 | 8 | | tablet | 800x600 | 9 | | mobile | 500x700 | 10 | 11 | @@ parameterized using devices 12 | Home page test on ${tag} device 13 | http://galenframework.github.io/galen-extras/${websiteFolder}/index.html ${size} 14 | check examples.gspec --include "${tag}" 15 | -------------------------------------------------------------------------------- /rules-tests/specs/stretch-vertically.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | 4 | = Main = 5 | some_panel: 6 | | stretches vertically to main_container with 10px margin 7 | | stretches vertically to main_container 8 | 9 | |login_panel, register_panel should stretch vertically to main_container with 10px margin 10 | |login_panel, register_panel should stretch vertically to main_container 11 | -------------------------------------------------------------------------------- /rules-tests/text-multiple.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | testSpec("text-multiple.gspec", [], function (spec) { 4 | assertSpec(spec) 5 | .hasRuleSection('text of all &labels should be ["text 1", "text 2", "text 3 with \\"double quotes\\""]', { 6 | "label-1":['text is "text 1"'], 7 | "label-2":['text is "text 2"'], 8 | "label-3":['text is "text 3 with \\"double quotes\\""'] 9 | }) 10 | }); 11 | -------------------------------------------------------------------------------- /rules-tests/specs/sides.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | @script sides.mock.js 3 | 4 | = Main = 5 | | box-* sides are inside main_container with 5px margin from top and bottom 6 | 7 | | box-* sides are vertically inside main_container with 4px margin 8 | 9 | | box-* sides are vertically inside main_container 10 | 11 | | box-* sides are horizontally inside main_container with 3px margin 12 | 13 | | box-* sides are horizontally inside main_container 14 | -------------------------------------------------------------------------------- /box-component.gspec: -------------------------------------------------------------------------------- 1 | @import galen-extras/galen-extras-rules.gspec 2 | 3 | @objects 4 | heading .panel-heading 5 | body .panel-body 6 | 7 | 8 | = Panel contents = 9 | heading: 10 | inside parent 0 to 1px top left right 11 | height ~ 41px 12 | 13 | | heading, body sides are inside parent with 0 to 1px margin from top and bottom 14 | 15 | | heading, body are aligned vertically above each other with 0 to 1px margin 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /rules-tests/specs/table-layout.mock.js: -------------------------------------------------------------------------------- 1 | var result = [{ 2 | name: "brick-1" 3 | }, { 4 | name: "brick-2" 5 | }, { 6 | name: "brick-3" 7 | }, { 8 | name: "brick-4" 9 | }, { 10 | name: "brick-5" 11 | }, { 12 | name: "brick-6" 13 | }]; 14 | mockFunction("findAll") 15 | .thenReturn(result) 16 | .thenReturn(result) 17 | .thenReturn(result) 18 | .thenReturn(result) 19 | .thenReturn(result) 20 | .thenReturn(result); 21 | -------------------------------------------------------------------------------- /rules-tests/specs/table-layout.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | @objects 4 | brick-1 #brick-1 5 | brick-2 #brick-2 6 | brick-3 #brick-3 7 | brick-4 #brick-4 8 | brick-5 #brick-5 9 | brick-6 #brick-6 10 | 11 | @script table-layout.mock.js 12 | 13 | = Main = 14 | | brick-* are rendered in 2 column table layout 15 | | brick-* are rendered in 2 column table layout, with 14px margin 16 | | brick-* are rendered in 2 column table layout, with 14px vertical and 12px horizontal margin 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /rules-tests/specs/one-liners.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | @script 4 | mockFunction("first") 5 | .thenReturn({name: "box-1"}) 6 | .thenReturn({name: "box-1"}); 7 | 8 | mockFunction("last") 9 | .thenReturn({name: "box-3"}) 10 | .thenReturn({name: "box-3"}); 11 | 12 | = Main = 13 | | every icon-* is inside menu 0px top bottom and has width 100px 14 | 15 | | every icon-* is inside menu 0px left 16 | 17 | | first box-* has width 134px 18 | | last box-* is visible 19 | 20 | | first box-*: 21 | centered vertically inside screen 22 | 23 | | last box-*: 24 | centered horizontally inside screen 25 | -------------------------------------------------------------------------------- /rules-tests/conditional-rules.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | 5 | testSpec("conditional-rules.gspec", [], function (spec) { 6 | assertSpec(spec) 7 | .hasRuleSection("if all icon-* are visible", { 8 | "screen": ['text is "qwe1"'] 9 | }) 10 | .doesNotHaveRuleSection("if all box-* are visible") 11 | .hasRuleSection("if none of icon-* are visible", { 12 | "screen": ['text is "qwe3"'] 13 | }) 14 | .doesNotHaveRuleSection("if none of box-* are visible") 15 | .hasRuleSection("if any of icon-* is visible", { 16 | "screen": ['text is "qwe5"'] 17 | }) 18 | .doesNotHaveRuleSection("if any of box-* is visible") 19 | }); 20 | -------------------------------------------------------------------------------- /rules-tests/specs/conditional-rules.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | @script conditional-rules.mock.js 4 | 5 | 6 | = Main = 7 | | if all icon-* are visible 8 | screen: 9 | text is "qwe1" 10 | 11 | | if all box-* are visible 12 | screen: 13 | text is "qwe2" 14 | 15 | 16 | 17 | 18 | | if none of icon-* are visible 19 | screen: 20 | text is "qwe3" 21 | 22 | | if none of box-* are visible 23 | screen: 24 | text is "qwe4" 25 | 26 | 27 | 28 | 29 | | if any of icon-* is visible 30 | screen: 31 | text is "qwe5" 32 | 33 | | if any of box-* is visible 34 | screen: 35 | text is "qwe6" 36 | -------------------------------------------------------------------------------- /rules-tests/specs/conditional-rules.mock.js: -------------------------------------------------------------------------------- 1 | 2 | mockFunction("findAll") 3 | .thenReturn([{ 4 | isVisible: function () {return true;} 5 | }, { 6 | isVisible: function () {return true;} 7 | }] 8 | ) 9 | .thenReturn([{ 10 | isVisible: function () {return true;} 11 | }, { 12 | isVisible: function () {return false;} 13 | }] 14 | ) 15 | .thenReturn([{ 16 | isVisible: function () {return false;} 17 | }, { 18 | isVisible: function () {return false;} 19 | }] 20 | ) 21 | .thenReturn([{ 22 | isVisible: function () {return false;} 23 | }, { 24 | isVisible: function () {return true;} 25 | }] 26 | ) 27 | .thenReturn([{ 28 | isVisible: function () {return false;} 29 | }, { 30 | isVisible: function () {return true;} 31 | }] 32 | ) 33 | .thenReturn([{ 34 | isVisible: function () {return false;} 35 | }, { 36 | isVisible: function () {return false;} 37 | }] 38 | ) 39 | -------------------------------------------------------------------------------- /rules-tests/specs/alignment.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | # Mocking js api calls 4 | @script 5 | mockFunction("findAll") 6 | .thenReturn([{ 7 | right: function () {return 100} 8 | }, { 9 | left: function () {return 105} 10 | }]) 11 | .thenReturn([{ 12 | bottom: function () {return 100} 13 | }, { 14 | top: function () {return 110} 15 | }]); 16 | = Main = 17 | |box-* are aligned horizontally next to each other 18 | |box-* are aligned vertically above each other 19 | 20 | |box-* are aligned horizontally next to each other with 11px margin 21 | |box-* are aligned vertically above each other with 12px margin 22 | 23 | 24 | |box-* are aligned horizontally next to each other with equal distance 25 | |box-* are aligned vertically above each other with equal distance 26 | 27 | 28 | |box-* are placed above each other with 123px margin 29 | |box-* are placed next to each other with 21px margin 30 | -------------------------------------------------------------------------------- /rules-tests/stretch.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("stretch.gspec", [], function (spec) { 5 | assertSpec(spec) 6 | .hasObjectWithSpecGroups("some_panel", { 7 | "stretches to main_container with 10px margin": [ 8 | "inside main_container 10px left right", 9 | ], 10 | "stretches to main_container": [ 11 | "inside main_container -1 to 1px left right", 12 | ] 13 | }) 14 | .hasRuleSection("login_panel, register_panel should stretch to main_container with 10px margin", { 15 | "login_panel": [ 16 | "inside main_container 10px left right", 17 | ], 18 | "register_panel": [ 19 | "inside main_container 10px left right", 20 | ] 21 | }) 22 | .hasRuleSection("login_panel, register_panel should stretch to main_container", { 23 | "login_panel": [ 24 | "inside main_container -1 to 1px left right", 25 | ], 26 | "register_panel": [ 27 | "inside main_container -1 to 1px left right", 28 | ] 29 | }) 30 | }); 31 | -------------------------------------------------------------------------------- /rules-tests/general-rules.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("general-rules.gspec", [], function (spec) { 5 | assertSpec(spec) 6 | .hasObjectWithSpecGroups("button", { 7 | "is more or less readable": [ 8 | "width > 30px", 9 | "height > 10px" 10 | ] 11 | }) 12 | .hasRuleSection("icon-* should be more or less readable", { 13 | "icon-1": [ 14 | "width > 30px", 15 | "height > 10px" 16 | ], 17 | "icon-2": [ 18 | "width > 30px", 19 | "height > 10px" 20 | ] 21 | }) 22 | .hasObjectWithSpecGroups("login_button", { 23 | "is tapable": [ 24 | "width > 50px", 25 | "height > 30px" 26 | ] 27 | }) 28 | .hasRuleSection("cancel_button,register_button should be tapable", { 29 | "cancel_button": [ 30 | "width > 50px", 31 | "height > 30px" 32 | ], 33 | "register_button": [ 34 | "width > 50px", 35 | "height > 30px" 36 | ] 37 | }) 38 | }); 39 | -------------------------------------------------------------------------------- /rules-tests/specs/mock.gspec.js: -------------------------------------------------------------------------------- 1 | function MockedFunction(funcName) { 2 | this.funcName = funcName; 3 | this.expectedReturns = []; 4 | } 5 | MockedFunction.prototype.invoke = function () { 6 | if (this.expectedReturns.length > 0) { 7 | var value = this.expectedReturns.shift(); 8 | return value; 9 | } else { 10 | throw new Error("Unexpected mock invokation for function: " + this.funcName); 11 | } 12 | }; 13 | MockedFunction.prototype.thenReturn = function (returnValue) { 14 | this.expectedReturns.push(returnValue); 15 | return this; 16 | }; 17 | 18 | this._mock = { 19 | funcs: {}, 20 | addMock: function (funcName) { 21 | this.funcs[funcName] = new MockedFunction(funcName); 22 | return this.funcs[funcName]; 23 | } 24 | }; 25 | 26 | this.mockFunction = function (funcName) { 27 | var mock = this._mock.addMock(funcName); 28 | this[funcName] = function () { 29 | return mock.invoke(); 30 | }; 31 | return mock; 32 | }; 33 | 34 | this.mockReturn = function (returnValue) { 35 | return function () {return returnValue;}; 36 | }; 37 | -------------------------------------------------------------------------------- /rules-tests/spec-one-liners.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | testSpec("one-liners.gspec", [], function (spec) { 4 | assertSpec(spec) 5 | .hasRuleSection("every icon-* is inside menu 0px top bottom and has width 100px", { 6 | "icon-1": [ 7 | "inside menu 0px top bottom", 8 | "width 100px" 9 | ], 10 | "icon-2": [ 11 | "inside menu 0px top bottom", 12 | "width 100px" 13 | ] 14 | }) 15 | .hasRuleSection("every icon-* is inside menu 0px left", { 16 | "icon-1": [ 17 | "inside menu 0px left" 18 | ], 19 | "icon-2": [ 20 | "inside menu 0px left" 21 | ] 22 | }) 23 | .hasRuleSection("first box-* has width 134px", { 24 | "box-1": ["width 134px"] 25 | }) 26 | .hasRuleSection("last box-* is visible", { 27 | "box-3": ["visible"] 28 | }) 29 | .hasRuleSection("first box-*:", { 30 | "box-1": ["centered vertically inside screen"] 31 | }) 32 | .hasRuleSection("last box-*:", { 33 | "box-3": ["centered horizontally inside screen"] 34 | }) 35 | }); 36 | 37 | 38 | -------------------------------------------------------------------------------- /rules-tests/stretch-vertically.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("stretch-vertically.gspec", [], function (spec) { 5 | assertSpec(spec) 6 | .hasObjectWithSpecGroups("some_panel", { 7 | "stretches vertically to main_container with 10px margin": [ 8 | "inside main_container 10px top bottom", 9 | ], 10 | "stretches vertically to main_container": [ 11 | "inside main_container -1 to 1px top bottom", 12 | ] 13 | }) 14 | .hasRuleSection("login_panel, register_panel should stretch vertically to main_container with 10px margin", { 15 | "login_panel": [ 16 | "inside main_container 10px top bottom", 17 | ], 18 | "register_panel": [ 19 | "inside main_container 10px top bottom", 20 | ] 21 | }) 22 | .hasRuleSection("login_panel, register_panel should stretch vertically to main_container", { 23 | "login_panel": [ 24 | "inside main_container -1 to 1px top bottom", 25 | ], 26 | "register_panel": [ 27 | "inside main_container -1 to 1px top bottom", 28 | ] 29 | }) 30 | }); 31 | -------------------------------------------------------------------------------- /rules-tests/specs/location.gspec: -------------------------------------------------------------------------------- 1 | @import init.gspec 2 | 3 | = Main = 4 | login_panel: 5 | | located at the top inside main_container with 10px margin 6 | | located at the right bottom inside main_container with 11px margin 7 | 8 | register_panel: 9 | | located at the top left inside main_container 10 | 11 | |comments_section should be located at the right inside main_container with ~10px margin 12 | |ads_section should be located at the top left inside main_container 13 | 14 | 15 | ads_panel: 16 | |located on the left side of main_container and takes 70 % of its width 17 | |located on the top side of main_container and takes 50 % of its height 18 | 19 | |login_panel should be located on the left side of main_container and take 70 % of its width 20 | |register_panel should be located on the top side of main_container and take 50 % of its height 21 | 22 | 23 | user_panel: 24 | |located on the left side of main_container with ~10px margin and takes 70 % of its width 25 | |located on the top side of main_container with 10 to 12px margin and takes 50 % of its height 26 | 27 | |login_panel should be located on the left side of main_container with 10px margin and take 70 % of its width 28 | |register_panel should be located on the top side of main_container with < 123px margin and take 50 % of its height 29 | -------------------------------------------------------------------------------- /rules-tests/sides.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("sides.gspec", [], function (spec) { 5 | assertSpec(spec) 6 | .hasRuleSection("box-* sides are inside main_container with 5px margin from top and bottom", { 7 | "box-1": ["inside main_container 5px top"], 8 | "box-2": ["inside main_container"], 9 | "box-3": ["inside main_container 5px bottom"] 10 | }) 11 | .hasRuleSection("box-* sides are vertically inside main_container with 4px margin", { 12 | "box-1": ["inside main_container 4px top"], 13 | "box-2": ["inside main_container"], 14 | "box-3": ["inside main_container 4px bottom"] 15 | }) 16 | .hasRuleSection("box-* sides are vertically inside main_container", { 17 | "box-1": ["inside main_container -1 to 1 px top"], 18 | "box-2": ["inside main_container"], 19 | "box-3": ["inside main_container -1 to 1 px bottom"] 20 | }) 21 | .hasRuleSection("box-* sides are horizontally inside main_container with 3px margin", { 22 | "box-1": ["inside main_container 3px left"], 23 | "box-2": ["inside main_container"], 24 | "box-3": ["inside main_container 3px right"] 25 | }) 26 | .hasRuleSection("box-* sides are horizontally inside main_container", { 27 | "box-1": ["inside main_container -1 to 1 px left"], 28 | "box-2": ["inside main_container"], 29 | "box-3": ["inside main_container -1 to 1 px right"] 30 | }) 31 | }); 32 | -------------------------------------------------------------------------------- /examples.gspec: -------------------------------------------------------------------------------- 1 | 2 | @import galen-extras/galen-extras-rules.gspec 3 | 4 | @objects 5 | header #header .middle-wrapper 6 | box-* .box-container .box .panel 7 | menu #menu .middle-wrapper 8 | item-* ul li 9 | content #content 10 | greeting #content h1 11 | footer #footer .middle-wrapper 12 | 13 | @groups 14 | (box, boxes) box-* 15 | (menu_item, menu_items) menu.item-* 16 | skeleton_elements header, menu, content, footer 17 | skeleton_element &skeleton_elements 18 | image_validation header, menu.item-* 19 | 20 | 21 | = Skeleton = 22 | | &skeleton_elements sides are vertically inside screen 23 | | &skeleton_elements are aligned vertically above each other 24 | 25 | = A page should be centered horizontally inside screen with 900px on desktop, but on mobile and tablet it should stretch to screen = 26 | | every &skeleton_element is centered horizontally inside screen 27 | 28 | @on desktop 29 | | every &skeleton_element has width 900px 30 | @on mobile, tablet 31 | | every &skeleton_element is aligned vertically all screen 32 | 33 | = Menu items should adapt layout = 34 | @on * 35 | | amount of visible &menu_items should be 4 36 | | every &menu_item is inside menu and has height ~ 64px 37 | | first &menu_item is inside menu 0px top left 38 | 39 | @on desktop, tablet 40 | | &menu_items are aligned horizontally next to each other 41 | 42 | @on mobile 43 | | &menu_items are rendered in 2 column table layout 44 | 45 | 46 | = Main Section = 47 | = Greeting = 48 | greeting: 49 | height 30 to 60px 50 | inside content 40px top, 20px left right 51 | 52 | @on * 53 | | amount of visible &boxes should be 3 54 | | test all &boxes with box-component.gspec 55 | 56 | @on desktop, tablet 57 | | &boxes are aligned horizontally next to each other with equal distance 58 | | &box sides are inside content with 20px margin from left and right 59 | | every &box is below greeting ~ 10px 60 | | every &box is above footer > 19px 61 | 62 | @on mobile 63 | | &boxes are aligned vertically above each other with 20px margin 64 | | every &box is inside content 20px left right 65 | | first &box is below greeting ~ 10px 66 | | last &box is above footer > 19px 67 | 68 | = If conditions = 69 | | if all header, menu are visible 70 | header: 71 | above menu 72 | -------------------------------------------------------------------------------- /rules-tests/alignment.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | testSpec("alignment.gspec", [], function (spec) { 4 | assertSpec(spec) 5 | .hasRuleSection("box-* are aligned horizontally next to each other", { 6 | "box-1": [ 7 | "aligned horizontally all box-2 1px", 8 | "left-of box-2 -1 to 1px" 9 | ], 10 | "box-2": [ 11 | "aligned horizontally all box-3 1px", 12 | "left-of box-3 -1 to 1px" 13 | ] 14 | }) 15 | .hasRuleSection("box-* are aligned vertically above each other", { 16 | "box-1": [ 17 | "aligned vertically all box-2 1px", 18 | "above box-2 -1 to 1px" 19 | ], 20 | "box-2": [ 21 | "aligned vertically all box-3 1px", 22 | "above box-3 -1 to 1px" 23 | ] 24 | }) 25 | .hasRuleSection("box-* are aligned horizontally next to each other with 11px margin", { 26 | "box-1": [ 27 | "aligned horizontally all box-2 1px", 28 | "left-of box-2 11px" 29 | ], 30 | "box-2": [ 31 | "aligned horizontally all box-3 1px", 32 | "left-of box-3 11px" 33 | ] 34 | }) 35 | .hasRuleSection("box-* are aligned vertically above each other with 12px margin", { 36 | "box-1": [ 37 | "aligned vertically all box-2 1px", 38 | "above box-2 12px" 39 | ], 40 | "box-2": [ 41 | "aligned vertically all box-3 1px", 42 | "above box-3 12px" 43 | ] 44 | }) 45 | .hasRuleSection("box-* are aligned horizontally next to each other with equal distance", { 46 | "box-1": [ 47 | "aligned horizontally all box-2 1px", 48 | "left-of box-2 4 to 6 px" 49 | ], 50 | "box-2": [ 51 | "aligned horizontally all box-3 1px", 52 | "left-of box-3 4 to 6 px" 53 | ] 54 | }) 55 | .hasRuleSection("box-* are aligned vertically above each other with equal distance", { 56 | "box-1": [ 57 | "aligned vertically all box-2 1px", 58 | "above box-2 9 to 11 px" 59 | ], 60 | "box-2": [ 61 | "aligned vertically all box-3 1px", 62 | "above box-3 9 to 11 px" 63 | ] 64 | }) 65 | .hasRuleSection("box-* are placed above each other with 123px margin", { 66 | "box-1": [ 67 | "above box-2 123px" 68 | ], 69 | "box-2": [ 70 | "above box-3 123px" 71 | ] 72 | }) 73 | .hasRuleSection("box-* are placed next to each other with 21px margin", { 74 | "box-1": [ 75 | "left-of box-2 21px" 76 | ], 77 | "box-2": [ 78 | "left-of box-3 21px" 79 | ] 80 | }) 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /rules-tests/table-layout.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("table-layout.gspec", [], function (spec) { 5 | assertSpec(spec) 6 | .hasRuleSection("brick-* are rendered in 2 column table layout", { 7 | "brick-1": [ 8 | "aligned horizontally all brick-2", 9 | "left-of brick-2 -1 to 1px", 10 | "aligned vertically all brick-3", 11 | "above brick-3 -1 to 1px" 12 | ], 13 | "brick-2": [ 14 | "aligned vertically all brick-4", 15 | "above brick-4 -1 to 1px" 16 | ], 17 | "brick-3": [ 18 | "aligned horizontally all brick-4", 19 | "left-of brick-4 -1 to 1px", 20 | "aligned vertically all brick-5", 21 | "above brick-5 -1 to 1px" 22 | ], 23 | "brick-4": [ 24 | "aligned vertically all brick-6", 25 | "above brick-6 -1 to 1px" 26 | ], 27 | "brick-5": [ 28 | "aligned horizontally all brick-6", 29 | "left-of brick-6 -1 to 1px" 30 | ] 31 | }) 32 | .hasRuleSection("brick-* are rendered in 2 column table layout, with 14px margin", { 33 | "brick-1": [ 34 | "aligned horizontally all brick-2", 35 | "left-of brick-2 14px", 36 | "aligned vertically all brick-3", 37 | "above brick-3 14px" 38 | ], 39 | "brick-2": [ 40 | "aligned vertically all brick-4", 41 | "above brick-4 14px" 42 | ], 43 | "brick-3": [ 44 | "aligned horizontally all brick-4", 45 | "left-of brick-4 14px", 46 | "aligned vertically all brick-5", 47 | "above brick-5 14px" 48 | ], 49 | "brick-4": [ 50 | "aligned vertically all brick-6", 51 | "above brick-6 14px" 52 | ], 53 | "brick-5": [ 54 | "aligned horizontally all brick-6", 55 | "left-of brick-6 14px" 56 | ] 57 | }) 58 | .hasRuleSection("brick-* are rendered in 2 column table layout, with 14px vertical and 12px horizontal margin", { 59 | "brick-1": [ 60 | "aligned horizontally all brick-2", 61 | "left-of brick-2 12px", 62 | "aligned vertically all brick-3", 63 | "above brick-3 14px" 64 | ], 65 | "brick-2": [ 66 | "aligned vertically all brick-4", 67 | "above brick-4 14px" 68 | ], 69 | "brick-3": [ 70 | "aligned horizontally all brick-4", 71 | "left-of brick-4 12px", 72 | "aligned vertically all brick-5", 73 | "above brick-5 14px" 74 | ], 75 | "brick-4": [ 76 | "aligned vertically all brick-6", 77 | "above brick-6 14px" 78 | ], 79 | "brick-5": [ 80 | "aligned horizontally all brick-6", 81 | "left-of brick-6 12px" 82 | ] 83 | }) 84 | }); 85 | -------------------------------------------------------------------------------- /rules-tests/location.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("location.gspec", [], function (spec) { 5 | assertSpec(spec) 6 | .hasObjectWithSpecGroups("login_panel", { 7 | "located at the top inside main_container with 10px margin": [ 8 | "inside main_container 10px top", 9 | ], 10 | "located at the right bottom inside main_container with 11px margin": [ 11 | "inside main_container 11px right bottom", 12 | ] 13 | }) 14 | .hasObjectWithSpecGroups("register_panel", { 15 | "located at the top left inside main_container": [ 16 | "inside main_container -1 to 1px top left", 17 | ] 18 | }) 19 | .hasRuleSection("comments_section should be located at the right inside main_container with ~10px margin", { 20 | "comments_section": ["inside main_container ~10px right"], 21 | }) 22 | .hasRuleSection("ads_section should be located at the top left inside main_container", { 23 | "ads_section": ["inside main_container -1 to 1px top left"], 24 | }) 25 | .hasObjectWithSpecGroups("ads_panel", { 26 | "located on the left side of main_container and takes 70 % of its width": [ 27 | "inside main_container -1 to 1px left", 28 | "width 70 % of main_container/width", 29 | ], 30 | "located on the top side of main_container and takes 50 % of its height": [ 31 | "inside main_container -1 to 1px top", 32 | "width 50 % of main_container/height", 33 | ] 34 | }) 35 | .hasRuleSection("login_panel should be located on the left side of main_container and take 70 % of its width", { 36 | "login_panel": [ 37 | "inside main_container -1 to 1px left", 38 | "width 70 % of main_container/width", 39 | ] 40 | }) 41 | .hasRuleSection("register_panel should be located on the top side of main_container and take 50 % of its height", { 42 | "register_panel": [ 43 | "inside main_container -1 to 1px top", 44 | "width 50 % of main_container/height", 45 | ] 46 | }) 47 | .hasObjectWithSpecGroups("user_panel", { 48 | "located on the left side of main_container with ~10px margin and takes 70 % of its width": [ 49 | "inside main_container ~10px left", 50 | "width 70 % of main_container/width", 51 | ], 52 | "located on the top side of main_container with 10 to 12px margin and takes 50 % of its height": [ 53 | "inside main_container 10 to 12px top", 54 | "width 50 % of main_container/height", 55 | ] 56 | }) 57 | .hasRuleSection("login_panel should be located on the left side of main_container with 10px margin and take 70 % of its width", { 58 | "login_panel": [ 59 | "inside main_container 10px left", 60 | "width 70 % of main_container/width", 61 | ] 62 | }) 63 | .hasRuleSection("register_panel should be located on the top side of main_container with < 123px margin and take 50 % of its height", { 64 | "register_panel": [ 65 | "inside main_container < 123px top", 66 | "width 50 % of main_container/height", 67 | ] 68 | }) 69 | }); 70 | -------------------------------------------------------------------------------- /rules-tests/rule.test.js: -------------------------------------------------------------------------------- 1 | load("init.js"); 2 | 3 | 4 | testSpec("squared.gspec", [], function (spec) { 5 | assertSpec(spec).hasRuleSection("icon-* should be squared", { 6 | "icon-1": ["width 100% of icon-1/height"], 7 | "icon-2": ["width 100% of icon-2/height"] 8 | }) 9 | .hasRuleSection("icon-* should be almost squared", { 10 | "icon-1": ["width 90 to 110% of icon-1/height"], 11 | "icon-2": ["width 90 to 110% of icon-2/height"] 12 | }) 13 | .hasObjectWithSpecGroups("icon-1", { 14 | "squared with ~ 50px size": [ 15 | "width ~ 50px", 16 | "height ~ 50px" 17 | ] 18 | }) 19 | .hasObjectWithSpecGroups("icon-2", { 20 | "squared": ["width 100% of icon-2/height"] 21 | }) 22 | .hasObjectWithSpecGroups("icon-3", { 23 | "almost squared": ["width 90 to 110% of icon-3/height"] 24 | }) 25 | .hasRuleSection("icon-* should be squared with 23 to 30 px size", { 26 | "icon-1": ["width 23 to 30 px", "height 23 to 30 px"], 27 | "icon-2": ["width 23 to 30 px", "height 23 to 30 px"] 28 | }); 29 | }); 30 | 31 | 32 | testSpec("ratio.gspec", [], function (spec) { 33 | assertSpec(spec).hasRuleSection("icon-* should have ~ 33% width/height ratio", { 34 | "icon-1": ["height ~ 33 % of icon-1/width"], 35 | "icon-2": ["height ~ 33 % of icon-2/width"] 36 | }) 37 | .hasObjectWithSpecGroups("icon-3", { 38 | "has 10% width/height ratio": ["height 10 % of icon-3/width"] 39 | }); 40 | }); 41 | 42 | 43 | testSpec("amount.gspec", [], function (spec) { 44 | assertSpec(spec) 45 | .hasRuleSection("amount of visible icon-* should be 2", { 46 | "global": ["count visible \"icon-*\" is 2"], 47 | }) 48 | .hasRuleSection("amount of absent logo-* should be < 2", { 49 | "global": ["count absent \"logo-*\" is < 2"], 50 | }); 51 | }); 52 | 53 | 54 | 55 | testSpec("tags.gspec", ["desktop"], function (spec) { 56 | assertSpec(spec) 57 | .hasRuleSection("login_button, cancel_button should be visible on desktop, tablet but absent on mobile", { 58 | "login_button": ["visible"], 59 | "cancel_button": ["visible"] 60 | }) 61 | .hasRuleSection("login_button should be absent on mobile but on desktop, tablet:", { 62 | "login_button": ["inside screen 0px top left"] 63 | }) 64 | }); 65 | testSpec("tags.gspec", ["tablet"], function (spec) { 66 | assertSpec(spec) 67 | .hasRuleSection("login_button, cancel_button should be visible on desktop, tablet but absent on mobile", { 68 | "login_button": ["visible"], 69 | "cancel_button": ["visible"] 70 | }) 71 | .hasRuleSection("login_button should be absent on mobile but on desktop, tablet:", { 72 | "login_button": ["inside screen 0px top left"] 73 | }) 74 | }); 75 | testSpec("tags.gspec", ["mobile"], function (spec) { 76 | assertSpec(spec) 77 | .hasRuleSection("login_button, cancel_button should be visible on desktop, tablet but absent on mobile", { 78 | "login_button": ["absent"], 79 | "cancel_button": ["absent"] 80 | }) 81 | .hasRuleSection("login_button should be absent on mobile but on desktop, tablet:", { 82 | "login_button": ["absent"] 83 | }) 84 | }); 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /rules-tests/init.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function testSpec(specPath, tags, callback) { 5 | test("Check spec " + specPath + " [" + tags.join(", ") + "]", function () { 6 | var pageSpec = parsePageSpec({ 7 | driver: null, 8 | spec: "specs/" + specPath, 9 | tags: tags 10 | }); 11 | 12 | callback.call(this, pageSpec); 13 | }); 14 | } 15 | 16 | 17 | function assertEquals(title, val1, val2) { 18 | if (val1 !== val2) { 19 | throw new Error("Does not equal: " + title + "\n - [" + val1 + "]\n - [" + val2 + "]\n"); 20 | } 21 | } 22 | 23 | function assertArrayContains(title, arr, item) { 24 | for (var i = 0; i < arr.length; i++) { 25 | if (arr[i] === item) { 26 | return; 27 | } 28 | } 29 | 30 | throw new Error("Does not contain: " + title + "\n Expected item: " + item + "\n Actual array:\n" + arrayToPrettyError(arr) 31 | ); 32 | } 33 | function arrayToPrettyError(arr) { 34 | return arr.map(function (arrItem) {return " - " + arrItem;}).join("\n") + "\n" 35 | } 36 | 37 | function assertArraysAreEqual(title, arr1, arr2) { 38 | if (arr1.length !== arr2.length) { 39 | throw new Error("Length doesn't match: " + title + "\n" 40 | + " array #1:\n" + arrayToPrettyError(arr1) 41 | + " array #2:\n" + arrayToPrettyError(arr2)); 42 | } else { 43 | for (var i = 0; i < arr1.length; i++) { 44 | assertArrayContains(title, arr2, arr1[i]); 45 | } 46 | } 47 | } 48 | 49 | function SpecAssert(spec) { 50 | this.spec = spec; 51 | } 52 | SpecAssert.prototype.doesNotHaveRuleSection = function (sectionName, sectionContents) { 53 | var section = this.findSection(sectionName); 54 | if (section !== null) { 55 | throw new Error("rule section should not be there but it is: " + sectionName); 56 | } 57 | 58 | return this; 59 | }; 60 | SpecAssert.prototype.hasRuleSection = function (sectionName, sectionContents) { 61 | var section = this.findSection(sectionName); 62 | if (section === null) { 63 | throw new Error("There is no section with name: " + sectionName 64 | + "\nThe only available sections are:\n" 65 | + this.extractAllSectionNames().map(function (name) { 66 | return " - [" + name + "]"; 67 | }).join("\n")); 68 | } 69 | 70 | var validatedObjects = 0; 71 | for (var objectName in sectionContents) { 72 | if (sectionContents.hasOwnProperty(objectName)) { 73 | validatedObjects += 1; 74 | var obj = this.findObjectInSection(section, objectName); 75 | if (obj === null) { 76 | throw new Error("There is no object " + objectName + " in section: " + sectionName); 77 | } 78 | 79 | var expectedSpecs = sectionContents[objectName]; 80 | var realSpecs = this.convertSpecsToStringArray(obj.getSpecs()); 81 | 82 | assertEquals("Amount of expectedSpecs and realSpecs in object " + objectName, expectedSpecs.length, realSpecs.length); 83 | for (var i = 0; i < expectedSpecs.length; i++) { 84 | assertArrayContains("realSpecs should contain: ", realSpecs, expectedSpecs[i]); 85 | } 86 | } 87 | } 88 | 89 | assertEquals("Amount of validated objects should match with actual object is spec", validatedObjects, section.getObjects().size()); 90 | return this; 91 | }; 92 | SpecAssert.prototype.hasObjectWithSpecGroups = function (objectName, expectedSpecsGroups) { 93 | var section = this.spec.getSections().get(0); 94 | var objectSpecs = this.findObjectInSection(section, objectName); 95 | if (objectSpecs === null) { 96 | throw new Error("There is no object with name: " + objectName + "\n The only available objects are:\n" 97 | + arrayToPrettyError(this.extractAllObjectNames(section)) 98 | ); 99 | } 100 | for (var groupName in expectedSpecsGroups) { 101 | if (expectedSpecsGroups.hasOwnProperty(groupName)) { 102 | var specGroup = this.findSpecGroupInObject(objectSpecs, groupName); 103 | if (specGroup === null) { 104 | throw new Error("Object " + objectName + " does not have spec group: " + groupName + "\n The only available groups are:\n" 105 | + arrayToPrettyError(this.extractAllSpecGroupNames(objectSpecs)) 106 | ); 107 | } 108 | var actualSpecs = this.convertSpecsToStringArray(specGroup.getSpecs()); 109 | assertArraysAreEqual("Actual specs should be equal to expected specs", actualSpecs, expectedSpecsGroups[groupName]); 110 | } 111 | } 112 | return this; 113 | }; 114 | SpecAssert.prototype.extractAllSpecGroupNames = function (objectSpecs) { 115 | var specGroups = []; 116 | if (objectSpecs.getSpecGroups() !== null) { 117 | var iterator = objectSpecs.getSpecGroups().iterator(); 118 | while (iterator.hasNext()) { 119 | var group = iterator.next(); 120 | specGroups.push("" + group.getName()); 121 | } 122 | } 123 | return specGroups; 124 | }; 125 | SpecAssert.prototype.findSpecGroupInObject = function (objectSpecs, groupName) { 126 | if (objectSpecs.getSpecGroups() !== null) { 127 | var iterator = objectSpecs.getSpecGroups().iterator(); 128 | while (iterator.hasNext()) { 129 | var group = iterator.next(); 130 | if (groupName === "" + group.getName()) { 131 | return group; 132 | } 133 | } 134 | } 135 | return null; 136 | }; 137 | SpecAssert.prototype.findObjectInSection = function (section, objectName) { 138 | var iterator = section.getObjects().iterator(); 139 | while (iterator.hasNext()) { 140 | var objectSpecs = iterator.next(); 141 | if (objectName === "" + objectSpecs.getObjectName()) { 142 | return objectSpecs; 143 | } 144 | } 145 | return null; 146 | }; 147 | SpecAssert.prototype.extractAllObjectNames = function (section) { 148 | var objectNames = []; 149 | var iterator = section.getObjects().iterator(); 150 | while (iterator.hasNext()) { 151 | var objectSpecs = iterator.next(); 152 | objectNames.push("" + objectSpecs.getObjectName()); 153 | } 154 | return objectNames; 155 | }; 156 | SpecAssert.prototype.convertSpecsToStringArray = function (specsList) { 157 | var specs = []; 158 | var iterator = specsList.iterator(); 159 | while (iterator.hasNext()) { 160 | var spec = iterator.next(); 161 | specs.push("" + spec.getOriginalText()); 162 | } 163 | return specs; 164 | }; 165 | SpecAssert.prototype.findSection = function (sectionName) { 166 | var iterator = this.spec.getSections().get(0).getSections().iterator(); 167 | while(iterator.hasNext()) { 168 | var section = iterator.next(); 169 | 170 | if (sectionName === "" + section.getName()) { 171 | return section; 172 | } 173 | } 174 | 175 | return null; 176 | }; 177 | SpecAssert.prototype.extractAllSectionNames = function () { 178 | var sectionNames = []; 179 | var iterator = this.spec.getSections().get(0).getSections().iterator(); 180 | while(iterator.hasNext()) { 181 | var section = iterator.next(); 182 | sectionNames.push("" + section.getName()); 183 | } 184 | 185 | return sectionNames; 186 | }; 187 | 188 | function assertSpec(spec) { 189 | return new SpecAssert(spec); 190 | } 191 | -------------------------------------------------------------------------------- /galen-extras/galen-extras-rules.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | * Copyright 2016 Ivan Shubin http://galenframework.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ***************************************************************************/ 16 | 17 | if (this.GEXTRAS_NO_MARGIN === undefined || this.GEXTRAS_NO_MARGIN === null) { 18 | this.GEXTRAS_NO_MARGIN = "-1 to 1px"; 19 | } 20 | 21 | function _ruleRenderedInTable(rule, itemPattern, columns, verticalMargin, horizontalMargin) { 22 | var allItems = findAll(itemPattern); 23 | 24 | var currentColumn = 0; 25 | 26 | for (var i = 0; i < allItems.length - 1; i += 1) { 27 | if (currentColumn < columns - 1) { 28 | rule.addObjectSpecs(allItems[i].name, [ 29 | "left-of " + allItems[i + 1].name + " " + horizontalMargin, 30 | "aligned horizontally all " + allItems[i + 1].name 31 | ]); 32 | } 33 | 34 | var j = i + columns; 35 | 36 | if (j < allItems.length) { 37 | rule.addObjectSpecs(allItems[i].name, [ 38 | "above " + allItems[j].name + " " + verticalMargin, 39 | "aligned vertically all " + allItems[j].name 40 | ]); 41 | } 42 | 43 | currentColumn += 1; 44 | if (currentColumn === columns) { 45 | currentColumn = 0; 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * This is a high-level spec for checking that elements are displayed in table layout 52 | * e.g. 53 | * 54 | * | menuItem-* are rendered in 2 column table layout 55 | */ 56 | rule("%{itemPattern} are rendered in %{columns: [0-9]+} column table layout", function (objectName, parameters) { 57 | _ruleRenderedInTable(this, parameters.itemPattern, parseInt(columns), GEXTRAS_NO_MARGIN, GEXTRAS_NO_MARGIN); 58 | }); 59 | 60 | 61 | /** 62 | * This is a high-level spec for checking that elements are displayed in table layout 63 | * e.g. 64 | * 65 | * | menuItem-* are rendered in 2 column table layout, with 0 to 4px margin 66 | */ 67 | rule("%{itemPattern} are rendered in %{columns: [0-9]+} column table layout, with %{margin} margin", function (objectName, parameters) { 68 | _ruleRenderedInTable(this, parameters.itemPattern, parseInt(columns), parameters.margin, parameters.margin); 69 | }); 70 | 71 | 72 | /** 73 | * This is a high-level spec for checking that elements are displayed in table layout 74 | * with different margins for vertical and horizontal sides 75 | * e.g. 76 | * 77 | * | menuItem-* are rendered in 2 column table layout, with 0 to 4px vertical and 1px horizontal margins 78 | */ 79 | rule("%{itemPattern} are rendered in %{columns: [0-9]+} column table layout, with %{verticalMargin} vertical and %{horizontalMargin} horizontal margin", function (objectName, parameters) { 80 | _ruleRenderedInTable(this, parameters.itemPattern, parseInt(columns), parameters.verticalMargin, parameters.horizontalMargin); 81 | }); 82 | 83 | 84 | function _applyRuleBodyForAllElements(rule, parameters, appliesConditionCallback) { 85 | var allElements = findAll(parameters.objectPattern); 86 | 87 | if (allElements.length > 0) { 88 | for (var i = 0; i < allElements.length; i += 1) { 89 | if (!appliesConditionCallback(allElements[i])) { 90 | return; 91 | } 92 | } 93 | rule.doRuleBody(); 94 | } 95 | } 96 | 97 | function _applyRuleBodyForSingleElement(rule, parameters, appliesConditionCallback) { 98 | var allElements = findAll(parameters.objectPattern); 99 | 100 | if (allElements.length > 0) { 101 | for (var i = 0; i < allElements.length; i += 1) { 102 | if (appliesConditionCallback(allElements[i])) { 103 | rule.doRuleBody(); 104 | return; 105 | } 106 | } 107 | } 108 | } 109 | 110 | rule("if all %{objectPattern} are visible", function (objectName, parameters) { 111 | _applyRuleBodyForAllElements(this, parameters, function (element) { 112 | return element.isVisible(); 113 | }); 114 | }); 115 | 116 | 117 | rule("if none of %{objectPattern} are visible", function (objectName, parameters) { 118 | _applyRuleBodyForAllElements(this, parameters, function (element) { 119 | return ! element.isVisible(); 120 | }); 121 | }); 122 | 123 | rule("if any of %{objectPattern} is visible", function (objectName, parameters) { 124 | _applyRuleBodyForSingleElement(this, parameters, function (element) { 125 | return element.isVisible(); 126 | }); 127 | }); 128 | 129 | 130 | rule("%{objectPattern} sides are inside %{containerObject} with %{margin} margin from %{sideAName} and %{sideBName}", function (objectName, parameters) { 131 | var items = findAll(parameters.objectPattern); 132 | if (items.length > 0) { 133 | this.addObjectSpecs(items[0].name, [ "inside " + parameters.containerObject + " " + parameters.margin + " " + parameters.sideAName ]); 134 | 135 | for (var i = 1; i < items.length - 1; i++) { 136 | this.addObjectSpecs(items[i].name, [ "inside " + parameters.containerObject ]); 137 | } 138 | 139 | this.addObjectSpecs(items[items.length - 1].name, [ "inside " + parameters.containerObject + " " + parameters.margin + " " + parameters.sideBName ]); 140 | } else { 141 | throw new Error("Couldn't find any items matching " + parameters.objectPattern); 142 | } 143 | }); 144 | 145 | 146 | rule("%{objectPattern} sides are vertically inside %{containerObject}", function (objectName, parameters) { 147 | var items = findAll(parameters.objectPattern); 148 | if (items.length > 0) { 149 | this.addObjectSpecs(items[0].name, [ "inside " + parameters.containerObject + " -1 to 1 px top" ]); 150 | 151 | for (var i = 1; i < items.length - 1; i++) { 152 | this.addObjectSpecs(items[i].name, [ "inside " + parameters.containerObject ]); 153 | } 154 | 155 | this.addObjectSpecs(items[items.length - 1].name, [ "inside " + parameters.containerObject + " -1 to 1 px bottom" ]); 156 | } else { 157 | throw new Error("Couldn't find any items matching " + parameters.objectPattern); 158 | } 159 | }); 160 | 161 | rule("%{objectPattern} sides are vertically inside %{containerObject} with %{margin} margin", function (objectName, parameters) { 162 | var items = findAll(parameters.objectPattern); 163 | if (items.length > 0) { 164 | this.addObjectSpecs(items[0].name, [ "inside " + parameters.containerObject + " " + parameters.margin + " top" ]); 165 | 166 | for (var i = 1; i < items.length - 1; i++) { 167 | this.addObjectSpecs(items[i].name, [ "inside " + parameters.containerObject ]); 168 | } 169 | 170 | this.addObjectSpecs(items[items.length - 1].name, [ "inside " + parameters.containerObject + " " + parameters.margin + " bottom" ]); 171 | } else { 172 | throw new Error("Couldn't find any items matching " + parameters.objectPattern); 173 | } 174 | }); 175 | 176 | 177 | rule("%{objectPattern} sides are horizontally inside %{containerObject}", function (objectName, parameters) { 178 | var items = findAll(parameters.objectPattern); 179 | if (items.length > 0) { 180 | this.addObjectSpecs(items[0].name, [ "inside " + parameters.containerObject + " -1 to 1 px left" ]); 181 | 182 | for (var i = 1; i < items.length - 1; i++) { 183 | this.addObjectSpecs(items[i].name, [ "inside " + parameters.containerObject ]); 184 | } 185 | 186 | this.addObjectSpecs(items[items.length - 1].name, [ "inside " + parameters.containerObject + " -1 to 1 px right" ]); 187 | } else { 188 | throw new Error("Couldn't find any items matching " + parameters.objectPattern); 189 | } 190 | }); 191 | 192 | 193 | rule("%{objectPattern} sides are horizontally inside %{containerObject} with %{margin} margin", function (objectName, parameters) { 194 | var items = findAll(parameters.objectPattern); 195 | if (items.length > 0) { 196 | this.addObjectSpecs(items[0].name, [ "inside " + parameters.containerObject + " " + parameters.margin + " left" ]); 197 | 198 | for (var i = 1; i < items.length - 1; i++) { 199 | this.addObjectSpecs(items[i].name, [ "inside " + parameters.containerObject ]); 200 | } 201 | 202 | this.addObjectSpecs(items[items.length - 1].name, [ "inside " + parameters.containerObject + " " + parameters.margin + " right" ]); 203 | } else { 204 | throw new Error("Couldn't find any items matching " + parameters.objectPattern); 205 | } 206 | }); 207 | 208 | 209 | 210 | rule("text of all %{objectPattern} should be [%{textJson}]", function (objectName, parameters) { 211 | var items = findAll(parameters.objectPattern); 212 | if (items.length > 0) { 213 | 214 | var expectedTexts = JSON.parse("[" + parameters.textJson + "]"); 215 | 216 | for (var i = 0; i < items.length; i++) { 217 | this.addObjectSpecs(items[i].name, [ "text is " + JSON.stringify(expectedTexts[i])]); 218 | } 219 | 220 | } else { 221 | throw new Error("Couldn't find any items matching " + parameters.objectPattern); 222 | } 223 | }); 224 | 225 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Works only in galen version 2.2.0 or higher 2 | 3 | Galen Extras 4 | ============ 5 | 6 | Galen Extras is a library for extending the [Galen Specs Language](http://galenframework.com/docs/reference-galen-spec-language-guide/) with most common complex expressions, which allows you to have minimal code and maximum test coverage. 7 | 8 | Here is a small example which describes all layouts of menu items on responsive website: 9 | 10 | ``` 11 | @import galen-extras/galen-extras-rules.gspec 12 | 13 | @objects 14 | menu #menu .middle-wrapper 15 | item-* ul li 16 | 17 | @groups 18 | (menu_item, menu_items) menu.item-* 19 | 20 | = Menu items should adapt layout = 21 | @on * 22 | | amount of visible &menu_items should be 4 23 | | every &menu_item has height ~ 64px 24 | | first &menu_item is inside menu 0px top left 25 | 26 | @on desktop, tablet 27 | | &menu_items are aligned horizontally next to each other with 0 to 5px margin 28 | 29 | @on mobile 30 | | &menu_items are rendered in 2 column table layout, with 0 to 4px vertical and 1px horizontal margin 31 | ``` 32 | 33 | Install 34 | -------------- 35 | 36 | Just download the folder `galen-extras` to your specs folder in your test project and import it: 37 | 38 | ``` 39 | @import galen-extras/galen-extras-rules.gspec 40 | 41 | # ... 42 | ``` 43 | 44 | You only need to import that one file. The `galen-extras-rules.js` is imported in it already. 45 | 46 | Inspiration 47 | --------------- 48 | 49 | If you want to see all these custom rules in action just look at `examples.gspec` file. It shows how different rules could be used effectively for testing a page on different breakpoints. 50 | Also you can play with this project and run galen tests for it: 51 | 52 | ``` 53 | galen test galen.test --htmlreport reports 54 | ``` 55 | 56 | Documentation 57 | --------------- 58 | 59 | In this section you will find syntax explanation for all rules and examples. 60 | 61 | 62 | ### Squared elements 63 | 64 | Allows to check that element has equal width and height. 65 | 66 | ![](http://galenframework.github.io/galen-extras/images/sketch-squared.png) 67 | 68 | ###### For multiple elements 69 | 70 | Scope: Section 71 | 72 | Syntax: `%{objectPattern} should be squared` 73 | 74 | ``` 75 | | header_icon, menu_button should be squared 76 | ``` 77 | 78 | ###### For single elements 79 | 80 | Scope: Object 81 | 82 | Syntax: `squared` 83 | 84 | ``` 85 | header_icon: 86 | | squared 87 | ``` 88 | 89 | 90 | ### Almost squared elements 91 | 92 | Same as `squared` rule but it allows an error rate of 10% 93 | 94 | ![](http://galenframework.github.io/galen-extras/images/sketch-squared-almost.png) 95 | 96 | ###### For multiple elements 97 | 98 | Scope: Section 99 | 100 | Syntax: `%{objectPattern} should be almost squared` 101 | 102 | ``` 103 | | header_icon, menu_button should be almost squared 104 | ``` 105 | 106 | ###### For single elements 107 | 108 | Scope: Object 109 | 110 | Syntax: `almost squared` 111 | 112 | ``` 113 | header_icon: 114 | | almost squared 115 | ``` 116 | 117 | 118 | ### Width/Height Ratio 119 | 120 | You can check the exact ratio of width/height in percentage 121 | 122 | ![](http://galenframework.github.io/galen-extras/images/sketch-width-ratio.png) 123 | 124 | ###### Multiple elements 125 | 126 | Scope: Section 127 | 128 | Syntax: `%{itemPattern} should have %{ratio}% width/height ratio` 129 | 130 | ``` 131 | | login_button, cancel_button should have 140 % width/height ratio 132 | ``` 133 | 134 | ###### Single elements 135 | 136 | Scope: Object 137 | 138 | Syntax: `%{ratio}% width/height ratio` 139 | 140 | ``` 141 | login_button: 142 | | 140 % width/height ratio 143 | ``` 144 | 145 | 146 | ### Testing amount of objects 147 | 148 | In Galen you can check the amount of objects using just the 2 lines of code: 149 | 150 | ``` 151 | global: 152 | count any menu.item-* is 3 153 | ``` 154 | 155 | But it is not very human readable. By using the rule below you can express this validation in simple sentence 156 | 157 | Scope: Section 158 | 159 | Syntax: `amount of %{visibilityType} %{objectPattern} should be %{amount}` 160 | 161 | where `visibilityType` can take `any`, `visible` or `absent` values 162 | 163 | e.g. amount of any elements: 164 | ``` 165 | | amount of any menu.item-* should be 5 166 | ``` 167 | 168 | e.g. testing only visible elements 169 | ``` 170 | | amount of visible menu.item-* should be 5 171 | ``` 172 | 173 | Giving a range of expected values 174 | ``` 175 | | amount of visible menu.item-* should be 4 to 7 176 | ``` 177 | 178 | or 179 | 180 | ``` 181 | | amount of visible menu.item-* should be > 4 182 | ``` 183 | 184 | ### Alignment of multiple elements with zero margin 185 | 186 | A very common situation when you have elements on the page aligned either vertically or horizontally. 187 | 188 | ###### Horizontal 189 | ![](http://galenframework.github.io/galen-extras/images/sketch-horizontal-align.png) 190 | 191 | Scope: Section 192 | 193 | Syntax: `%{objectPattern} are aligned horizontally next to each other` 194 | 195 | ``` 196 | | menu.item-* are aligned horizontally next to each other 197 | ``` 198 | 199 | ###### Vertical 200 | ![](http://galenframework.github.io/galen-extras/images/sketch-vertical-align.png) 201 | 202 | Scope: Section 203 | 204 | Syntax: `%{objectPattern} are aligned vertically above each other` 205 | 206 | ``` 207 | | menu.item-* are aligned vertically above each other 208 | ``` 209 | 210 | ### Alignment of multiple elements with equal distance 211 | 212 | A very common situation when you have elements on the page aligned either vertically or horizontally. At the same time their margin might change depending on page width. 213 | The following rules will help you when you can't know the exact margin and you just want to check that it is consistent. 214 | 215 | ###### Horizontal 216 | ![](http://galenframework.github.io/galen-extras/images/sketch-horizontal-align.png) 217 | 218 | Scope: Section 219 | 220 | Syntax: `%{objectPattern} are aligned horizontally next to each other with equal distance` 221 | 222 | ``` 223 | | menu.item-* are aligned horizontally next to each other with equal distance 224 | ``` 225 | 226 | ###### Vertical 227 | ![](http://galenframework.github.io/galen-extras/images/sketch-vertical-align.png) 228 | 229 | Scope: Section 230 | 231 | Syntax: `%{objectPattern} are aligned vertically above each other with equal distance` 232 | 233 | ``` 234 | | menu.item-* are aligned vertically above each other with equal distance 235 | ``` 236 | 237 | 238 | ### Alignment of multiple elements with specific margin 239 | 240 | Similar to the above statement but in this case you can declare a specific margin between elements 241 | 242 | ###### Horizontal 243 | 244 | Scope: Section 245 | 246 | Syntax: `%{objectPattern} are aligned horizontally next to each other with %{margin} margin` 247 | 248 | ``` 249 | | menu.item-* are aligned horizontally next to each other with 5px margin 250 | | box-* are aligned horizontally next to each other with 25 to 30px margin 251 | ``` 252 | 253 | ###### Vertical 254 | 255 | Scope: Section 256 | 257 | Syntax: `%{objectPattern} are aligned vertically above each other with %{margin} margin` 258 | 259 | ``` 260 | | menu.item-* are aligned vertically above each other with 5px margin 261 | | box-* are aligned vertically above each other with 25 to 30px margin 262 | ``` 263 | 264 | ### Table layout 265 | 266 | Allows to check that a set of elements is displayed in simple table. You can define the amount of columns for this table layout. 267 | ![](http://galenframework.github.io/galen-extras/images/sketch-table-layout.png) 268 | 269 | ###### Table layout with zero margin between cols and rows 270 | 271 | Scope: Section 272 | 273 | Syntax: `%{itemPattern} are rendered in %{columns} column table layout` 274 | 275 | ``` 276 | | menu.item-* are rendered in 2 column table layout 277 | ``` 278 | 279 | ###### Table layout with equal cols and rows margin 280 | 281 | Scope: Section 282 | 283 | Syntax: `%{itemPattern} are rendered in %{columns} column table layout, with %{margin} margin` 284 | 285 | ``` 286 | | menu.item-* are rendered in 2 column table layout, with 0 to 5px margin 287 | ``` 288 | 289 | ###### Table layout with different cols and rows margin 290 | 291 | Scope: Section 292 | 293 | Syntax: `%{itemPattern} are rendered in %{columns} column table layout, with %{verticalMargin} vertical and %{horizontalMargin} horizontal margins` 294 | 295 | ``` 296 | | menu.item-* are rendered in 2 column table layout, with 0px vertical and 5px horizontal margins 297 | ``` 298 | 299 | 300 | ### Location of sides of multiple elements inside a container 301 | 302 | The following statement checks that a set of elements is located inside specified container and that the first and last element have a specific margin from sides between the container sides. 303 | 304 | 305 | ##### Horizontal zero margin 306 | 307 | Syntax: `%{objectPattern} sides are horizontally inside %{containerObject}` 308 | 309 | ![](http://galenframework.github.io/galen-extras/images/sketch-sides-horizontal.png) 310 | 311 | ``` 312 | | menu.item-* sides are horizontally inside menu 313 | ``` 314 | 315 | ##### Vertical zero margin 316 | 317 | Syntax: `%{objectPattern} sides are vertically inside %{containerObject}` 318 | 319 | ![](http://galenframework.github.io/galen-extras/images/sketch-sides-vertical.png) 320 | 321 | ``` 322 | | menu.item-* sides are vertically inside menu 323 | ``` 324 | 325 | ##### Custom margin for both sides 326 | 327 | Syntax: `%{objectPattern} sides are inside %{containerObject} with %{margin} margin from %{sideAName} and %{sideBName}` 328 | 329 | where `sideAName` and `sideBName` can take the following values: `left`, `right`, `top`, `bottom` 330 | 331 | ####### Left and Right 332 | 333 | ![](http://galenframework.github.io/galen-extras/images/sketch-sides-horizontal.png) 334 | 335 | ``` 336 | | menu.item-* sides are inside menu with > 0px margin from left and right 337 | ``` 338 | 339 | ####### Top and Bottom 340 | 341 | ![](http://galenframework.github.io/galen-extras/images/sketch-sides-vertical.png) 342 | 343 | ``` 344 | | box-* sides are inside box_container with > 0px margin from top and bottom 345 | ``` 346 | 347 | ### Common Conditions 348 | 349 | The following common conditions allow you to insert your own code block and invoke only if condition succeeds. 350 | 351 | ###### If all elements are visible 352 | 353 | Scope: Any 354 | 355 | Syntax: `if all %{objectPattern} are visible` 356 | 357 | ``` 358 | | if all menu.item-* are visible 359 | menu.item-*: 360 | height 30px 361 | ``` 362 | 363 | ###### If any of elements is visible: 364 | 365 | Scope: Any 366 | 367 | Syntax: `if any of %{objectPattern} is visible` 368 | 369 | ``` 370 | | if any of menu.item-* is visible 371 | menu: 372 | height 50px 373 | ``` 374 | 375 | ###### If none of elements are visible: 376 | 377 | Scope: Any 378 | 379 | Syntax: `if none of %{objectPattern} are visible` 380 | 381 | ``` 382 | | if none of menu.item-* are visible 383 | main: 384 | below header 0px 385 | ``` 386 | 387 | 388 | ### Appearance of elements per breakpoints 389 | 390 | Quite often you have elements of the website that are hidden on small devices and are only shown on large layouts. 391 | You can use the following statements to express that behaviour 392 | 393 | Scope: Section 394 | 395 | Syntax: `%{objectPatterns} should be visible on %{tagsVisible} but absent on %{tagsAbsent}` 396 | 397 | ``` 398 | | twitter_button should be visible on desktop, laptop but absent on tablet, mobile 399 | ``` 400 | 401 | 402 | ### Component validations for multiple elements 403 | 404 | Allows to specify a component check for a set of elements 405 | 406 | Scope: Section 407 | 408 | Syntax: `test all %{objectPattern} with %{componentPath}` 409 | 410 | ``` 411 | | test all box-* with components/box.gspec 412 | ``` 413 | 414 | ### Applying generic checks for multiple elements in a single line 415 | 416 | This is usefull when you have repetitive specs for different elements and you don't want to use forEach loops. Basically if you have forEach loop for one iterated object and one spec for it, you can do in a single statement using this rule. 417 | 418 | 419 | Scope: Section 420 | 421 | Syntax 1: `every %{objectPattern} is %{spec}` 422 | 423 | Syntax 2: `every %{objectPattern} has %{spec}` 424 | 425 | ``` 426 | | every menu.item-* is inside menu 0px top bottom 427 | | every menu.item-* has width 100px 428 | ``` 429 | 430 | ### Applying two specs for multiple elements in a single line 431 | 432 | Similar to the above statement, this one allows to have two specs separated by word `and` 433 | 434 | Scope: Section 435 | 436 | Syntax 1: `every %{objectPattern} is %{spec1} and has %{spec2}` 437 | 438 | Syntax 2: `every %{objectPattern} has %{spec1} and is %{spec2}` 439 | 440 | Syntax 3: `every %{objectPattern} has %{spec1} and has %{spec2}` 441 | 442 | Syntax 4: `every %{objectPattern} is %{spec1} and is %{spec2}` 443 | 444 | ``` 445 | | every menu.item-* is inside menu 0px top bottom and has width 100px 446 | ``` 447 | 448 | ### Checking only first element from given expression 449 | 450 | Often when working with set of elements you need to check only the first one or the last 451 | 452 | Syntax 1: `first %{objectPattern} is %{spec}` 453 | 454 | Syntax 2: `first %{objectPattern} has %{spec}` 455 | 456 | ``` 457 | | first menu.item-* is inside menu 0px top bottom 458 | | first menu.item-* has width 100px 459 | ``` 460 | 461 | ### Checking only last element from given expression 462 | 463 | Same as above, but this time it checks the last element 464 | 465 | Syntax 1: `last %{objectPattern} is %{spec}` 466 | 467 | Syntax 2: `last %{objectPattern} has %{spec}` 468 | 469 | ``` 470 | | last menu.item-* is inside menu 0px top bottom 471 | | last menu.item-* has width 100px 472 | ``` 473 | 474 | 475 | ### Applying multiple checks for only first element from given expression 476 | 477 | Allows to apply multiple specs to only first element 478 | 479 | Syntax: `first %{objectPattern}:` 480 | 481 | ``` 482 | | first menu.item-* : 483 | below header 10px 484 | inside main_container 0px top left 485 | ``` 486 | 487 | 488 | ### Applying multiple checks for only last element from given expression 489 | 490 | Allows to apply multiple specs to only last element 491 | 492 | Syntax: `last %{objectPattern}:` 493 | 494 | ``` 495 | | last menu.item-* : 496 | below header 10px 497 | inside main_container 0px top left 498 | ``` 499 | 500 | 501 | License 502 | -------------- 503 | Galen Extras lib is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 504 | 505 | -------------------------------------------------------------------------------- /galen-extras/galen-extras-rules.gspec: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2016 Ivan Shubin http://galenframework.com 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################## 16 | 17 | @if ${this.GEXTRAS_NO_MARGIN === undefined || this.GEXTRAS_NO_MARGIN === null} 18 | @set GEXTRAS_NO_MARGIN -1 to 1px 19 | 20 | @if ${this.GEXTRAS_ === undefined || this.GEXTRAS_ALIGN_ERROR_RATE === null} 21 | @set GEXTRAS_ALIGN_ERROR_RATE 1px 22 | 23 | @script galen-extras-rules.js 24 | 25 | 26 | # Check that all matching elements are squared 27 | # e.g. 28 | # 29 | # | header-icon, menu-button should be squared 30 | # 31 | @rule %{objectPattern} should be squared 32 | @forEach [${objectPattern}] as item 33 | ${item}: 34 | width 100% of ${item}/height 35 | 36 | 37 | 38 | # Check that all matching elements are almost squared 39 | # allowing 10% difference between width and height 40 | # e.g. 41 | # 42 | # | header-icon, menu-button should be almost squared 43 | # 44 | @rule %{objectPattern} should be almost squared 45 | @forEach [${objectPattern}] as item 46 | ${item}: 47 | width 90 to 110% of ${item}/height 48 | 49 | 50 | 51 | # Check that some element is squared and has a specific size 52 | # e.g. 53 | # 54 | # icon: 55 | # | squared with ~ 100px size 56 | # 57 | @rule squared with %{size} size 58 | width ${size} 59 | height ${size} 60 | 61 | 62 | # Check that multiple elements are squared and that they have a specific size 63 | # e.g. 64 | # 65 | # | menu.item-* should be squared with ~ 100px size 66 | # 67 | @rule %{objectPattern} should be squared with %{size} size 68 | ${objectPattern}: 69 | width ${size} 70 | height ${size} 71 | 72 | 73 | # Check that element is strictly squared 74 | # e.g. 75 | # 76 | # header.icon: 77 | # | squared 78 | # 79 | @rule squared 80 | width 100% of ${objectName}/height 81 | 82 | 83 | 84 | # Check that element is almost squared 85 | # allowing 10% difference between its width and height 86 | # e.g. 87 | # 88 | # header.icon: 89 | # | almost squared 90 | # 91 | @rule almost squared 92 | width 90 to 110% of ${objectName}/height 93 | 94 | 95 | 96 | # Checks width/height ratio in percentage of all specified objects 97 | # e.g. 98 | # 99 | # | login_button, cancel_button should have 130% width/height ratio 100 | # 101 | @rule %{itemPattern} should have %{ratio}% width/height ratio 102 | @forEach [${itemPattern}] as item 103 | ${item}: 104 | height ${ratio} % of ${item}/width 105 | 106 | 107 | 108 | # Checks width/height ratio in percentage 109 | # e.g. 110 | # 111 | # login_button: 112 | # |has 130% width/height ratio 113 | # 114 | @rule has %{ratio}% width/height ratio 115 | height ${ratio} % of ${objectName}/width 116 | 117 | 118 | 119 | # Checking amount of objects 120 | # e.g. 121 | # 122 | # | amount of any menu.item should be > 4 123 | # 124 | # or 125 | # 126 | # | amount of visible menu.item-* should be 5 to 6 127 | # 128 | @rule amount of %{visibilityType: any|visible|absent} %{objectPattern} should be %{amount} 129 | global: 130 | count ${visibilityType} "${objectPattern}" is ${amount} 131 | 132 | 133 | 134 | # Check elements horizontal alignment with zero distance 135 | # e.g. 136 | # 137 | # | home_box_* are aligned horizontally next to each other 138 | # 139 | @rule %{objectPattern} are aligned horizontally next to each other 140 | @forEach [${objectPattern}] as item, next as nextItem 141 | ${item}: 142 | aligned horizontally all ${nextItem} ${GEXTRAS_ALIGN_ERROR_RATE} 143 | left-of ${nextItem} ${GEXTRAS_NO_MARGIN} 144 | 145 | 146 | 147 | # Check elements vertical alignment with zero distance 148 | # e.g. 149 | # 150 | # | home_box_* are aligned vertically above each other 151 | # 152 | @rule %{objectPattern} are aligned vertically above each other 153 | @forEach [${objectPattern}] as item, next as nextItem 154 | ${item}: 155 | aligned vertically all ${nextItem} ${GEXTRAS_ALIGN_ERROR_RATE} 156 | above ${nextItem} ${GEXTRAS_NO_MARGIN} 157 | 158 | 159 | # Check elements horizontal alignment and equal distance between each other 160 | # e.g. 161 | # 162 | # | home_box_* are aligned horizontally next to each other with equal distance 163 | # 164 | @rule %{objectPattern} are aligned horizontally next to each other with equal distance 165 | @if ${count(objectPattern) > 1} 166 | @set _distance_ ${var all = findAll(objectPattern); Math.abs(all[1].left() - all[0].right())} 167 | @set _distance_a ${parseInt(_distance_) - 1} 168 | @set _distance_b ${parseInt(_distance_) + 1} 169 | @forEach [${objectPattern}] as item, next as nextItem 170 | ${item}: 171 | aligned horizontally all ${nextItem} ${GEXTRAS_ALIGN_ERROR_RATE} 172 | left-of ${nextItem} ${_distance_a} to ${_distance_b} px 173 | 174 | 175 | 176 | # Check elements vertical alignment and equal distance between each other 177 | # e.g. 178 | # 179 | # | home_box_* are aligned vertically above each other with equal distance 180 | # 181 | @rule %{objectPattern} are aligned vertically above each other with equal distance 182 | @if ${count(objectPattern) > 1} 183 | @set _distance_ ${var all = findAll(objectPattern); Math.abs(all[1].top() - all[0].bottom())} 184 | @set _distance_a ${parseInt(_distance_) - 1} 185 | @set _distance_b ${parseInt(_distance_) + 1} 186 | @forEach [${objectPattern}] as item, next as nextItem 187 | ${item}: 188 | aligned vertically all ${nextItem} ${GEXTRAS_ALIGN_ERROR_RATE} 189 | above ${nextItem} ${_distance_a} to ${_distance_b} px 190 | 191 | 192 | 193 | # Check elements horizontal alignment and specific margin between each other 194 | # e.g. 195 | # 196 | # | home_box_* are aligned horizontally next to each other with 10 to 30px margin 197 | # 198 | @rule %{objectPattern} are aligned horizontally next to each other with %{margin} margin 199 | @forEach [${objectPattern}] as item, next as nextItem 200 | ${item}: 201 | aligned horizontally all ${nextItem} ${GEXTRAS_ALIGN_ERROR_RATE} 202 | left-of ${nextItem} ${margin} 203 | 204 | 205 | 206 | # Check elements vertical alignment and specific margin between each other 207 | # e.g. 208 | # 209 | # | home_box_* are aligned vertically above each other with 10 to 20 px margin 210 | # 211 | @rule %{objectPattern} are aligned vertically above each other with %{margin} margin 212 | @forEach [${objectPattern}] as item, next as nextItem 213 | ${item}: 214 | aligned vertically all ${nextItem} ${GEXTRAS_ALIGN_ERROR_RATE} 215 | above ${nextItem} ${margin} 216 | 217 | 218 | # Check that elements are places vertically above each other 219 | # without checking their alignment by sides 220 | # e.g. 221 | # 222 | # | menu.item-* are placed above each other with 10px margin 223 | # 224 | @rule %{itemPattern} are placed above each other with %{margin} margin 225 | @forEach [${itemPattern}] as item, next as nextItem 226 | ${item}: 227 | above ${nextItem} ${margin} 228 | 229 | 230 | 231 | # Check that elements are places horizontally above each other 232 | # without checking their alignment by sides 233 | # e.g. 234 | # 235 | # | menu.item-* are placed next to each other with 10px margin 236 | # 237 | @rule %{itemPattern} are placed next to each other with %{margin} margin 238 | @forEach [${itemPattern}] as item, next as nextItem 239 | ${item}: 240 | left-of ${nextItem} ${margin} 241 | 242 | 243 | 244 | 245 | # Check that elements appear and hide on different tags 246 | # e.g. 247 | # 248 | # | login_button, menu.item-* should be visible on desktop, tablet but absent on mobile 249 | # 250 | @rule %{objectPatterns} should be visible on %{tagsVisible} but absent on %{tagsAbsent} 251 | ${objectPatterns}: 252 | @on ${tagsVisible} 253 | visible 254 | @on ${tagsAbsent} 255 | absent 256 | 257 | 258 | 259 | # Validate all matching objects using specified component spec 260 | # e.g. 261 | # 262 | # | test all box-* with components/box.gspec 263 | # 264 | @rule test all %{objectPattern} with %{componentPath} 265 | @forEach [${objectPattern}] as item 266 | ${item}: 267 | component ${componentPath} 268 | 269 | 270 | # Apply spec to all elements in a single line 271 | # e.g. 272 | # 273 | # | every menu.item-* is inside menu 0px top bottom 274 | # 275 | # or 276 | # 277 | # | every menu.item-* has width 100px 278 | # 279 | @rule %{subj: every|a|an} %{objectPattern} %{verb: is|has} %{spec} 280 | @forEach [${objectPattern}] as item 281 | ${item}: 282 | ${spec} 283 | 284 | 285 | # Apply two specs to all elements in a single line 286 | # e.g. 287 | # 288 | # | every menu.item-* is inside menu 0px top bottom and has width 100px 289 | # 290 | # or 291 | # 292 | # | every menu.item-* has width and is inside menu 0px top bottom 293 | # 294 | @rule %{subj: every|a|an} %{objectPattern} %{verb: is|has} %{spec1} and %{verb2: is|has} %{spec2} 295 | ${objectPattern}: 296 | ${spec1} 297 | ${spec2} 298 | 299 | 300 | 301 | # Apply spec to only first element in a single line 302 | # e.g. 303 | # 304 | # | first menu.item-* is inside menu 0px top bottom 305 | # 306 | # or 307 | # 308 | # | first menu.item-* has width 100px 309 | # 310 | @rule first %{objectPattern} %{verb: is|has} %{spec} 311 | @if ${count(objectPattern) > 0} 312 | ${first(objectPattern).name}: 313 | ${spec} 314 | 315 | 316 | 317 | # Apply spec to only last element in a single line 318 | # e.g. 319 | # 320 | # | last menu.item-* is inside menu 0px top bottom 321 | # 322 | # or 323 | # 324 | # | last menu.item-* has width 100px 325 | # 326 | @rule last %{objectPattern} %{verb: is|has} %{spec} 327 | @if ${count(objectPattern) > 0} 328 | ${last(objectPattern).name}: 329 | ${spec} 330 | 331 | 332 | 333 | # Apply rule body to only first element 334 | # e.g. 335 | # 336 | # | first menu.item-* : 337 | # below header 10px 338 | # inside main_container 0px top left 339 | # 340 | @rule first %{objectPattern}: 341 | @if ${count(objectPattern) > 0} 342 | ${first(objectPattern).name}: 343 | @ruleBody 344 | 345 | 346 | # Apply rule body to only last element 347 | # e.g. 348 | # 349 | # | last menu.item-* : 350 | # above footer 10px 351 | # inside main_container 0px left right 352 | # 353 | @rule last %{objectPattern}: 354 | @if ${count(objectPattern) > 0} 355 | ${last(objectPattern).name}: 356 | @ruleBody 357 | 358 | 359 | # Check that the element has non null width and height 360 | # e.g. 361 | # 362 | # login_label: 363 | # | is more or less readable 364 | # 365 | @rule is more or less readable 366 | width > 30px 367 | height > 10px 368 | 369 | 370 | # Check that multiple elements have non null width and height 371 | # e.g. 372 | # 373 | # | menu.item-* should be more or less readable 374 | # 375 | @rule %{itemPatterns} should be more or less readable 376 | ${itemPatterns}: 377 | width > 30px 378 | height > 10px 379 | 380 | 381 | # Check that an element is big enough so that you can tap it 382 | # 383 | # login_button: 384 | # | is tapable 385 | # 386 | @rule is tapable 387 | width > 50px 388 | height > 30px 389 | 390 | # Check that multiple elements are big enough so that you can tap them 391 | # 392 | # | every menu.item-* is tapable 393 | # 394 | @rule %{itemPatterns} should be tapable 395 | ${itemPatterns}: 396 | width > 50px 397 | height > 30px 398 | 399 | 400 | # Check that element stretches horizontally to another element with minimal margin from sides 401 | # e.g. 402 | # 403 | # login_panel: 404 | # | stretches to main_container 405 | # 406 | @rule stretches to %{parentObject} 407 | inside ${parentObject} ${GEXTRAS_NO_MARGIN} left right 408 | 409 | 410 | 411 | # Check that element stretches horizontally to another element 412 | # and that it has some specific margin from sides 413 | # e.g. 414 | # 415 | # login_panel: 416 | # | stretches to main_container with 10px margin 417 | # 418 | @rule stretches to %{parentObject} with %{margin} margin 419 | inside ${parentObject} ${margin} left right 420 | 421 | 422 | # Check that multiple elements stretch horizontally to another element with minimal margin from sides 423 | # e.g. 424 | # 425 | # | login_panel, register_panel should stretch to main_container 426 | # 427 | @rule %{itemPattern} should stretch to %{parentObject} 428 | ${itemPattern}: 429 | inside ${parentObject} ${GEXTRAS_NO_MARGIN} left right 430 | 431 | 432 | 433 | # Check that multiple elements stretch horizontally to another element 434 | # and that it has some specific margin from sides 435 | # e.g. 436 | # 437 | # | login_panel, register_panel should stretch to main_container with 10px margin 438 | # 439 | @rule %{itemPattern} should stretch to %{parentObject} with %{margin} margin 440 | ${itemPattern}: 441 | inside ${parentObject} ${margin} left right 442 | 443 | 444 | 445 | # Check that element stretches vertically to another element with minimal margin from sides 446 | # e.g. 447 | # 448 | # login_panel: 449 | # | stretches vertically to main_container 450 | # 451 | @rule stretches vertically to %{parentObject} 452 | inside ${parentObject} ${GEXTRAS_NO_MARGIN} top bottom 453 | 454 | 455 | 456 | # Check that element stretches vertically to another element 457 | # and that it has some specific margin from sides 458 | # e.g. 459 | # 460 | # login_panel: 461 | # | stretches vertically to main_container with 10px margin 462 | # 463 | @rule stretches vertically to %{parentObject} with %{margin} margin 464 | inside ${parentObject} ${margin} top bottom 465 | 466 | 467 | 468 | # Check that multiple elements stretch vertically to another element with minimal margin from sides 469 | # e.g. 470 | # 471 | # | login_panel, register_panel should stretch vertically to main_container 472 | # 473 | @rule %{itemPattern} should stretch vertically to %{parentObject} 474 | ${itemPattern}: 475 | inside ${parentObject} ${GEXTRAS_NO_MARGIN} top bottom 476 | 477 | 478 | 479 | # Check that multiple elements stretch vertically to another element 480 | # and that it has some specific margin from sides 481 | # e.g. 482 | # 483 | # | login_panel, register_panel should stretch vertically to main_container with 10px margin 484 | # 485 | @rule %{itemPattern} should stretch vertically to %{parentObject} with %{margin} margin 486 | ${itemPattern}: 487 | inside ${parentObject} ${margin} top bottom 488 | 489 | 490 | # Check that element is located inside another element on specific side with minimal margin 491 | # e.g. 492 | # 493 | # login_button: 494 | # | located at the top inside main_container 495 | # 496 | @rule located at the %{side} inside %{parentObject} 497 | inside ${parentObject} ${GEXTRAS_NO_MARGIN} ${side} 498 | 499 | 500 | 501 | # Check that element is located inside another element on specific side with specific margin 502 | # e.g. 503 | # 504 | # login_button: 505 | # | located at the top inside main_container with 10px margin 506 | # 507 | @rule located at the %{side} inside %{parentObject} with %{margin} margin 508 | inside ${parentObject} ${margin} ${side} 509 | 510 | 511 | 512 | # Check that multiple elements are located inside another element on specific side with minimal margin 513 | # e.g. 514 | # 515 | # | menu.item-* should be located at the right inside menu with ~10px margin 516 | # 517 | @rule %{objectName} should be located at the %{side} inside %{parentObject} 518 | ${objectName}: 519 | inside ${parentObject} ${GEXTRAS_NO_MARGIN} ${side} 520 | 521 | 522 | # Check that multiple elements are located inside another element on specific side with specific margin 523 | # e.g. 524 | # 525 | # | menu.item-* should be located at the right inside menu with ~10px margin 526 | # 527 | @rule %{objectName} should be located at the %{side} inside %{parentObject} with %{margin} margin 528 | ${objectName}: 529 | inside ${parentObject} ${margin} ${side} 530 | 531 | 532 | 533 | # Check that element is located on specific side inside another element 534 | # and that it takes some specific percentage of its width 535 | # e.g. 536 | # 537 | # login_panel: 538 | # | located on the left side of main_container and takes 70 % of its width 539 | # 540 | @rule located on the %{side} side of %{parentItem} and takes %{percentWidth}% of its %{sizeSide: width|height} 541 | inside ${parentItem} ${GEXTRAS_NO_MARGIN} ${side} 542 | width ${percentWidth} % of ${parentItem}/${sizeSide} 543 | 544 | 545 | 546 | # Check that element is located on specific side inside another element 547 | # and that it takes some specific percentage of its width 548 | # e.g. 549 | # 550 | # login_panel: 551 | # | located on the left side of main_container with ~10px margin and takes 70 % of its width 552 | # 553 | @rule located on the %{side} side of %{parentItem} with %{margin} margin and takes %{percentWidth}% of its %{sizeSide: width|height} 554 | inside ${parentItem} ${margin} ${side} 555 | width ${percentWidth} % of ${parentItem}/${sizeSide} 556 | 557 | 558 | 559 | 560 | # Check that multiple elements are located on specific side inside another element 561 | # and that they take some specific percentage of its width 562 | # e.g. 563 | # 564 | # | message-* should be located on left side of main_container and take 70 % of its width 565 | # 566 | @rule %{itemPattern} should be located on the %{side} side of %{parentItem} and take %{percentWidth}% of its %{sizeSide: width|height} 567 | ${itemPattern}: 568 | inside ${parentItem} ${GEXTRAS_NO_MARGIN} ${side} 569 | width ${percentWidth} % of ${parentItem}/${sizeSide} 570 | 571 | 572 | 573 | # Check that multiple elements are located on specific side inside another element 574 | # and that they take some specific percentage of its width 575 | # e.g. 576 | # 577 | # | message-* should be located on left side of main_container and take 70 % of its width 578 | # 579 | @rule %{itemPattern} should be located on the %{side} side of %{parentItem} with %{margin} margin and take %{percentWidth}% of its %{sizeSide: width|height} 580 | ${itemPattern}: 581 | inside ${parentItem} ${margin} ${side} 582 | width ${percentWidth} % of ${parentItem}/${sizeSide} 583 | 584 | 585 | 586 | 587 | # Apply some validations to the element only on a specific tag, and at the same time check that is absent on another tags 588 | # e.g. 589 | # 590 | # | register_panel should be absent on mobile but on desktop,tablet: 591 | # inside main_container 10 to 20px top left 592 | # 593 | @rule %{item} should be absent on %{absentTags} but on %{activeTags}: 594 | ${item}: 595 | @on ${absentTags} 596 | absent 597 | @on ${activeTags} 598 | @ruleBody 599 | 600 | 601 | --------------------------------------------------------------------------------