Framework for building UIs for OpenStack projects dealing with complex input data
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

directivesSpec.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. /* Copyright (c) 2015 Mirantis, Inc.
  2. Licensed under the Apache License, Version 2.0 (the "License"); you may
  3. not use this file except in compliance with the License. You may obtain
  4. a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  8. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  9. License for the specific language governing permissions and limitations
  10. under the License.
  11. */
  12. describe('merlin directives', function() {
  13. 'use strict';
  14. var $compile, $scope, $httpBackend;
  15. beforeEach(function() {
  16. module('merlin', function($provide) {
  17. $provide.value('fieldTemplates', ['number', 'text']);
  18. });
  19. module('preprocessedTemplates');
  20. });
  21. beforeEach(inject(function(_$compile_, _$rootScope_, _$httpBackend_, _$templateCache_) {
  22. $compile = _$compile_;
  23. $scope = _$rootScope_.$new();
  24. $httpBackend = _$httpBackend_;
  25. $httpBackend.whenGET('/static/merlin/templates/fields/text.html').respond(
  26. 200, _$templateCache_.get('/static/merlin/templates/fields/text.html'));
  27. $httpBackend.whenGET('/static/merlin/templates/fields/number.html').respond(
  28. 200, _$templateCache_.get('/static/merlin/templates/fields/number.html'));
  29. }));
  30. describe('<panel>', function() {
  31. function getPanelHeading(panelElem) {
  32. var div = panelElem.children().children().eq(0);
  33. return div.hasClass('panel-heading') && div;
  34. }
  35. function getPanelRemoveButton(panelElem) {
  36. return panelElem.find('a').eq(2);
  37. }
  38. function getCollapseBtn(panelElem) {
  39. return panelElem.find('a').eq(0);
  40. }
  41. function getPanelBody(panelElem) {
  42. var div = panelElem.children().children().eq(1);
  43. return div.hasClass('panel-body') && div;
  44. }
  45. function makePanelElem(content) {
  46. var panel = $compile('<panel content="' + content + '"></panel>')($scope);
  47. $scope.$digest();
  48. return panel;
  49. }
  50. function makePanelWithInnerTags() {
  51. var element = $compile('<panel><span class="inner"></span></panel>')($scope);
  52. $scope.$digest();
  53. return element;
  54. }
  55. it('shows panel heading when and only when its title() is not false', function() {
  56. var title = 'My Panel',
  57. element1, element2;
  58. $scope.panel1 = {
  59. title: function() { return title; }
  60. };
  61. $scope.panel2 = {};
  62. element1 = makePanelElem('panel1');
  63. element2 = makePanelElem('');
  64. expect(getPanelHeading(element1).hasClass('ng-hide')).toBe(false);
  65. expect(element1.html()).toContain(title);
  66. expect(getPanelHeading(element2).hasClass('ng-hide')).toBe(true);
  67. });
  68. it('requires both `.title()` and `.removable` to be removable', function() {
  69. var title = 'My Panel',
  70. element1, element2;
  71. $scope.panel1 = {
  72. title: function() { return title; },
  73. removable: true
  74. };
  75. $scope.panel2 = {
  76. title: function() { return title; }
  77. };
  78. element1 = makePanelElem('panel1');
  79. element2 = makePanelElem('panel2');
  80. expect(getPanelRemoveButton(element1).hasClass('ng-hide')).toBe(false);
  81. expect(getPanelRemoveButton(element2).hasClass('ng-hide')).toBe(true);
  82. });
  83. it('contents are inserted into div.panel-body tag', function() {
  84. var panel = makePanelWithInnerTags();
  85. expect(getPanelBody(panel).find('span').hasClass('inner')).toBe(true);
  86. });
  87. it('starts as being expanded', function() {
  88. var panel = makePanelWithInnerTags(),
  89. body = getPanelBody(panel);
  90. expect(body.hasClass('collapse')).toBe(true);
  91. expect(body.hasClass('in')).toBe(true);
  92. });
  93. it('starts to collapse after pressing on triangle next to group title', function() {
  94. // NOTE(tsufiev): I wasn't able to test the final .collapse state (without .in)
  95. // most probably due to transition from .collapse.in -> .collapsing -> .collapse
  96. // is made with means of CSS, not
  97. var element = makePanelWithInnerTags(),
  98. body = getPanelBody(element),
  99. link = getCollapseBtn(element);
  100. link.triggerHandler('click');
  101. expect(body.hasClass('collapse')).toBe(false);
  102. expect(body.hasClass('collapsing')).toBe(true);
  103. });
  104. });
  105. describe('<collapsible-group>', function() {
  106. function getGroupBody(groupElem) {
  107. var div = groupElem.children().children().eq(1);
  108. return div.hasClass('section-body') && div;
  109. }
  110. function getGroupRemoveBtn(groupElem) {
  111. return groupElem.find('.remove-entry');
  112. }
  113. function getGroupAddBtn(groupElem) {
  114. return groupElem.find('.add-entry');
  115. }
  116. function getCollapseBtn(groupElem) {
  117. return groupElem.find('.collapse-entries');
  118. }
  119. function makeGroupElement(contents) {
  120. var group = $compile(
  121. '<collapsible-group ' + contents + '></collapsible-group>')($scope);
  122. $scope.$digest();
  123. return group;
  124. }
  125. function makeGroupWithInnerTags() {
  126. var group = $compile(
  127. '<collapsible-group><span class="inner"></span></collapsible-group>'
  128. )($scope);
  129. $scope.$digest();
  130. return group;
  131. }
  132. it('starts as being expanded', function() {
  133. var element = makeGroupWithInnerTags(),
  134. body = getGroupBody(element);
  135. expect(body.hasClass('collapse')).toBe(true);
  136. expect(body.hasClass('in')).toBe(true);
  137. });
  138. it('starts to collapse after pressing on triangle next to group title', function() {
  139. // NOTE(tsufiev): I wasn't able to test the final .collapse state (without .in)
  140. // most probably due to transition from .collapse.in -> .collapsing -> .collapse
  141. // is made with means of CSS, not
  142. var element = makeGroupWithInnerTags(),
  143. body = getGroupBody(element),
  144. link = getCollapseBtn(element);
  145. link.triggerHandler('click');
  146. expect(body.hasClass('collapse')).toBe(false);
  147. expect(body.hasClass('collapsing')).toBe(true);
  148. });
  149. it('requires to specify `on-remove` to make group removable', function() {
  150. var element1, element2;
  151. $scope.remove = function() {};
  152. element1 = makeGroupElement('');
  153. element2 = makeGroupElement('on-remove="remove()"');
  154. expect(getGroupRemoveBtn(element1).length).toBe(0);
  155. expect(getGroupRemoveBtn(element2).hasClass('ng-hide')).toBe(false);
  156. });
  157. it('`removable` attribute set explicitly to `false` makes group not removable', function() {
  158. var element;
  159. $scope.remove = function() {};
  160. element = makeGroupElement('on-remove="remove()" removable="false"');
  161. expect(getGroupRemoveBtn(element).length).toBe(0);
  162. });
  163. it('requires to specify `on-add` to make group additive', function() {
  164. var element1, element2;
  165. $scope.add = function() {};
  166. element1 = makeGroupElement('');
  167. element2 = makeGroupElement('on-add="add()"');
  168. expect(getGroupAddBtn(element1).length).toBe(0);
  169. expect(getGroupAddBtn(element2).hasClass('ng-hide')).toBe(false);
  170. });
  171. it('`additive` attribute set explicitly to `false` makes group not additive', function() {
  172. var element;
  173. $scope.add = function() {};
  174. element = makeGroupElement('on-add="add()" additive="false"');
  175. expect(getGroupAddBtn(element).length).toBe(0);
  176. });
  177. it('contents are inserted into div.collapse tag', function() {
  178. var element = makeGroupWithInnerTags();
  179. expect(getGroupBody(element).find('span').hasClass('inner')).toBe(true);
  180. });
  181. });
  182. describe('<typed-field>', function() {
  183. function makeFieldElem(contents) {
  184. return $compile(
  185. '<div><typed-field ' + contents + '></typed-field></div>')($scope);
  186. }
  187. it('type of resulting field is determined by `type` attribute', function() {
  188. var element1, element2;
  189. $scope.value1 = {type: 'text'};
  190. $scope.value2 = {type: 'number'};
  191. element1 = makeFieldElem('value="value1" type="{$ value1.type $}"');
  192. element2 = makeFieldElem('value="value2" type="{$ value2.type $}"');
  193. $httpBackend.flush();
  194. $scope.$digest();
  195. expect(element1.html()).toContain('<textarea');
  196. expect(element2.html()).toContain('<input type="number"');
  197. });
  198. it('field is not rendered until the corresponding template has been served', function() {
  199. var element;
  200. $scope.value = {type: 'text'};
  201. element = makeFieldElem('value="value" type="{$ value.type $}"');
  202. expect(element.html()).not.toContain('<textarea');
  203. $httpBackend.flush();
  204. expect(element.html()).toContain('<textarea');
  205. });
  206. describe('various types', function() {
  207. describe('.title() of every field except group', function() {
  208. it("tries to extract title from '@meta' key", function() {
  209. });
  210. it("when no title found in '@meta', takes value of 'name' subfield given it's ImmutableObj", function() {
  211. });
  212. it("when no title found both in '@meta' and in 'name' subfield, uses capitalized field ID", function() {
  213. });
  214. });
  215. describe('.title() of group field', function() {
  216. it('if the field is not removable, uses the conventional .title()', function() {
  217. });
  218. it('if the field is removable, uses .title() as a wrapper around .getID()/.setID()', function() {
  219. });
  220. })
  221. })
  222. });
  223. xdescribe("'show-focus'", function() {
  224. var element;
  225. beforeEach(function() {
  226. element = $compile(
  227. '<div><input type="text" ng-show="show" show-focus="show"></div>')($scope);
  228. $scope.$digest();
  229. });
  230. it('allows to immediately set focus on element after it was shown', function() {
  231. expect(element.is(':focus')).toBe(false);
  232. $scope.show = true;
  233. $scope.$apply();
  234. expect(element.is(':focus')).toBe(true);
  235. })
  236. });
  237. describe('<editable>', function() {
  238. it('starts with the value not being edited', function() {
  239. });
  240. it("enters the editing mode once user clicks 'fa-pencil' icon", function() {
  241. });
  242. describe('during editing', function() {
  243. it("pressing any key except 'Enter' or 'Esc' neither exits editing state, nor changes model", function() {
  244. });
  245. it("pressing 'Enter' key changes model and exits editing state", function() {
  246. });
  247. it("clicking 'fa-check' icon changes model and exits editing state", function() {
  248. });
  249. it("pressing 'Esc' key exits editing state without changing model", function() {
  250. });
  251. it("clicking 'fa-close' icon exits editing state without changing model", function() {
  252. });
  253. describe('edit box automatically enlarges', function() {
  254. it('to fit the value being edited', function() {
  255. });
  256. it('up to the limit', function() {
  257. });
  258. })
  259. });
  260. });
  261. describe("'validatable'", function() {
  262. var fields;
  263. beforeEach(inject(function($injector) {
  264. fields = $injector.get('merlin.field.models');
  265. }));
  266. describe('working with the @constraints property:', function() {
  267. var model, elt,
  268. goodValue = 'allowedValue',
  269. badValue = 'restrictedValue',
  270. errorMessage = 'Wrong value provided';
  271. beforeEach(function() {
  272. var modelClass = fields.string.extend({}, {
  273. '@constraints': [
  274. function(value) {
  275. return value !== badValue ? true : errorMessage;
  276. }
  277. ]
  278. });
  279. $scope.model = modelClass.create();
  280. elt = $compile('<form name="form"><input name="model" type="text" ' +
  281. 'ng-model="model.value" ng-model-options="{ getterSetter: true }" ' +
  282. 'validatable-with="model"></form>')($scope);
  283. });
  284. describe('any valid value', function() {
  285. beforeEach(function() {
  286. $scope.form.model.$setViewValue(goodValue);
  287. $scope.$digest();
  288. });
  289. it('is allowed to be entered', function() {
  290. expect($scope.form.model.$viewValue).toEqual(goodValue);
  291. });
  292. it('is propagated into the model', function() {
  293. expect($scope.model.value()).toEqual(goodValue);
  294. });
  295. it('does not cause the input to be marked as erroneous', function() {
  296. expect(elt.find('input').hasClass('ng-valid')).toBe(true);
  297. });
  298. it('sets error message on scope to an empty string', function() {
  299. expect($scope.error).toEqual('');
  300. });
  301. });
  302. describe('any invalid value', function() {
  303. beforeEach(function() {
  304. $scope.form.model.$setViewValue(badValue);
  305. $scope.$digest();
  306. });
  307. it('is allowed to be entered', function() {
  308. expect($scope.form.model.$viewValue).toEqual(badValue);
  309. });
  310. it('is not propagated into the model', function() {
  311. expect($scope.model.value()).toBe(undefined);
  312. });
  313. it('causes the input to be marked as erroneous', function() {
  314. expect(elt.find('input').hasClass('ng-invalid')).toBe(true);
  315. });
  316. it('exposes error message in the parent scope', function() {
  317. expect($scope.error).toEqual(errorMessage);
  318. })
  319. });
  320. });
  321. describe('working with the @required property', function() {
  322. // TODO: fill in once validation of @required fields changes in Barricade
  323. });
  324. });
  325. });