PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/test/editors/select.js

https://github.com/powmedia/backbone-forms
JavaScript | 733 lines | 566 code | 154 blank | 13 comment | 2 complexity | b1cef727a0e44c48138eec8330d986a7 MD5 | raw file
  1. ;(function(Form, Editor) {
  2. module('Select', {
  3. setup: function() {
  4. this.sinon = sinon.sandbox.create();
  5. },
  6. teardown: function() {
  7. this.sinon.restore();
  8. }
  9. });
  10. var same = deepEqual;
  11. var OptionModel = Backbone.Model.extend({
  12. toString: function() {
  13. return this.get('name');
  14. }
  15. });
  16. var OptionCollection = Backbone.Collection.extend({
  17. model: OptionModel
  18. });
  19. var schema = {
  20. options: ['Sterling', 'Lana', 'Cyril', 'Cheryl', 'Pam']
  21. };
  22. var optGroupSchema = {
  23. options: [
  24. {
  25. group: 'Cities',
  26. options: [ 'Paris', 'Beijing', 'San Francisco']
  27. },
  28. {
  29. group: 'Countries',
  30. options: [{val: 'fr', label: 'France'}, {val: 'cn', label: 'China'}]
  31. }
  32. ]
  33. };
  34. test('Default value', function() {
  35. var editor = new Editor({
  36. schema: schema
  37. }).render();
  38. equal(editor.getValue(), 'Sterling');
  39. });
  40. test('Custom value', function() {
  41. var editor = new Editor({
  42. value: 'Cyril',
  43. schema: schema
  44. }).render();
  45. equal(editor.getValue(), 'Cyril');
  46. });
  47. test('Value from model', function() {
  48. var editor = new Editor({
  49. model: new Backbone.Model({ name: 'Lana' }),
  50. key: 'name',
  51. schema: schema
  52. }).render();
  53. equal(editor.getValue(), 'Lana');
  54. });
  55. test('Correct type', function() {
  56. var editor = new Editor({
  57. schema: schema
  58. }).render();
  59. equal($(editor.el).get(0).tagName, 'SELECT');
  60. });
  61. test('Uses Backbone.$ not global', function() {
  62. var old$ = window.$,
  63. exceptionCaught = false;
  64. window.$ = null;
  65. try {
  66. var editor = new Editor({
  67. schema: schema
  68. }).render();
  69. } catch(e) {
  70. exceptionCaught = true;
  71. }
  72. window.$ = old$;
  73. ok(!exceptionCaught, ' using global \'$\' to render');
  74. });
  75. test('Option groups', function() {
  76. var editor = new Editor({
  77. schema: optGroupSchema
  78. }).render();
  79. equal(editor.$('optgroup').length, 2);
  80. equal(editor.$('optgroup').first().attr('label'), 'Cities')
  81. });
  82. test('Option groups only contain their "own" options', function() {
  83. var editor = new Editor({
  84. schema: optGroupSchema
  85. }).render();
  86. var group = editor.$('optgroup').first();
  87. equal($('option', group).length, 3);
  88. var options = _.map($('option', group), function(el) {
  89. return $(el).text();
  90. });
  91. ok(~options.indexOf('Paris'));
  92. ok(~options.indexOf('Beijing'));
  93. ok(~options.indexOf('San Francisco'));
  94. var group = editor.$('optgroup').last();
  95. equal($('option', group).length, 2);
  96. var options = _.map($('option', group), function(el) {
  97. return $(el).text();
  98. });
  99. ok(~options.indexOf('France'));
  100. ok(~options.indexOf('China'));
  101. });
  102. test('Option groups allow to specify option value / label', function() {
  103. var editor = new Editor({
  104. schema: optGroupSchema
  105. }).render();
  106. var group = editor.$('optgroup').last();
  107. var options = $('option', group);
  108. equal(options.first().attr('value'), 'fr');
  109. equal(options.last().attr('value'), 'cn');
  110. equal(options.first().text(), 'France');
  111. equal(options.last().text(), 'China');
  112. });
  113. test('Option groups with options as string', function() {
  114. var editor = new Editor({
  115. schema: {
  116. options: [
  117. {
  118. group: 'Cities',
  119. options: '<option>Paris</option><option>Beijing</option><option>San Francisco</option>'
  120. },
  121. {
  122. group: 'Countries',
  123. options: '<option value="fr">France</option><option value="cn">China</option>'
  124. }
  125. ]
  126. }
  127. }).render();
  128. var group = editor.$('optgroup').first();
  129. equal(group.attr('label'), 'Cities');
  130. equal($('option', group).length, 3);
  131. equal($('option', group).first().text(), 'Paris');
  132. equal(editor.$('optgroup').length, 2);
  133. });
  134. test('Option groups with options as callback', function() {
  135. var editor = new Editor({
  136. schema: {
  137. options: function(callback, thisEditor) {
  138. ok(thisEditor instanceof Editor);
  139. ok(thisEditor instanceof Form.editors.Base);
  140. callback(optGroupSchema.options);
  141. }
  142. }
  143. }).render();
  144. var optgroups = editor.$('optgroup');
  145. equal(optgroups.length, 2);
  146. equal($('option', optgroups.first()).first().text(), 'Paris');
  147. equal($('option', optgroups.last()).first().text(), 'France');
  148. equal($('option', optgroups.last()).first().attr('value'), 'fr');
  149. });
  150. test('Each option group as its own callback', function() {
  151. var editor = new Editor({
  152. schema: {
  153. options: [
  154. {
  155. group: 'Cities',
  156. options: function(callback, thisEditor) {
  157. ok(thisEditor instanceof Editor);
  158. ok(thisEditor instanceof Form.editors.Base);
  159. callback(optGroupSchema.options[0].options);
  160. }
  161. },
  162. {
  163. group: 'Countries',
  164. options: function(callback, thisEditor) {
  165. ok(thisEditor instanceof Editor);
  166. ok(thisEditor instanceof Form.editors.Base);
  167. callback(optGroupSchema.options[1].options);
  168. }
  169. }
  170. ]
  171. }
  172. }).render();
  173. var optgroups = editor.$('optgroup');
  174. equal(optgroups.length, 2);
  175. equal($('option', optgroups.first()).first().text(), 'Paris');
  176. equal($('option', optgroups.last()).first().text(), 'France');
  177. equal($('option', optgroups.last()).first().attr('value'), 'fr');
  178. });
  179. test('Mixed specification for option groups', function() {
  180. var countries = new OptionCollection([
  181. { id: 'fr', name: 'France' },
  182. { id: 'cn', name: 'China' }
  183. ]);
  184. var editor = new Editor({
  185. schema: {
  186. options: [
  187. { group: 'Countries', options: countries },
  188. { group: 'Cities', options: ['Paris', 'Beijing', 'Tokyo']},
  189. { group: 'Food', options: '<option>Bread</option>'},
  190. { group: 'Cars', options: function(callback, thisEditor) {
  191. ok(thisEditor instanceof Editor);
  192. ok(thisEditor instanceof Form.editors.Base);
  193. callback(['VolksWagen', 'Fiat', 'Opel', 'Tesla']);
  194. }}
  195. ]
  196. }
  197. }).render();
  198. var optgroups = editor.$('optgroup');
  199. equal(optgroups.length, 4);
  200. // Countries:
  201. var options = $('option', optgroups.get(0));
  202. equal(options.length, 2);
  203. equal(options.first().attr('value'), 'fr');
  204. equal(options.first().text(), 'France');
  205. // Cities
  206. var options = $('option', optgroups.get(1));
  207. equal(options.length, 3);
  208. equal(options.first().text(), 'Paris');
  209. // Food
  210. var options = $('option', optgroups.get(2));
  211. equal(options.length, 1);
  212. equal(options.first().text(), 'Bread');
  213. // Cars
  214. var options = $('option', optgroups.get(3));
  215. equal(options.length, 4);
  216. equal(options.last().text(), 'Tesla');
  217. });
  218. test('Option groups with collections', function() {
  219. var countries = new OptionCollection([
  220. { id: 'fr', name: 'France' },
  221. { id: 'cn', name: 'China' }
  222. ]);
  223. var cities = new OptionCollection([
  224. { id: 'paris', name: 'Paris' },
  225. { id: 'bj', name: 'Beijing' },
  226. { id: 'sf', name: 'San Francisco' }
  227. ]);
  228. var editor = new Editor({
  229. schema: {
  230. options: [
  231. {
  232. group: 'Countries',
  233. options: countries
  234. },
  235. {
  236. group: 'Cities',
  237. options: cities
  238. }
  239. ]
  240. }
  241. }).render();
  242. var optgroups = editor.$el.find('optgroup');
  243. equal(optgroups.length, 2);
  244. equal($('option', optgroups.first()).first().text(), 'France');
  245. equal($('option', optgroups.first()).first().attr('value'), 'fr');
  246. equal($('option', optgroups.last()).last().attr('value'), 'sf');
  247. equal($('option', optgroups.last()).last().text(), 'San Francisco');
  248. });
  249. test('setOptions() - updates the options on a rendered select', function() {
  250. var editor = new Editor({
  251. schema: schema
  252. }).render();
  253. editor.setOptions([1,2,3]);
  254. var newOptions = editor.$el.find('option');
  255. equal(newOptions.length, 3);
  256. equal(newOptions.first().html(), 1);
  257. equal(newOptions.last().html(), 3);
  258. });
  259. test('Options as array of items', function() {
  260. var editor = new Editor({
  261. schema: {
  262. options: ['Matilda', 'Larry']
  263. }
  264. }).render();
  265. var newOptions = editor.$el.find('option');
  266. equal(newOptions.first().html(), 'Matilda');
  267. equal(newOptions.last().html(), 'Larry');
  268. });
  269. test('Options as array of objects', function() {
  270. var editor = new Editor({
  271. schema: {
  272. options: [
  273. { val: 'kid1', label: 'Teo' },
  274. { val: 'kid2', label: 'Lilah' },
  275. ]
  276. }
  277. }).render();
  278. var newOptions = editor.$el.find('option');
  279. equal(newOptions.first().val(), 'kid1');
  280. equal(newOptions.last().val(), 'kid2');
  281. equal(newOptions.first().html(), 'Teo');
  282. equal(newOptions.last().html(), 'Lilah');
  283. });
  284. test('Options as any object', function() {
  285. var editor = new Editor({
  286. schema: {
  287. options: {y:"Yes",n:"No"}
  288. }
  289. }).render();
  290. var newOptions = editor.$el.find('option');
  291. equal(newOptions.first().val(), 'y');
  292. equal(newOptions.last().val(), 'n');
  293. equal(newOptions.first().html(), 'Yes');
  294. equal(newOptions.last().html(), 'No');
  295. });
  296. test('Options as function that calls back with options', function() {
  297. var editor = new Editor({
  298. schema: {
  299. options: function(callback, thisEditor) {
  300. ok(thisEditor instanceof Editor);
  301. ok(thisEditor instanceof Form.editors.Base);
  302. callback(['Melony', 'Frank']);
  303. }
  304. }
  305. }).render();
  306. var newOptions = editor.$el.find('option');
  307. equal(newOptions.first().html(), 'Melony');
  308. equal(newOptions.last().html(), 'Frank');
  309. });
  310. test('Options as string of HTML', function() {
  311. var editor = new Editor({
  312. schema: {
  313. options: '<option>Howard</option><option>Bree</option>'
  314. }
  315. }).render();
  316. var newOptions = editor.$el.find('option');
  317. equal(newOptions.first().html(), 'Howard');
  318. equal(newOptions.last().html(), 'Bree');
  319. });
  320. test('Options as a pre-populated collection', function() {
  321. var options = new OptionCollection([
  322. { id: 'kid1', name: 'Billy' },
  323. { id: 'kid2', name: 'Sarah' }
  324. ]);
  325. var editor = new Editor({
  326. schema: {
  327. options: options
  328. }
  329. }).render();
  330. var newOptions = editor.$el.find('option');
  331. equal(newOptions.first().val(), 'kid1');
  332. equal(newOptions.last().val(), 'kid2');
  333. equal(newOptions.first().html(), 'Billy');
  334. equal(newOptions.last().html(), 'Sarah');
  335. });
  336. test('Options as a new collection (needs to be fetched)', function() {
  337. var options = new OptionCollection();
  338. this.sinon.stub(options, 'fetch', function(options) {
  339. this.set([
  340. { id: 'kid1', name: 'Barbara' },
  341. { id: 'kid2', name: 'Phil' }
  342. ]);
  343. options.success(this);
  344. });
  345. var editor = new Editor({
  346. schema: {
  347. options: options
  348. }
  349. }).render();
  350. var newOptions = editor.$el.find('option');
  351. equal(newOptions.first().val(), 'kid1');
  352. equal(newOptions.last().val(), 'kid2');
  353. equal(newOptions.first().html(), 'Barbara');
  354. equal(newOptions.last().html(), 'Phil');
  355. });
  356. test("setValue() - updates the input value", function() {
  357. var editor = new Editor({
  358. value: 'Pam',
  359. schema: schema
  360. }).render();
  361. editor.setValue('Lana');
  362. equal(editor.getValue(), 'Lana');
  363. equal($(editor.el).val(), 'Lana');
  364. });
  365. module('Select events', {
  366. setup: function() {
  367. this.sinon = sinon.sandbox.create();
  368. this.editor = new Editor({
  369. value: 'Pam',
  370. schema: schema
  371. }).render();
  372. $('body').append(this.editor.el);
  373. },
  374. teardown: function() {
  375. this.sinon.restore();
  376. this.editor.remove();
  377. }
  378. });
  379. test("focus() - gives focus to editor and its selectbox", function() {
  380. var editor = this.editor;
  381. editor.focus();
  382. ok(editor.hasFocus);
  383. ok(editor.$el.is(':focus'));
  384. });
  385. test("focus() - triggers the 'focus' event", function() {
  386. var editor = this.editor;
  387. var spy = this.sinon.spy();
  388. editor.on('focus', spy);
  389. editor.focus();
  390. ok(spy.called);
  391. ok(spy.calledWith(editor));
  392. });
  393. test("blur() - removes focus from the editor and its selectbox", function() {
  394. var editor = this.editor;
  395. editor.focus();
  396. editor.blur();
  397. ok(!editor.hasFocus);
  398. ok(!editor.$el.is(':focus'));
  399. });
  400. test("blur() - triggers the 'blur' event", function() {
  401. var editor = this.editor;
  402. editor.focus()
  403. var spy = this.sinon.spy();
  404. editor.on('blur', spy);
  405. editor.blur();
  406. ok(spy.called);
  407. ok(spy.calledWith(editor));
  408. });
  409. test("'change' event - bubbles up from the selectbox", function() {
  410. var editor = this.editor;
  411. var spy = this.sinon.spy();
  412. editor.on('change', spy);
  413. editor.$el.val('Cyril');
  414. editor.$el.change();
  415. ok(spy.calledOnce);
  416. ok(spy.alwaysCalledWith(editor));
  417. });
  418. test("'change' event - is triggered when value of select changes", function() {
  419. var editor = this.editor;
  420. var callCount = 0;
  421. var spy = this.sinon.spy();
  422. editor.on('change', spy);
  423. // Pressing a key
  424. editor.$el.keypress();
  425. editor.$el.val('a');
  426. stop();
  427. setTimeout(function(){
  428. callCount++;
  429. editor.$el.keyup();
  430. // Keeping a key pressed for a longer time
  431. editor.$el.keypress();
  432. editor.$el.val('Pam');
  433. setTimeout(function(){
  434. callCount++;
  435. editor.$el.keypress();
  436. editor.$el.val('Cheryl');
  437. setTimeout(function(){
  438. callCount++;
  439. editor.$el.keyup();
  440. // Left; Right: Pointlessly moving around
  441. editor.$el.keyup();
  442. editor.$el.keyup();
  443. ok(spy.callCount == callCount);
  444. ok(spy.alwaysCalledWith(editor));
  445. start();
  446. }, 0);
  447. }, 0);
  448. }, 0);
  449. });
  450. test("'focus' event - bubbles up from the selectbox", function() {
  451. var editor = this.editor;
  452. var spy = this.sinon.spy();
  453. editor.on('focus', spy);
  454. editor.$el.focus();
  455. ok(spy.calledOnce);
  456. ok(spy.alwaysCalledWith(editor));
  457. });
  458. test("'blur' event - bubbles up from the selectbox", function() {
  459. var editor = this.editor;
  460. editor.$el.focus();
  461. var spy = this.sinon.spy();
  462. editor.on('blur', spy);
  463. editor.$el.blur();
  464. ok(spy.calledOnce);
  465. ok(spy.alwaysCalledWith(editor));
  466. });
  467. module('Select Text Escaping', {
  468. setup: function() {
  469. this.sinon = sinon.sandbox.create();
  470. this.options = [
  471. {
  472. val: '"/><script>throw("XSS Success");</script>',
  473. label: '"/><script>throw("XSS Success");</script>'
  474. },
  475. {
  476. val: '\"?\'\/><script>throw("XSS Success");</script>',
  477. label: '\"?\'\/><script>throw("XSS Success");</script>',
  478. },
  479. {
  480. val: '><b>HTML</b><',
  481. label: '><div class=>HTML</b><',
  482. }
  483. ];
  484. this.editor = new Editor({
  485. schema: {
  486. options: this.options
  487. }
  488. }).render();
  489. $('body').append(this.editor.el);
  490. },
  491. teardown: function() {
  492. this.sinon.restore();
  493. this.editor.remove();
  494. }
  495. });
  496. test('options content gets properly escaped', function() {
  497. same( this.editor.schema.options, this.options );
  498. //What an awful string.
  499. //CAN'T have white-space on the left, or the string will no longer match
  500. //If this bothers you aesthetically, can switch it to concat syntax
  501. var escapedHTML = "<option value=\"&quot;/><script>throw(&quot;XSS Success&quot;);\
  502. </script>\">\"/&gt;&lt;script&gt;throw(\"XSS Success\");&lt;/script&gt;</option><option \
  503. value=\"&quot;?'/><script>throw(&quot;XSS Success&quot;);</script>\">\"?'/&gt;&lt;script&gt;\
  504. throw(\"XSS Success\");&lt;/script&gt;</option><option value=\"><b>HTML</b><\">&gt;&lt;div \
  505. class=&gt;HTML&lt;/b&gt;&lt;</option>";
  506. same( this.editor.$el.html(), escapedHTML );
  507. same( this.editor.$('option').val(), this.options[0].val );
  508. same( this.editor.$('option').first().text(), this.options[0].label );
  509. same( this.editor.$('option').first().html(), '\"/&gt;&lt;script&gt;throw(\"XSS Success\");&lt;/script&gt;' );
  510. same( this.editor.$('option').text(), "\"/><script>throw(\"XSS Success\");</script>\"?'/><script>throw(\"XSS Success\");</script>><div class=>HTML</b><" );
  511. });
  512. test('options object content gets properly escaped', function() {
  513. var options = {
  514. key1: '><b>HTML</b><',
  515. key2: '><div class=>HTML</b><'
  516. };
  517. var editor = new Editor({
  518. schema: {
  519. options: options
  520. }
  521. }).render();
  522. same( editor.schema.options, options );
  523. //What an awful string.
  524. //CAN'T have white-space on the left, or the string will no longer match
  525. //If this bothers you aesthetically, can switch it to concat syntax
  526. var escapedHTML = "<option value=\"key1\">&gt;&lt;b&gt;HTML&lt;/b&gt;&lt;</option>\
  527. <option value=\"key2\">&gt;&lt;div class=&gt;HTML&lt;/b&gt;&lt;</option>";
  528. same( editor.$el.html(), escapedHTML );
  529. same( editor.$('option').val(), _.keys(options)[0] );
  530. same( editor.$('option').first().text(), options.key1 );
  531. same( editor.$('option').first().html(), '&gt;&lt;b&gt;HTML&lt;/b&gt;&lt;' );
  532. same( editor.$('option').text(), '><b>HTML</b><><div class=>HTML</b><' );
  533. });
  534. test('option groups content gets properly escaped', function() {
  535. var options = [{
  536. group: '"/><script>throw("XSS Success");</script>',
  537. options: [
  538. {
  539. val: '"/><script>throw("XSS Success");</script>',
  540. label: '"/><script>throw("XSS Success");</script>'
  541. },
  542. {
  543. val: '\"?\'\/><script>throw("XSS Success");</script>',
  544. label: '\"?\'\/><script>throw("XSS Success");</script>',
  545. },
  546. {
  547. val: '><b>HTML</b><',
  548. label: '><div class=>HTML</b><',
  549. }
  550. ]
  551. }];
  552. var editor = new Editor({
  553. schema: {
  554. options: options
  555. }
  556. }).render();
  557. same( editor.schema.options, options );
  558. //What an awful string.
  559. //CAN'T have white-space on the left, or the string will no longer match
  560. //If this bothers you aesthetically, can switch it to concat syntax
  561. var escapedHTML = "<optgroup label=\"&quot;/>\<script>throw(&quot;XSS \
  562. Success&quot;);</script>\"><option value=\"&quot;/>\
  563. <script>throw(&quot;XSS Success&quot;);</script>\">\"/&gt;&lt;script&gt;throw\
  564. (\"XSS Success\");&lt;/script&gt;</option><option value=\"&quot;?'/><script>\
  565. throw(&quot;XSS Success&quot;);</script>\">\"?'/&gt;&lt;script&gt;throw(\"XSS \
  566. Success\");&lt;/script&gt;</option><option value=\"><b>HTML</b><\">&gt;&lt;\
  567. div class=&gt;HTML&lt;/b&gt;&lt;</option></optgroup>";
  568. same( editor.$el.html(), escapedHTML );
  569. same( editor.$('option').val(), options[0].options[0].val );
  570. same( editor.$('option').first().text(), options[0].options[0].label );
  571. same( editor.$('option').first().html(), '\"/&gt;&lt;script&gt;throw(\"XSS Success\");&lt;/script&gt;' );
  572. same( editor.$('option').text(), "\"/><script>throw(\"XSS Success\");</script>\"?'/><script>throw(\"XSS Success\");</script>><div class=>HTML</b><" );
  573. });
  574. })(Backbone.Form, Backbone.Form.editors.Select);