1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            portions copyright ©2011 Apple Inc.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /*global module, test, ok, equals, stop, start */
  9 
 10 var items = [
 11   { title: 'Menu Item', keyEquivalent: 'ctrl_shift_n' },
 12   { title: 'Checked Menu Item', isChecked: YES, keyEquivalent: 'ctrl_a' },
 13   { title: 'Selected Menu Item', keyEquivalent: 'backspace' },
 14   { isSeparator: YES },
 15   { title: 'Menu Item with Icon', icon: 'inbox', keyEquivalent: 'ctrl_m' },
 16   { title: 'Menu Item with Icon', icon: 'folder', keyEquivalent: ['ctrl_p', 'ctrl_f'] },
 17   { isSeparator: YES },
 18   { title: 'Selected Menu Item…', isChecked: YES, keyEquivalent: 'ctrl_shift_o' },
 19   { title: 'Item with Submenu', subMenu: [{ title: 'Submenu item 1' }, { title: 'Submenu item 2'}] },
 20   { title: 'Disabled Menu Item', isEnabled: NO },
 21   { isSeparator: YES },
 22   { title: 'Unique Menu Item Class Per Item', exampleView: SC.MenuItemView.extend({
 23       classNames: 'custom-menu-item'.w()
 24     }) }
 25   // { isSeparator: YES },
 26   // { groupTitle: 'Menu Label', items: [{ title: 'Nested Item' }, { title: 'Nested Item' }] }
 27   ];
 28 
 29 var menu;
 30 
 31 module('SC.MenuPane UI', {
 32   setup: function () {
 33     SC.run(function () {
 34       menu = SC.MenuPane.create({
 35         layout: { width: 206 },
 36 
 37         selectedItemChanged: function () {
 38           this._selectedItemCount = (this._selectedItemCount || 0) + 1;
 39         }.observes('selectedItem'),
 40 
 41         countAction: function () {
 42           this._actionCount = (this._actionCount || 0) + 1;
 43         }
 44       });
 45 
 46       items[0].target = menu;
 47       items[0].action = 'countAction';
 48 
 49       items[1].action = function () {
 50         menu._functionActionCount = (menu._functionActionCount || 0) + 1;
 51       };
 52 
 53       menu.set('items', items);
 54     });
 55   },
 56 
 57   teardown: function () {
 58     SC.run(function () {
 59       menu.destroy();
 60     });
 61     menu = null;
 62   }
 63 });
 64 
 65 /**
 66   Simulates clicking on the specified view.
 67 
 68   @param {SC.View} view the view
 69   @param {Boolean} [shiftKey] simulate shift key pressed
 70   @param {Boolean} [ctrlKey] simulate ctrlKey pressed
 71 */
 72 function clickOn(view, shiftKey, ctrlKey) {
 73   var layer    = view.get('layer'),
 74       opts     = { shiftKey: !!shiftKey, ctrlKey: !!ctrlKey, which: 1 },
 75       ev;
 76 
 77   ok(layer, 'clickOn() precond - view %@ should have layer'.fmt(view.toString()));
 78 
 79   ev = SC.Event.simulateEvent(layer, 'mousedown', opts);
 80   SC.Event.trigger(layer, 'mousedown', [ev]);
 81 
 82   ev = SC.Event.simulateEvent(layer, 'mouseup', opts);
 83   SC.Event.trigger(layer, 'mouseup', [ev]);
 84   SC.RunLoop.begin().end();
 85   layer = null;
 86 }
 87 
 88 /**
 89   Simulates a key press on the specified view.
 90 
 91   @param {SC.View} view the view
 92   @param {Number} keyCode key to simulate
 93   @param {Boolean} [isKeyPress] simulate key press event
 94   @param {Boolean} [shiftKey] simulate shift key pressed
 95   @param {Boolean} [ctrlKey] simulate ctrlKey pressed
 96 */
 97 function keyPressOn(view, keyCode, isKeyPress, shiftKey, ctrlKey) {
 98   var layer    = view.get('layer'),
 99     opts     = {
100       shiftKey: !!shiftKey,
101       ctrlKey: !!ctrlKey,
102       keyCode: keyCode,
103       charCode: isKeyPress ? keyCode : 0,
104       which: keyCode
105     },
106     ev;
107 
108   ok(layer, 'keyPressOn() precond - view %@ should have layer'.fmt(view.toString()));
109 
110   ev = SC.Event.simulateEvent(layer, 'keydown', opts);
111   SC.Event.trigger(layer, 'keydown', [ev]);
112 
113   if (isKeyPress) {
114     ev = SC.Event.simulateEvent(layer, 'keypress', opts);
115     SC.Event.trigger(layer, 'keypress', [ev]);
116   }
117 
118   ev = SC.Event.simulateEvent(layer, 'keyup', opts);
119   SC.Event.trigger(layer, 'keyup', [ev]);
120   SC.RunLoop.begin().end();
121   layer = null;
122 }
123 
124 test('Basic UI', function () {
125   SC.run(function () {
126     menu.popup();
127   });
128   ok(menu.$().hasClass('sc-menu'), 'pane should have "sc-menu" class');
129   ok(menu.$().hasClass('sc-regular-size'), 'pane should have default control size class');
130   ok(!menu.get('isSubMenu'), 'isSubMenu should be NO on menus that are not submenus');
131   var menuItem = menu.get('menuItemViews')[0], selectedItem;
132   menuItem.mouseEntered();
133   clickOn(menuItem, NO, NO);
134   stop();
135 
136   setTimeout(function () {
137     selectedItem = menu.get('selectedItem');
138     ok(selectedItem, 'menu should have selectedItem property set after clicking on menu item');
139     equals(selectedItem ? selectedItem.title : null, menuItem.get('content').title, 'selectedItem should be set to the content item that was clicked');
140     equals(1, menu._selectedItemCount, 'selectedItem should only change once when a menu item is clicked');
141     equals(1, menu._actionCount, 'action is fired once when menu item is clicked');
142     SC.run(function () {
143       menu.remove();
144     });
145     ok(!menu.get('isVisibleInWindow'), 'menu should not be visible after being removed');
146     equals(menu.get('currentMenuItem'), null, 'currentMenuItem should be null after being removed');
147     start();
148   }, 250);
149 });
150 
151 test('Control size', function () {
152   var smallPane, largePane, views, items = [
153     { title: 'Can I get get get' },
154     { title: 'To know know know know', isSeparator: YES },
155     { title: 'Ya better better baby' }
156   ];
157 
158   SC.run(function () {
159     smallPane = SC.MenuPane.create({
160       controlSize: SC.SMALL_CONTROL_SIZE,
161       items: items
162     });
163 
164     smallPane.popup();
165   });
166   views = smallPane.get('menuItemViews');
167 
168   var small_constants = SC.BaseTheme.menuRenderDelegate['sc-small-size'];
169   equals(views[0].get('frame').height, small_constants.itemHeight, 'should change itemHeight');
170   equals(views[1].get('frame').height, small_constants.itemSeparatorHeight, 'should change itemSeparatorHeight');
171   equals(views[0].get('frame').y, small_constants.menuHeightPadding / 2, 'should change menuHeightPadding');
172   SC.run(function () {
173     smallPane.remove();
174   });
175 
176   SC.run(function () {
177     largePane = SC.MenuPane.create({
178       controlSize: SC.LARGE_CONTROL_SIZE,
179       items: items
180     });
181 
182     largePane.popup();
183   });
184   views = largePane.get('menuItemViews');
185 
186   var large_constants = SC.BaseTheme.menuRenderDelegate['sc-large-size'];
187   equals(views[0].get('frame').height, large_constants.itemHeight, 'should change itemHeight');
188   equals(views[1].get('frame').height, large_constants.itemSeparatorHeight, 'should change itemSeparatorHeight');
189   equals(views[0].get('frame').y, large_constants.menuHeightPadding / 2, 'should change menuHeightPadding');
190 
191   SC.run(function () {
192     largePane.remove();
193   });
194 });
195 
196 test('Legacy Function Support', function () {
197   SC.run(function () {
198     menu.popup();
199   });
200   var menuItem = menu.get('menuItemViews')[1], selectedItem;
201   menuItem.mouseEntered();
202   clickOn(menuItem, NO, NO);
203   stop();
204 
205   setTimeout(function () {
206     selectedItem = menu.get('selectedItem');
207     equals(1, menu._functionActionCount, 'Function should be called if it is set as the action and the menu item is clicked');
208 
209     SC.run(function () {
210       menu.remove();
211     });
212     start();
213   }, 250);
214 });
215 
216 test('Custom MenuItemView Class', function () {
217   equals(menu.get('exampleView'), SC.MenuItemView, 'SC.MenuPane should generate SC.MenuItemViews by default');
218   var menu2;
219 
220   SC.run(function () {
221     menu2 = SC.MenuPane.create({
222       exampleView: SC.MenuItemView.extend({
223         classNames: 'custom-menu-item'.w()
224       }),
225 
226       items: items
227     });
228 
229     menu2.popup();
230   });
231   ok(menu2.$('.custom-menu-item').length > 0, 'SC.MenuPane should generate instances of custom classes if exampleView is changed');
232   SC.run(function () {
233     menu2.remove();
234   });
235 });
236 
237 
238 test('Custom MenuItemView Class on an item using itemExampleViewKey', function () {
239   equals(menu.get('exampleView'), SC.MenuItemView, 'SC.MenuPane should generate SC.MenuItemViews by default');
240   SC.run(function () {
241     menu.popup();
242   });
243   ok(menu.$('.custom-menu-item').length === 1, 'SC.MenuPane should generate one instance of a custom class if the item has an exampleView property');
244   ok(SC.$(SC.$('.sc-menu-item')[11]).hasClass('custom-menu-item'), 'The last menu item should have a custom class');
245 
246   SC.run(function () {
247     menu.remove();
248   });
249 });
250 
251 test('Basic Submenus', function () {
252   var smallMenu,
253     menuItem, subMenu;
254 
255   SC.run(function () {
256     smallMenu = SC.MenuPane.create({
257       controlSize: SC.SMALL_CONTROL_SIZE,
258       items: items
259     });
260     menuItem = smallMenu.get('menuItemViews')[8];
261 
262     smallMenu.popup();
263   });
264 
265   menuItem.mouseEntered();
266   SC.RunLoop.begin().end();
267   ok(menuItem.get('hasSubMenu'), 'precond - menu item has a submenu');
268   subMenu = menuItem.get('subMenu');
269   ok(!subMenu.get('isVisibleInWindow'), 'submenus should not open immediately');
270   stop();
271   setTimeout(function () {
272     ok(subMenu.get('isVisibleInWindow'), 'submenu should open after 100ms delay');
273     ok(subMenu.get('isSubMenu'), 'isSubMenu should be YES on submenus');
274     ok(subMenu.get('controlSize'), SC.SMALL_CONTROL_SIZE, "submenu should inherit parent's controlSize");
275     SC.run(function () {
276       smallMenu.remove();
277     });
278     ok(!subMenu.get('isVisibleInWindow'), 'submenus should close if their parent menu is closed');
279     equals(subMenu.getPath('items.length'), 2, 'submenus should have 2 items');
280 
281     menuItem.get('content').set('subMenu', [{ title: 'Submenu item 3' }]);
282     subMenu = menuItem.get('subMenu');
283     equals(subMenu.getPath('items.length'), 1, 'submenus should have 1 item');
284 
285     smallMenu.destroy();
286     ok(smallMenu.get('isDestroyed'), 'smallMenu should be destroyed');
287     ok(menuItem.get('isDestroyed'), 'menuItem should be destroyed');
288     ok(subMenu.get('isDestroyed'), 'submenus should be destroyed');
289     start();
290   }, 150);
291 });
292 
293 test('Menu Item Localization', function () {
294   ok(menu.get('localize'), 'menu panes should be localized by default');
295   var locMenu, items;
296 
297   SC.stringsFor('en', { 'Localized.Text': 'LOCALIZED TEXT' });
298   items = [ 'Localized.Text' ];
299 
300   SC.run(function () {
301     locMenu = SC.MenuPane.create({
302       layout: { width: 200 },
303       items: items,
304       localize: NO
305     });
306 
307     locMenu.popup();
308   });
309   equals('Localized.Text', locMenu.$('.sc-menu-item .value').text(), 'Menu item titles should not be localized if localize is NO');
310 
311   SC.run(function () {
312     locMenu.remove();
313   });
314 
315   SC.run(function () {
316     locMenu = SC.MenuPane.create({
317       items: items,
318       localize: YES
319     });
320 
321     locMenu.popup();
322   });
323   equals('LOCALIZED TEXT', locMenu.$('.sc-menu-item .value').text(), 'Menu item titles should be localized if localize is YES');
324   SC.run(function () {
325     locMenu.remove();
326   });
327 });
328 
329 test('Automatic Closing', function () {
330 
331   SC.run(function () {
332     menu.popup();
333   });
334   ok(menu.get('isVisibleInWindow'), 'precond - window should be visible');
335   SC.run(function () {
336     menu.windowSizeDidChange();
337   });
338   ok(!menu.get('isVisibleInWindow'), 'menu should close if window resizes');
339 
340   SC.run(function () {
341     menu.popup();
342   });
343   clickOn(menu);
344   ok(!menu.get('isVisibleInWindow'), 'menu should close if anywhere other than a menu item is clicked');
345 });
346 
347 test('keyEquivalents', function () {
348   var keyEquivalents = menu._keyEquivalents;
349 
350   // verify that keyEquivalents were mapped correctly and that multiple
351   // keyEquivalents work
352   menu.items.forEach(function (item) {
353     var keyEq = item.keyEquivalent, idx, len;
354     if (!keyEq) return;
355 
356     if (SC.typeOf(keyEq) === SC.T_ARRAY) {
357       for (idx = 0, len = keyEq.length; idx < len; idx++) {
358         ok(keyEquivalents[keyEq[idx]], "keyEquivalent should map to " + keyEq[idx]);
359       }
360     }
361     else {
362       ok(keyEquivalents[keyEq], "keyEquivalent should map to " + keyEq);
363     }
364   });
365 });
366 
367 test('scrolling', function () {
368   var currentMenuItem;
369 
370   SC.run(function () {
371     menu.popup();
372   });
373   menu.set('currentMenuItem', menu.get('menuItemViews')[0]);
374   currentMenuItem = menu.get('currentMenuItem');
375   equals(currentMenuItem.get('title'), 'Menu Item', 'menu should begin at first item');
376 
377   keyPressOn(menu, SC.Event.KEY_DOWN);
378   currentMenuItem = menu.get('currentMenuItem');
379   equals(currentMenuItem.get('title'), 'Checked Menu Item', 'arrow down should move one item down');
380 
381   keyPressOn(menu, SC.Event.KEY_UP);
382   currentMenuItem = menu.get('currentMenuItem');
383   equals(currentMenuItem.get('title'), 'Menu Item', 'arrow up should move one item up');
384 
385   keyPressOn(menu, SC.Event.KEY_PAGEDOWN);
386   currentMenuItem = menu.get('currentMenuItem');
387   equals(currentMenuItem.get('title'), 'Unique Menu Item Class Per Item', 'page down should move one page down');
388 
389   keyPressOn(menu, SC.Event.KEY_PAGEUP);
390   currentMenuItem = menu.get('currentMenuItem');
391   equals(currentMenuItem.get('title'), 'Menu Item', 'page up should move one page up');
392 
393   keyPressOn(menu, SC.Event.KEY_END);
394   currentMenuItem = menu.get('currentMenuItem');
395   equals(currentMenuItem.get('title'), 'Unique Menu Item Class Per Item', 'end should move to the last item');
396 
397   keyPressOn(menu, SC.Event.KEY_HOME);
398   currentMenuItem = menu.get('currentMenuItem');
399   equals(currentMenuItem.get('title'), 'Menu Item', 'home should move to the first item');
400 });
401 
402 test('aria-role attribute', function () {
403   var menuPane, menuItems, normalItem, itemWithCheckBox, separatorItem;
404 
405   SC.run(function () {
406     menuPane = SC.MenuPane.create({
407       layout: { width: 200 },
408       items: items,
409       itemCheckboxKey: 'isChecked'
410     });
411 
412     menuPane.popup();
413   });
414 
415   equals(menuPane.$().attr('role'), 'menu', "menu pane should have role set");
416 
417   menuItems = menuPane.get('menuItemViews');
418   normalItem        = menuItems[0];
419   itemWithCheckBox  = menuItems[1];
420   separatorItem     = menuItems[3];
421 
422   equals(normalItem.$().attr('role'), 'menuitem', "normal menuitem has correct role set");
423   equals(itemWithCheckBox.$().attr('role'), 'menuitemcheckbox', "menuitem with checkbox has correct role set");
424   equals(separatorItem.$().attr('role'), 'separator', "separator menuitem has correct role set");
425   clickOn(menuPane);
426 });
427 
428 test('aria-checked attribute', function () {
429   var menuPane,
430     itemWithCheckBox;
431 
432   SC.run(function () {
433     menuPane = SC.MenuPane.create({
434       layout: { width: 200 },
435       items: items,
436       itemCheckboxKey: 'isChecked'
437     });
438 
439     menuPane.popup();
440   });
441 
442   itemWithCheckBox = menuPane.get('menuItemViews')[1];
443 
444   equals(itemWithCheckBox.$().attr('aria-checked'), "true", "checked menuitem has aria-checked attribute set");
445   clickOn(menuPane);
446 });
447 
448 test('Menu item keys', function () {
449   var menuPane,
450     items,
451     submenu,
452     menuItem;
453 
454   submenu = [
455     { title: 'Submenu item' }
456   ];
457 
458   items = [
459     {
460       TheTitle: 'Menu item',
461       TheValue: 1,
462       TheToolTip: 'Menu tooltip',
463       AmIEnabled: false,
464       MyIcon: 'folder',
465       MyHeight: 50,
466       MySubmenu: submenu,
467       AmIASeparator: false
468     }
469   ];
470 
471   SC.run(function () {
472     menuPane = SC.MenuPane.create({
473       layout: { width: 200 },
474       items: items,
475       itemTitleKey: 'TheTitle',
476       itemValueKey: 'TheValue',
477       itemToolTipKey: 'TheToolTip',
478       itemIsEnabledKey: 'AmIEnabled',
479       itemIconKey: 'MyIcon',
480       itemSubMenuKey: 'MySubmenu',
481       itemSeparatorKey: 'AmIASeparator'
482     });
483 
484     menuPane.popup();
485   });
486 
487   menuItem = menuPane.get('menuItemViews')[0];
488 
489   equals(menuItem.get('title'), 'Menu item');
490   equals(menuItem.get('value'), 1);
491   equals(menuItem.get('toolTip'), 'Menu tooltip');
492   equals(menuItem.get('isEnabled'), false);
493   equals(menuItem.get('icon'), 'folder');
494   ok(SC.kindOf(menuItem.get('subMenu'), SC.MenuPane));
495   equals(menuItem.get('isSeparator'), false);
496   clickOn(menuPane);
497 });
498