/* globals _ */ (function() { 'use strict'; describe('Video Context Menu', function() { var state, openMenu, keyPressEvent, openSubmenuMouse, openSubmenuKeyboard, closeSubmenuMouse, closeSubmenuKeyboard, menu, menuItems, menuSubmenuItem, submenu, submenuItems, overlay, $playButton; openMenu = function() { var $container = $('.video'); $container.find('video').trigger('contextmenu'); menu = $container.children('.contextmenu'); menuItems = menu.children('.menu-item').not('.submenu-item'); menuSubmenuItem = menu.children('.menu-item.submenu-item'); submenu = menuSubmenuItem.children('.submenu'); submenuItems = submenu.children('.menu-item'); overlay = $container.children('.overlay'); $playButton = $('.video_control.play'); }; keyPressEvent = function(key) { return $.Event('keydown', {keyCode: key}); }; // eslint-disable-next-line no-shadow openSubmenuMouse = function(menuSubmenuItem) { menuSubmenuItem.mouseover(); jasmine.clock().tick(200); expect(menuSubmenuItem).toHaveClass('is-opened'); }; // eslint-disable-next-line no-shadow openSubmenuKeyboard = function(menuSubmenuItem, keyCode) { menuSubmenuItem.focus().trigger(keyPressEvent(keyCode || $.ui.keyCode.RIGHT)); expect(menuSubmenuItem).toHaveClass('is-opened'); expect(menuSubmenuItem.children().last().children().first()).toBeFocused(); }; // eslint-disable-next-line no-shadow closeSubmenuMouse = function(menuSubmenuItem) { menuSubmenuItem.mouseleave(); jasmine.clock().tick(200); expect(menuSubmenuItem).not.toHaveClass('is-opened'); }; // eslint-disable-next-line no-shadow closeSubmenuKeyboard = function(menuSubmenuItem) { menuSubmenuItem.children().first().focus().trigger(keyPressEvent($.ui.keyCode.LEFT)); expect(menuSubmenuItem).not.toHaveClass('is-opened'); expect(menuSubmenuItem).toBeFocused(); }; beforeEach(function() { jasmine.clock().install(); // $.cookie is mocked, make sure we have a state with an unmuted volume. $.cookie.and.returnValue('100'); jasmine.addMatchers({ toHaveCorrectLabels: function() { return { compare: function(actual, labelsList) { return { pass: _.difference(labelsList, _.map(actual, function(item) { return $(item).text(); })).length === 0 }; } }; } }); }); afterEach(function() { $('source').remove(); _.result(state.storage, 'clear'); _.result($('video').data('contextmenu'), 'destroy'); _.result(state.videoPlayer, 'destroy'); jasmine.clock().uninstall(); }); describe('constructor', function() { it('the structure should be created on first `contextmenu` call', function() { state = jasmine.initializePlayer(); expect(menu).not.toExist(); openMenu(); /* Make sure we have the expected HTML structure: - Play (Pause) - Mute (Unmute) - Fill browser (Exit full browser) - Speed > - 0.75x - 1.0x - 1.25x - 1.50x */ // Only one context menu per video container expect(menu).toBeInDOM(); expect(menu).toHaveClass('is-opened'); expect(menuItems).toHaveCorrectLabels(['Play', 'Mute', 'Fill browser']); expect(menuSubmenuItem.children('span')).toHaveText('Speed'); expect(submenuItems).toHaveCorrectLabels(['0.75x', '1.0x', '1.25x', '1.50x']); // Check that one of the speed submenu item is selected expect(_.size(submenuItems.filter('.is-selected'))).toBe(1); }); it('add ARIA attributes to menu, menu items, submenu and submenu items', function() { state = jasmine.initializePlayer(); openMenu(); // Menu and its items. expect(menu).toHaveAttr('role', 'menu'); menuItems.each(function() { expect($(this)).toHaveAttrs({ 'aria-selected': 'false', role: 'menuitem' }); }); expect(menuSubmenuItem).toHaveAttrs({ 'aria-expanded': 'false', 'aria-haspopup': 'true', role: 'menuitem' }); // Submenu and its items. expect(submenu).toHaveAttr('role', 'menu'); submenuItems.each(function() { expect($(this)).toHaveAttr('role', 'menuitem'); expect($(this)).toHaveAttr('aria-selected'); }); }); it('is not used by Youtube type of video player', function() { state = jasmine.initializePlayer('video.html'); expect($('video, iframe')).not.toHaveData('contextmenu'); }); }); describe('methods:', function() { beforeEach(function() { state = jasmine.initializePlayer(); openMenu(); }); it('menu can be destroyed successfully', function() { var menuitemEvents = ['click', 'keydown', 'contextmenu', 'mouseover'], menuEvents = ['keydown', 'contextmenu', 'mouseleave', 'mouseover']; menu.data('menu').destroy(); expect(menu).not.toBeInDOM(); expect(overlay).not.toBeInDOM(); _.each(menuitemEvents, function(eventName) { expect(menuItems.first()).not.toHandle(eventName); }); _.each(menuEvents, function(eventName) { expect(menuSubmenuItem).not.toHandle(eventName); }); _.each(menuEvents, function(eventName) { expect(menu).not.toHandle(eventName); }); expect($('video')).not.toHandle('contextmenu'); expect($('video')).not.toHaveData('contextmenu'); }); it('can change label for the submenu', function() { expect(menuSubmenuItem.children('span')).toHaveText('Speed'); menuSubmenuItem.data('menu').setLabel('New Name'); expect(menuSubmenuItem.children('span')).toHaveText('New Name'); }); it('can change label for the menuitem', function() { expect(menuItems.first()).toHaveText('Play'); menuItems.first().data('menu').setLabel('Pause'); expect(menuItems.first()).toHaveText('Pause'); }); }); describe('when video is right-clicked', function() { beforeEach(function() { state = jasmine.initializePlayer(); jasmine.mockFullscreenAPI(); openMenu(); }); it('context menu opens', function() { expect(menu).toHaveClass('is-opened'); expect(overlay).toBeInDOM(); }); it('mouseover and mouseleave behave as expected', function() { openSubmenuMouse(menuSubmenuItem); expect(menuSubmenuItem).toHaveClass('is-opened'); closeSubmenuMouse(menuSubmenuItem); expect(menuSubmenuItem).not.toHaveClass('is-opened'); submenuItems.eq(1).mouseover(); expect(submenuItems.eq(1)).toBeFocused(); }); it('mouse left-clicking outside of the context menu will close it', function() { // Left-click outside of open menu, for example on Play button $playButton.click(); expect(menu).not.toHaveClass('is-opened'); expect(overlay).not.toBeInDOM(); }); it('mouse right-clicking outside of video will close it', function() { // Right-click outside of open menu for example on Play button $playButton.trigger('contextmenu'); expect(menu).not.toHaveClass('is-opened'); expect(overlay).not.toBeInDOM(); }); it('mouse right-clicking inside video but outside of context menu will not close it', function() { spyOn(menu.data('menu'), 'pointInContainerBox').and.returnValue(true); overlay.trigger('contextmenu'); expect(menu).toHaveClass('is-opened'); expect(overlay).toBeInDOM(); }); it('mouse right-clicking inside video but outside of context menu will close submenus', function() { spyOn(menu.data('menu'), 'pointInContainerBox').and.returnValue(true); openSubmenuMouse(menuSubmenuItem); expect(menuSubmenuItem).toHaveClass('is-opened'); overlay.trigger('contextmenu'); expect(menuSubmenuItem).not.toHaveClass('is-opened'); }); it('mouse left/right-clicking behaves as expected on play/pause menu item', function() { var menuItem = menuItems.first(); spyOn(state.videoPlayer, 'isPlaying'); spyOn(state.videoPlayer, 'play').and.callFake(function() { state.videoPlayer.isPlaying.and.returnValue(true); state.el.trigger('play'); }); spyOn(state.videoPlayer, 'pause').and.callFake(function() { state.videoPlayer.isPlaying.and.returnValue(false); state.el.trigger('pause'); }); // Left-click on play menuItem.click(); expect(state.videoPlayer.play).toHaveBeenCalled(); expect(menuItem).toHaveText('Pause'); openMenu(); // Left-click on pause menuItem.click(); expect(state.videoPlayer.pause).toHaveBeenCalled(); expect(menuItem).toHaveText('Play'); state.videoPlayer.play.calls.reset(); // Right-click on play menuItem.trigger('contextmenu'); expect(state.videoPlayer.play).toHaveBeenCalled(); expect(menuItem).toHaveText('Pause'); }); it('mouse left/right-clicking behaves as expected on mute/unmute menu item', function() { var menuItem = menuItems.eq(1); // Left-click on mute menuItem.click(); expect(state.videoVolumeControl.getMuteStatus()).toBe(true); expect(menuItem).toHaveText('Unmute'); openMenu(); // Left-click on unmute menuItem.click(); expect(state.videoVolumeControl.getMuteStatus()).toBe(false); expect(menuItem).toHaveText('Mute'); // Right-click on mute menuItem.trigger('contextmenu'); expect(state.videoVolumeControl.getMuteStatus()).toBe(true); expect(menuItem).toHaveText('Unmute'); openMenu(); // Right-click on unmute menuItem.trigger('contextmenu'); expect(state.videoVolumeControl.getMuteStatus()).toBe(false); expect(menuItem).toHaveText('Mute'); }); it('mouse left/right-clicking behaves as expected on go to Exit full browser menu item', function() { var menuItem = menuItems.eq(2); // Left-click on Fill browser menuItem.click(); expect(state.isFullScreen).toBe(true); expect(menuItem).toHaveText('Exit full browser'); openMenu(); // Left-click on Exit full browser menuItem.click(); expect(state.isFullScreen).toBe(false); expect(menuItem).toHaveText('Fill browser'); // Right-click on Fill browser menuItem.trigger('contextmenu'); expect(state.isFullScreen).toBe(true); expect(menuItem).toHaveText('Exit full browser'); openMenu(); // Right-click on Exit full browser menuItem.trigger('contextmenu'); expect(state.isFullScreen).toBe(false); expect(menuItem).toHaveText('Fill browser'); }); it('mouse left/right-clicking behaves as expected on speed submenu item', function() { // Set speed to 0.75x state.videoSpeedControl.setSpeed('0.75'); // Left-click on second submenu speed (1.0x) openSubmenuMouse(menuSubmenuItem); submenuItems.eq(1).click(); // Expect speed to be 1.0x expect(state.videoSpeedControl.currentSpeed).toBe('1.0'); // Expect speed submenu item 0.75x not to be active expect(submenuItems.first()).not.toHaveClass('is-selected'); // Expect speed submenu item 1.0x to be active expect(submenuItems.eq(1)).toHaveClass('is-selected'); // Set speed to 0.75x state.videoSpeedControl.setSpeed('0.75'); // Right-click on second submenu speed (1.0x) openSubmenuMouse(menuSubmenuItem); submenuItems.eq(1).trigger('contextmenu'); // Expect speed to be 1.0x expect(state.videoSpeedControl.currentSpeed).toBe('1.0'); // Expect speed submenu item 0.75x not to be active expect(submenuItems.first()).not.toHaveClass('is-selected'); // Expect speed submenu item 1.0x to be active expect(submenuItems.eq(1)).toHaveClass('is-selected'); }); }); describe('Keyboard interactions', function() { beforeEach(function() { state = jasmine.initializePlayer(); openMenu(); }); it('focus the first item of the just opened menu on UP keydown', function() { menu.trigger(keyPressEvent($.ui.keyCode.UP)); expect(menuSubmenuItem).toBeFocused(); }); it('focus the last item of the just opened menu on DOWN keydown', function() { menu.trigger(keyPressEvent($.ui.keyCode.DOWN)); expect(menuItems.first()).toBeFocused(); }); it('open the submenu on ENTER keydown', function() { openSubmenuKeyboard(menuSubmenuItem, $.ui.keyCode.ENTER); expect(menuSubmenuItem).toHaveClass('is-opened'); expect(submenuItems.first()).toBeFocused(); }); it('open the submenu on SPACE keydown', function() { openSubmenuKeyboard(menuSubmenuItem, $.ui.keyCode.SPACE); expect(menuSubmenuItem).toHaveClass('is-opened'); expect(submenuItems.first()).toBeFocused(); }); it('open the submenu on RIGHT keydown', function() { openSubmenuKeyboard(menuSubmenuItem, $.ui.keyCode.RIGHT); expect(menuSubmenuItem).toHaveClass('is-opened'); expect(submenuItems.first()).toBeFocused(); }); it('close the menu on ESCAPE keydown', function() { menu.trigger(keyPressEvent($.ui.keyCode.ESCAPE)); expect(menu).not.toHaveClass('is-opened'); expect(overlay).not.toBeInDOM(); }); it('close the submenu on ESCAPE keydown', function() { openSubmenuKeyboard(menuSubmenuItem); menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.ESCAPE)); expect(menuSubmenuItem).not.toHaveClass('is-opened'); expect(overlay).not.toBeInDOM(); }); it('close the submenu on LEFT keydown on submenu items', function() { closeSubmenuKeyboard(menuSubmenuItem); }); it('do nothing on RIGHT keydown on submenu item', function() { submenuItems.eq(1).focus().trigger(keyPressEvent($.ui.keyCode.RIGHT)); // Mute // Is still focused. expect(submenuItems.eq(1)).toBeFocused(); }); it('do nothing on TAB keydown on menu item', function() { submenuItems.eq(1).focus().trigger(keyPressEvent($.ui.keyCode.TAB)); // Mute // Is still focused. expect(submenuItems.eq(1)).toBeFocused(); }); it('UP and DOWN keydown function as expected on menu/submenu items', function() { menuItems.eq(0).focus(); // Play expect(menuItems.eq(0)).toBeFocused(); menuItems.eq(0).trigger(keyPressEvent($.ui.keyCode.DOWN)); expect(menuItems.eq(1)).toBeFocused(); // Mute menuItems.eq(1).trigger(keyPressEvent($.ui.keyCode.DOWN)); expect(menuItems.eq(2)).toBeFocused(); // Fullscreen menuItems.eq(2).trigger(keyPressEvent($.ui.keyCode.DOWN)); expect(menuSubmenuItem).toBeFocused(); // Speed menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.DOWN)); expect(menuItems.eq(0)).toBeFocused(); // Play menuItems.eq(0).trigger(keyPressEvent($.ui.keyCode.UP)); expect(menuSubmenuItem).toBeFocused(); // Speed // Check if hidden item can be skipped correctly. menuItems.eq(2).hide(); // hide Fullscreen item menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.UP)); expect(menuItems.eq(1)).toBeFocused(); // Mute menuItems.eq(1).trigger(keyPressEvent($.ui.keyCode.UP)); expect(menuItems.eq(0)).toBeFocused(); // Play }); it('current item is still focused if all siblings are hidden', function() { menuItems.eq(0).focus(); // Play expect(menuItems.eq(0)).toBeFocused(); // hide all siblings menuItems.eq(0).siblings().hide(); menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.DOWN)); expect(menuItems.eq(0)).toBeFocused(); menuSubmenuItem.trigger(keyPressEvent($.ui.keyCode.UP)); expect(menuItems.eq(0)).toBeFocused(); }); it('ENTER keydown on menu/submenu item selects its data and closes menu', function() { menuItems.eq(2).focus().trigger(keyPressEvent($.ui.keyCode.ENTER)); // Fullscreen expect(menuItems.eq(2)).toHaveClass('is-selected'); expect(menuItems.eq(2).siblings()).not.toHaveClass('is-selected'); expect(state.isFullScreen).toBeTruthy(); expect(menuItems.eq(2)).toHaveText('Exit full browser'); }); it('SPACE keydown on menu/submenu item selects its data and closes menu', function() { submenuItems.eq(2).focus().trigger(keyPressEvent($.ui.keyCode.SPACE)); // 1.25x expect(submenuItems.eq(2)).toHaveClass('is-selected'); expect(submenuItems.eq(2).siblings()).not.toHaveClass('is-selected'); expect(state.videoSpeedControl.currentSpeed).toBe('1.25'); }); }); }); }());