UNPKG

ui-contextmenu

Version:

Turn a jQuery UI Menu widget into a contextmenu.

667 lines (580 loc) 21.2 kB
// jQUnit defines: // asyncTest,deepEqual,equal,expect,module,notDeepEqual,notEqual,notStrictEqual, // ok,QUnit,raises,start,stop,strictEqual,test /*globals QUnit */ /** * Tools inspired by https://github.com/jquery/jquery-ui/blob/master/tests/unit/menu/ */ function TestHelpers() { var lastItem = "", log = [], $ = jQuery, match = $.ui.menu.version.match(/^(\d)\.(\d+)/), uiVersion = { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) }, uiVersionBefore11 = ( uiVersion.major < 2 && uiVersion.minor < 11 ), uiVersionBefore12 = ( uiVersion.major < 2 && uiVersion.minor < 12 ), findEntry = function( menu, indexOrCommand ) { if ( typeof indexOrCommand === "number" ) { return menu.children( ":eq(" + indexOrCommand + ")" ); } return menu.find("li[data-command=" + indexOrCommand + "]"); }, findEntryInner = function( menu, indexOrCommand ) { if ( uiVersionBefore11 ) { // jQuery UI <= 1.10 used `<a>` tags return findEntry(menu, indexOrCommand).find( "a:first" ); } else if ( uiVersionBefore12 ) { // jQuery UI == 1.11 prefered to avoid `<a>` tags return findEntry(menu, indexOrCommand); } else { // jQuery UI 1.12+ introduced `<div>` wrappers return findEntry(menu, indexOrCommand).find( ">div:first" ); // return findEntry(menu, indexOrCommand).children( ".ui-menu-item-wrapper" ); } }; return { log: function( message, clear ) { if ( clear ) { log.length = 0; } if ( message === undefined ) { message = lastItem; } // window.console.log(message); log.push( $.trim( message ) ); }, logOutput: function() { return log.join( "," ); }, clearLog: function() { log.length = 0; }, entryEvent: function( menu, item, type ) { lastItem = item; findEntryInner(menu, item).trigger( type ); }, click: function( menu, item ) { lastItem = item; // console.log("click", menu, item, findEntryInner(menu, item)); findEntryInner(menu, item).trigger( "click" ); }, entry: findEntry, entryTitle: function( menu, item ) { // return the plain text (without sub-elements) var ei = findEntryInner(menu, item); if ( !ei || !ei.length ) { return null; } return ei.contents().filter(function() { return this.nodeType === 3; })[0].nodeValue; } }; } // **************************************************************************** jQuery(document).ready(function() { /******************************************************************************* * QUnit setup */ QUnit.config.requireExpects = true; var th = new TestHelpers(), $ = jQuery, log = th.log, logOutput = th.logOutput, click = th.click, entryEvent = th.entryEvent, entryTitle = th.entryTitle, entry = th.entry, lifecycle = { setup: function() { th.clearLog(); // Always create a fresh copy of the menu <UL> definition $("#sampleMenuTemplate").clone().attr("id", "sampleMenu").appendTo("body"); }, teardown: function() { $(":moogle-contextmenu").contextmenu("destroy"); $("#sampleMenu").remove(); } }, SAMPLE_MENU = [ { title: "Cut", cmd: "cut", uiIcon: "ui-icon-scissors" }, { title: "Copy", cmd: "copy", uiIcon: "ui-icon-copy" }, { title: "Paste", cmd: "paste", uiIcon: "ui-icon-clipboard", disabled: true }, { title: "----" }, { title: "More", children: [ { title: "Sub Item 1", cmd: "sub1" }, { title: "Sub Item 2", cmd: "sub2" } ] } ], sauceLabsLog = []; // SauceLabs integration QUnit.testStart(function(testDetails) { QUnit.log(function(details) { if (!details.result) { details.name = testDetails.name; sauceLabsLog.push(details); } }); }); QUnit.done(function(testResults) { var tests = [], i, len, details; for (i = 0, len = sauceLabsLog.length; i < len; i++) { details = sauceLabsLog[i]; tests.push({ name: details.name, result: details.result, expected: details.expected, actual: details.actual, source: details.source }); } testResults.tests = tests; /*jshint camelcase:false*/ // jscs: disable window.global_test_results = testResults; // used by saucelabs /*jshint camelcase:true*/ // jscs: enable }); //--------------------------------------------------------------------------- QUnit.module("prototype", lifecycle); QUnit.test("globals", function(assert) { assert.expect(2); assert.ok( !!$.moogle.contextmenu, "exists in ui namnespace"); assert.ok( !!$.moogle.contextmenu.version, "has version number"); }); // --------------------------------------------------------------------------- QUnit.module("create", lifecycle); function _createTest(menu, assert) { var $ctx; assert.expect(5); log( "constructor"); $("#container").contextmenu({ delegate: ".hasmenu", menu: menu, preventSelect: true, create: function() { log("create"); }, createMenu: function() { log("createMenu"); } }); log( "afterConstructor"); $ctx = $(":moogle-contextmenu"); assert.equal( $ctx.length, 1, "widget created"); // equal( $("#sampleMenu").hasClass( "ui-contextmenu" ), true, // "Class set to menu definition"); assert.equal( $("head style.moogle-contextmenu-style").length, 1, "global stylesheet created"); $ctx.contextmenu("destroy"); assert.equal( $(":moogle-contextmenu").length, 0, "widget destroyed"); // equal( $("#sampleMenu").hasClass("ui-contextmenu"), false, // "Class removed from menu definition"); assert.equal( $("head style.moogle-contextmenu-style").length, 0, "global stylesheet removed"); assert.equal(logOutput(), "constructor,createMenu,create,afterConstructor", "Event sequence OK." ); } QUnit.test("create from UL", function(assert) { _createTest("ul#sampleMenu", assert); }); QUnit.test("create from array", function(assert) { _createTest(SAMPLE_MENU, assert); }); //--------------------------------------------------------------------------- QUnit.module("open", lifecycle); function _openTest(menu, assert) { var $ctx, $popup, done = assert.async(); assert.expect(19); $("#container").contextmenu({ delegate: ".hasmenu", menu: menu, beforeOpen: function(event, ui) { log("beforeOpen"); assert.equal( event.type, "contextmenubeforeopen", "beforeOpen: Got contextmenubeforeopen event" ); assert.equal( ui.target.text(), "AAA", "beforeOpen: ui.target is set" ); assert.ok( $popup.is(":hidden"), "beforeOpen: Menu is hidden" ); assert.ok( !entry($popup, 0).hasClass("ui-state-disabled"), "beforeOpen: Entry 0 is enabled" ); assert.ok( entry($popup, 2).hasClass("ui-state-disabled"), "beforeOpen: Entry 2 is disabled" ); assert.ok($ctx.contextmenu("isOpen"), "isOpen() false in beforeOpen event"); $("#container").contextmenu("enableEntry", "cut", false); $("#container").contextmenu("showEntry", "copy", false); }, open: function(event) { log("open"); assert.ok( $popup.is(":visible"), "open: Menu is visible" ); assert.ok( $popup.hasClass("ui-contextmenu"), "Class removed from menu definition"); assert.ok( entry($popup, 2).hasClass("ui-state-disabled"), "open: Entry is disabled" ); assert.ok( $ctx.contextmenu("isOpen"), "isOpen() true in open event"); assert.ok( entry($popup, 0).is(":visible"), "beforeOpen: Entry 0 is visible" ); assert.ok( entry($popup, 0).hasClass("ui-state-disabled"), "beforeOpen: Entry 0 is disabled: enableEntry(false) worked" ); assert.ok( entry($popup, 1).is(":hidden"), "beforeOpen: Entry 1 is hidden: showEntry(false) worked" ); assert.ok( !entry($popup, 1).hasClass("ui-state-disabled"), "beforeOpen: Entry 1 is enabled" ); assert.equal(logOutput(), "open(),beforeOpen,after open(),open", "Event sequence OK."); done(); } }); $ctx = $(":moogle-contextmenu"); $popup = $ctx.contextmenu("getMenu"); assert.ok($popup, "getMenu() works"); assert.ok(!$ctx.contextmenu("isOpen"), "menu initially closed"); assert.equal( $ctx.length, 1, "widget created"); assert.ok($popup.is(":hidden"), "Menu is hidden"); log("open()"); $ctx.contextmenu("open", $("span.hasmenu:first")); log("after open()"); } QUnit.test("UL menu", function(assert) { _openTest("ul#sampleMenu", assert); }); QUnit.test("Array menu", function(assert) { _openTest(SAMPLE_MENU, assert); }); //--------------------------------------------------------------------------- QUnit.module("click event sequence", lifecycle); function _clickTest(menu, assert) { var $ctx, $popup, done = assert.async(); assert.expect(13); $("#container").contextmenu({ delegate: ".hasmenu", menu: menu, // show: false, // hide: false, beforeOpen: function(event, ui) { log("beforeOpen(" + ui.target.text() + ")"); assert.equal( ui.target.text(), "AAA", "beforeOpen: ui.target is set" ); assert.equal( ui.extraData.foo, "bar", "beforeOpen: ui.extraData is set" ); ui.extraData.helloFromBO = true; }, create: function(event, ui) { log("create"); }, createMenu: function(event, ui) { log("createMenu"); }, /*TODO: Seems that focus gets called twice in Safary, but not PhantomJS */ // focus: function(event, ui) { // var t = ui.item ? $(ui.item).find("a:first").attr("href") : ui.item; // log("focus(" + t + ")"); //// equal( ui.cmd, "cut", "focus: ui.cmd is set" ); //// ok( !ui.target || ui.target.text() === "AAA", "focus: ui.target is set" ); // }, // /* blur seems always to have ui.item === null. Also called twice in Safari? */ // blur: function(event, ui) { // var t = ui.item ? $(ui.item).find("a:first").attr("href") : ui.item; // log("blur(" + t + ")"); //// equal( ui.cmd, "cut", "blur: ui.cmd is set" ); //// equal( ui.target && ui.target.text(), "AAA", "blur: ui.target is set" ); // }, select: function(event, ui) { // window.console.log("select"); var t = ui.item ? $(ui.item).attr("data-command") : ui.item; log("select(" + t + ")"); assert.equal( ui.cmd, "cut", "select: ui.cmd is set" ); assert.equal( ui.target.text(), "AAA", "select: ui.target is set" ); assert.equal( ui.extraData.foo, "bar", "select: ui.extraData is set" ); assert.equal( ui.extraData.helloFromBO, true, "select: ui.extraData is maintained" ); }, open: function(event, ui) { log("open"); assert.equal( ui.target.text(), "AAA", "open: ui.target is set" ); assert.equal( ui.extraData.foo, "bar", "open: ui.extraData is set" ); assert.equal( ui.extraData.helloFromBO, true, "open: ui.extraData is maintained" ); setTimeout(function() { entryEvent($popup, 0, "mouseenter"); click($popup, 0); }, 10); }, close: function(event, ui) { log("close"); assert.equal( ui.target.text(), "AAA", "close: ui.target is set" ); assert.equal( ui.extraData.foo, "bar", "close: ui.extraData is set" ); assert.equal( ui.extraData.helloFromBO, true, "close: ui.extraData is maintained" ); assert.equal(logOutput(), "createMenu,create,open(),beforeOpen(AAA),after open(),open,select(cut),close", "Event sequence OK."); done(); } }); $ctx = $(":moogle-contextmenu"); $popup = $ctx.contextmenu("getMenu"); log("open()"); $ctx.contextmenu("open", $("span.hasmenu:first"), { foo: "bar" }); log("after open()"); } QUnit.test("Array menu", function(assert) { _clickTest(SAMPLE_MENU, assert); }); QUnit.test("UL menu", function(assert) { _clickTest("ul#sampleMenu", assert); }); // **************************************************************************** QUnit.module("Dynmic options", lifecycle); QUnit.test("'action' option", function(assert) { var $ctx, $popup, menu = [ { title: "Cut", cmd: "cut", uiIcon: "ui-icon-scissors", data: { foo: "bar" }, addClass: "custom-class-1", action: function(event, ui) { log("cut action"); assert.equal( ui.cmd, "cut", "action: ui.cmd is set" ); assert.equal( ui.target.text(), "AAA", "action: ui.target is set" ); assert.equal( ui.item.data().foo, "bar", "action: ui.item.data() is set" ); assert.ok( ui.item.hasClass("custom-class-1"), "action: addClass property works" ); } }, { title: "Copy", cmd: "copy", uiIcon: "ui-icon-copy" }, { title: "Paste", cmd: "paste", uiIcon: "ui-icon-clipboard", disabled: true } ], done = assert.async(); assert.expect(9); $("#container").contextmenu({ delegate: ".hasmenu", menu: menu, open: function(event) { log("open"); setTimeout(function() { click($popup, 0); }, 10); }, select: function(event, ui) { var t = ui.item ? $(ui.item).attr("data-command") : ui.item; log("select(" + t + ")"); assert.equal( ui.cmd, "cut", "select: ui.cmd is set" ); assert.equal( ui.target.text(), "AAA", "select: ui.target is set" ); assert.equal( ui.item.data().foo, "bar", "ui.item.data() is set" ); assert.ok( ui.item.hasClass("custom-class-1"), "addClass property works" ); }, close: function(event) { log("close"); assert.equal(logOutput(), "open(),after open(),open,select(cut),cut action,close", "Event sequence OK."); done(); } }); $ctx = $(":moogle-contextmenu"); $popup = $ctx.contextmenu("getMenu"); log("open()"); $ctx.contextmenu("open", $("span.hasmenu:first")); log("after open()"); }); QUnit.test("'tooltip' / 'disabled' options", function(assert) { var $ctx, $popup, menu = [ { title: "Cut", cmd: "cut", tooltip: function(event, ui) { log("tooltip(cut)"); assert.equal( ui.cmd, "cut", "ui.cmd is set" ); assert.equal( ui.target.text(), "AAA", "ui.target is set" ); assert.equal( ui.item.text(), "Cut", "ui.item is set" ); return "dynamic tt"; } }, { title: "Copy", cmd: "copy", tooltip: "static tt" }, { title: "Paste", cmd: "paste", disabled: true }, { title: "Delete", cmd: "delete", disabled: function(event, ui) { log("disabled(delete)"); return false; } }, { title: "Edit", cmd: "edit", disabled: function(event, ui) { log("disabled(edit)"); return true; } }, { title: "Hidden", cmd: "hidden", disabled: function(event, ui) { log("disabled(hidden)"); return "hide"; } } ], done = assert.async(); assert.expect(12); $("#container").contextmenu({ delegate: ".hasmenu", menu: menu, open: function(event) { log("open"); assert.equal(entry($popup, "cut").attr("title"), "dynamic tt", "tooltip callback result was used"); assert.equal(entry($popup, "copy").attr("title"), "static tt", "static tooltip value was used"); assert.equal(entry($popup, "paste").hasClass("ui-state-disabled"), true, "static disabled value was used"); assert.equal(entry($popup, "delete").hasClass("ui-state-disabled"), false, "dynamic disabled value 'false' was used"); assert.ok(entry($popup, "delete").is(":visible"), "dynamic disabled value 'false' does not hide"); assert.equal(entry($popup, "paste").hasClass("ui-state-disabled"), true, "dynamic disabled value 'true' was used"); assert.ok(entry($popup, "delete").is(":visible"), "dynamic disabled value 'true' does not hide"); assert.ok(entry($popup, "hidden").is(":hidden"), "dynamic disabled value 'hide' was used"); // Click s.th. to close the menu setTimeout(function() { click($popup, 0); }, 10); }, close: function(event) { log("close"); assert.equal(logOutput(), "open(),tooltip(cut),disabled(delete),disabled(edit),disabled(hidden)," + "after open(),open,close", "Event sequence OK."); done(); } }); $ctx = $(":moogle-contextmenu"); $popup = $ctx.contextmenu("getMenu"); log("open()"); $ctx.contextmenu("open", $("span.hasmenu:first")); log("after open()"); }); // **************************************************************************** QUnit.module("'beforeOpen' event", lifecycle); QUnit.test("modify on open", function(assert) { var $ctx, $popup, menu = [ { title: "Entry 1", cmd: "e1", uiIcon: "ui-icon-copy" }, { title: "Entry 2", cmd: "e2", uiIcon: "ui-icon-copy" }, { title: "Entry 3", cmd: "e3", uiIcon: "ui-icon-copy" }, { title: "Entry 4", cmd: "e4", uiIcon: "ui-icon-copy" }, { title: "Entry 5", cmd: "e5", uiIcon: "ui-icon-copy" }, { title: "Entry 6", cmd: "e6", uiIcon: "ui-icon-copy" }, { title: "Entry 7", cmd: "e7", uiIcon: "ui-icon-copy" }, { title: "Entry 8", cmd: "e8", uiIcon: "ui-icon-copy" }, { title: "Entry 9", cmd: "e9", uiIcon: "ui-icon-copy" } ], done = assert.async(); assert.expect(29); $("#container").contextmenu({ delegate: ".hasmenu", menu: menu, beforeOpen: function(event, ui) { log("beforeOpen"); $ctx .contextmenu("setTitle", "e1", "Entry 1 - changed") .contextmenu("setEntry", "e2", { uiIcon: "ui-icon-changed" }) .contextmenu("setEntry", "e3", { title: "Entry 3 - changed", cmd: "e3", children: [ { title: "Sub 1", cmd: "e3_1" }, { title: "Sub 2", cmd: "e3_2", disabled: true } ] } ) .contextmenu("setEntry", "e4", { title: "Entry 4 - changed", cmd: "e4_changed" }) .contextmenu("updateEntry", "e5", { uiIcon: "ui-icon-changed" }); }, open: function(event) { log("open"); // setTitle() assert.equal(entryTitle($popup, "e1"), "Entry 1 - changed", "setTitle(string) changed title"); assert.equal(entry($popup, "e1").find("span.ui-icon").length, 1, "setTitle(string) keeps existing icon"); // setEntry() with icon assert.equal(entry($popup, "e2").find("span.ui-icon-changed").length, 1, "setEntry(<uiIcon>) sets icon"); assert.equal(entryTitle($popup, "e2"), "undefined", "setEntry(<uiIcon>) resets title"); // setEntry() with new sub-elements assert.equal(entryTitle($popup, "e3"), "Entry 3 - changed", "setEntry(object) set nested title"); assert.ok( !entry($popup, "e3").hasClass("ui-state-disabled"), "setEntry(object) has reset 'disabled' attribute"); assert.equal(entryTitle($popup, "e3_1"), "Sub 1", "setEntry(object) created nested entry"); assert.ok(entry($popup, "e3_2").hasClass("ui-state-disabled"), "setEntry(object) created nested disabled entry"); // setEntry() with different CMD assert.equal(entry($popup, "e4").length, 0, "setEntry(object) change command id (old is gone)"); assert.equal(entry($popup, "e4_changed").length, 1, "setEntry(object) change command id (new is set)"); assert.equal(entryTitle($popup, "e4_changed"), "Entry 4 - changed", "setEntry(object) set title"); // updateEntry() with icon assert.equal(entry($popup, "e5").find("span.ui-icon-changed").length, 1, "updateEntry(<uiIcon>) sets icon"); assert.equal(entryTitle($popup, "e5"), "Entry 5", "updateEntry(<uiIcon>) keeps title"); // Some more on-the-fly modifications assert.ok( !entry($popup, "e9").is(":hidden"), "Entry 9 is visible" ); assert.ok( !entry($popup, "e9").hasClass("ui-state-disabled"), "Entry 9 is enabled" ); $ctx.contextmenu("enableEntry", "e9", false); assert.ok( entry($popup, "e9").hasClass("ui-state-disabled"), "enableEntry(false)" ); $ctx.contextmenu("setIcon", "e9", "ui-icon-changed"); assert.equal(entry($popup, "e9").find("span.ui-icon-changed").length, 1, "setIcon()"); $ctx.contextmenu("setTitle", "e9", "Entry 9 - changed"); assert.equal(entryTitle($popup, "e9"), "Entry 9 - changed", "setTitle()"); $ctx.contextmenu("showEntry", "e9", false); assert.ok( entry($popup, "e9").is(":hidden"), "showEntry(false)" ); // Use updateEntry() $ctx.contextmenu("updateEntry", "e9", { title: "Entry 9 - updated", uiIcon: "ui-icon-updated", tooltip: "tooltip updated", disabled: false, hide: false, data: { foo: "bar" }, setClass: "custom-class" }); assert.ok( !entry($popup, "e9").hasClass("ui-state-disabled"), "updateEntry(disabled: false)" ); assert.equal(entry($popup, "e9").find("span.ui-icon-updated").length, 1, "updateEntry(icon)"); assert.ok( !entry($popup, "e9").is(":hidden"), "updateEntry(hide: false)" ); assert.equal(entryTitle($popup, "e9"), "Entry 9 - updated", "updateEntry(title)"); assert.equal(entry($popup, "e9").attr("title"), "tooltip updated", "updateEntry(tooltip)"); assert.equal(entry($popup, "e9").data().foo, "bar", "updateEntry(data)"); assert.ok(entry($popup, "e9").is(".ui-menu-item.custom-class"), "updateEntry(setClass)"); setTimeout(function() { click($popup, "e1"); }, 10); }, select: function(event, ui) { var t = ui.item ? $(ui.item).attr("data-command") : ui.item; log("select(" + t + ")"); assert.equal( ui.cmd, "e1", "select: ui.cmd is set" ); assert.equal( ui.target.text(), "AAA", "select: ui.target is set" ); }, close: function(event) { log("close"); assert.equal(logOutput(), "open(),beforeOpen,after open(),open,select(e1),close", "Event sequence OK."); done(); } }); $ctx = $(":moogle-contextmenu"); $popup = $ctx.contextmenu("getMenu"); log("open()"); $ctx.contextmenu("open", $("span.hasmenu:first")); log("after open()"); }); });