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