diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 37e2a3a01a27c09b324110e2671851b781b67bd8..8b23ff1b3a6ffce815152264157a5276596c481c 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -271,38 +271,36 @@ div.steps { font-weight: normal; } -#manage .buttonrow { - margin: 72px 0 14px; +#manage section { + margin-bottom: 20px; +} + +.buttonrow { + margin: 0 0 14px; } -#manage .buttonrow button { +.buttonrow > h2 { + display: inline-block; + font-size: 1em; +} + +#manage button { line-height: 20px; height: 24px; - width: 48px; font-size: 12px; } -#manageAccounts { - background-color: #37A6FF; - border: 1px solid #37A6FF; - text-shadow: -1px -1px 0 #37A6FF; - cursor: pointer; - - -webkit-box-shadow: 0 0 0 1px #76C2FF inset; - -moz-box-shadow: 0 0 0 1px #76C2FF inset; - -o-box-shadow: 0 0 0 1px #76C2FF inset; - box-shadow: 0 0 0 1px #76C2FF inset; - - background-image: -moz-linear-gradient(#76C2FF 0pt, #37A6FF 100%); - background-image: -o-linear-gradient(#76C2FF 0pt, #37A6FF 100%); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #76C2FF), color-stop(100%, #37A6FF)); +.buttonrow > button { + width: 48px; } -.edit #manageAccounts { +.buttonrow > .edit { } + +.edit .buttonrow > .edit { display: none; } -#cancelManage { +.buttonrow > .done { display: none; background-color: #006EC6; border: 1px solid #003E70; @@ -319,13 +317,12 @@ div.steps { background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3AA7FF), color-stop(100%, #006EC6)); } -.edit #cancelManage { +.edit .buttonrow > .done { display: inline-block; } #manage #emailList { list-style-type: none; - margin: 0 0 72px 0; border-top: 1px solid #eee; } @@ -376,10 +373,7 @@ div.steps { button.delete { background-color: #EA7676; - height: 24px; - vertical-align: middle; border: 1px solid #B13D3D; - font-size: 12px; font-weight: bold; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #fff; @@ -417,6 +411,32 @@ button.delete:active { background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #C84343), color-stop(100%, #AA3D3D)); } +#edit_password { + margin-bottom: 10px; +} + +#edit_password label { + width: 40%; + display: inline-block; +} + +#edit_password input[type=password] { + width: 40%; +} + +.showedit { + opacity: 0; + -webkit-transition: all 500ms; + -moz-transition: all 500ms; + -ms-transition: all 500ms; + -o-transition: all 500ms; + transition: all 500ms; +} + +.edit .showedit { + opacity: 1; +} + #disclaimer { color: #888; text-align: right; @@ -683,7 +703,7 @@ a.forgot { } -header { +#wrapper > header { position: absolute; top: 0; font-weight: bold; @@ -725,7 +745,7 @@ header a.signIn:hover, header a.signOut:hover { display: inline; } -header, +#wrapper > header, footer { display: block; width: 100%; diff --git a/resources/static/pages/manage_account.js b/resources/static/pages/manage_account.js index c606a69f42ade1ae55adbe5bfadcec9a25140243..0aa3a655f9ec8f5614dbf16a19b0d263cc30347d 100644 --- a/resources/static/pages/manage_account.js +++ b/resources/static/pages/manage_account.js @@ -45,7 +45,8 @@ BrowserID.manageAccount = (function() { pageHelpers = bid.PageHelpers, cancelEvent = pageHelpers.cancelEvent, confirmAction = confirm, - doc = document; + doc = document, + tooltip = bid.Tooltip; function relativeDate(date) { var diff = (((new Date()).getTime() - date.getTime()) / 1000), @@ -211,12 +212,46 @@ BrowserID.manageAccount = (function() { } } - function manageAccounts() { - $("body").addClass("edit"); + function startEdit(event) { + // XXX add some helpers in the dom library to find section. + event.preventDefault(); + $(event.target).closest("section").addClass("edit"); } - function cancelManage() { - $("body").removeClass("edit"); + function cancelEdit(event) { + event.preventDefault(); + $(event.target).closest("section").removeClass("edit"); + } + + function changePassword(oncomplete) { + var oldPassword = dom.getInner("#old_password"), + newPassword = dom.getInner("#new_password"); + + function complete(status) { + typeof oncomplete == "function" && oncomplete(status); + } + + if(!oldPassword) { + tooltip.showTooltip("#tooltipOldRequired"); + complete(false); + } + else if(!newPassword) { + tooltip.showTooltip("#tooltipNewRequired"); + complete(false); + } + else { + user.changePassword(oldPassword, newPassword, function(status) { + if(status) { + dom.removeClass("#edit_password", "edit"); + } + else { + tooltip.showTooltip("#tooltipInvalidPassword"); + } + + complete(status); + }, pageHelpers.getFailure(errors.updatePassword, oncomplete)); + } + } function displayHelpTextToNewUser() { @@ -233,8 +268,10 @@ BrowserID.manageAccount = (function() { if (options.confirm) confirmAction = options.confirm; dom.bindEvent("#cancelAccount", "click", cancelEvent(cancelAccount)); - dom.bindEvent("#manageAccounts", "click", cancelEvent(manageAccounts)); - dom.bindEvent("#cancelManage", "click", cancelEvent(cancelManage)); + + dom.bindEvent("button.edit", "click", startEdit); + dom.bindEvent("button.done", "click", cancelEdit); + dom.bindEvent("#edit_password_form", "submit", cancelEvent(changePassword)); syncAndDisplayEmails(oncomplete); @@ -244,13 +281,16 @@ BrowserID.manageAccount = (function() { // BEGIN TESTING API function reset() { dom.unbindEvent("#cancelAccount", "click"); - dom.unbindEvent("#manageAccounts", "click"); - dom.unbindEvent("#cancelManage", "click"); + + dom.unbindEvent("button.edit", "click"); + dom.unbindEvent("button.done", "click"); + dom.unbindEvent("#edit_password_form", "submit"); } init.reset = reset; init.cancelAccount = cancelAccount; init.removeEmail = removeEmail; + init.changePassword = changePassword; // END TESTING API return init; diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js index c18b2817c54e3c336841f8bde097c31995ea95e2..e024ee1750279d617dedb8d3165a68403cb35f59 100644 --- a/resources/static/pages/page_helpers.js +++ b/resources/static/pages/page_helpers.js @@ -94,7 +94,7 @@ BrowserID.PageHelpers = (function() { $("#errorBackground").stop().fadeIn(); $("#error").stop().fadeIn(); - callback && callback(); + callback && callback(false); } } diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js index f1e6cbb681922c7fce6ce5f9978b0a3094b34fa8..340af7f66b437e009941f38af6e3e7ac0c87304f 100644 --- a/resources/static/shared/error-messages.js +++ b/resources/static/shared/error-messages.js @@ -119,6 +119,10 @@ BrowserID.Errors = (function(){ title: "Sync Keys for Address" }, + updatePassword: { + title: "Updating password" + }, + xhrError: { title: "Communication Error" } diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index d15d8d345f41f2834949b6f78bbe1bd859fe35f8..305084129c1311f78e7bedbfddf79af7b2805ba2 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -120,7 +120,7 @@ BrowserID.Network = (function() { else { var url = "/wsapi/session_context"; xhr.ajax({ - url: "/wsapi/session_context", + url: url, success: function(result) { csrf_token = result.csrf_token; server_time = { @@ -379,15 +379,22 @@ BrowserID.Network = (function() { * @method changePassword * @param {string} oldpassword - old password. * @param {string} newpassword - new password. - * @param {function} [onSuccess] - Callback to call when complete. Will be + * @param {function} [onComplete] - Callback to call when complete. Will be * called with true if successful, false otw. * @param {function} [onFailure] - Called on XHR failure. */ - changePassword: function(oldPassword, newPassword, onSuccess, onFailure) { - // XXX fill this in - if (onSuccess) { - onSuccess(true); - } + changePassword: function(oldPassword, newPassword, onComplete, onFailure) { + post({ + url: "/wsapi/update_password", + data: { + oldpass: oldPassword, + newpass: newPassword + }, + success: function(response) { + if (onComplete) onComplete(response.success); + }, + error: onFailure + }); }, @@ -420,8 +427,8 @@ BrowserID.Network = (function() { email: email, site: origin }, - success: function(status) { - if (onSuccess) onSuccess(status.success); + success: function(response) { + if (onSuccess) onSuccess(response.success); }, error: function(info) { // 403 is throttling. diff --git a/resources/static/shared/tooltip.js b/resources/static/shared/tooltip.js index eb512c0f90e5dbcf75d4a7f9412378640c4ee854..14d21932fee721cc290077c1f3eff982e2b52376 100644 --- a/resources/static/shared/tooltip.js +++ b/resources/static/shared/tooltip.js @@ -44,7 +44,7 @@ BrowserID.Tooltip = (function() { bid = BrowserID, dom = bid.DOM, renderer = bid.Renderer, - lastTooltip; + hideTimer; function createTooltip(el) { var contents = el.html(); @@ -74,7 +74,7 @@ BrowserID.Tooltip = (function() { bid.Tooltip.shown = true; el.fadeIn(ANIMATION_TIME, function() { - setTimeout(function() { + hideTimer = setTimeout(function() { el.fadeOut(ANIMATION_TIME, function() { bid.Tooltip.shown = false; if(complete) complete(); @@ -118,7 +118,19 @@ BrowserID.Tooltip = (function() { return { - showTooltip: showTooltip + showTooltip: showTooltip + // BEGIN TESTING API + , + reset: function() { + if(hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + + $(".tooltip").hide(); + bid.Tooltip.shown = false; + } + // END TESTING API }; }()); diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index c093b48de9e41c4f2bc194a0af9b4d21806cdd01..ade50c2718769eca3224a6146c4557069c844255 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -322,7 +322,7 @@ BrowserID.User = (function() { }, /** - * Set the password of the current user. + * Set the initial password of the current user. * @method setPassword * @param {string} password - password to set * @param {function} [onComplete] - Called on successful completion. @@ -332,6 +332,20 @@ BrowserID.User = (function() { network.setPassword(password, onComplete, onFailure); }, + /** + * update the password of the current user. + * @method changePassword + * @param {string} oldpassword - the old password. + * @param {string} newpassword - the new password. + * @param {function} [onComplete] - called on completion. Called with one + * parameter, status - set to true if password update is successful, false + * otw. + * @param {function} [onFailure] - called on XHR failure. + */ + changePassword: function(oldpassword, newpassword, onComplete, onFailure) { + network.changePassword(oldpassword, newpassword, onComplete, onFailure); + }, + /** * Request a password reset for the given email address. * @method requestPasswordReset diff --git a/resources/static/test/index.html b/resources/static/test/index.html index 192c9d03d66c68b00953b0d2d80cdcbf2d3ecbac..6758da3193e71ad8178756de8a1924755330347a 100644 --- a/resources/static/test/index.html +++ b/resources/static/test/index.html @@ -40,6 +40,8 @@ <input id="newEmail" /> <input id="password" /> <input id="vpassword" /> + <input id="old_password" /> + <input id="new_password" /> <input type="checkbox" id="remember" /> </div> <div id="congrats">Congrats!</div> diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js index a65e6e5bfd0a89c83b719b86e4dc151e70421679..8593376f06d2985ee2954f628d95520cd2b864e2 100644 --- a/resources/static/test/qunit/mocks/xhr.js +++ b/resources/static/test/qunit/mocks/xhr.js @@ -56,14 +56,8 @@ BrowserID.Mocks.xhr = (function() { var xhr = { results: { "get /wsapi/session_context valid": contextInfo, - "get /wsapi/session_context invalid": contextInfo, // We are going to test for XHR failures for session_context using - // call to serverTime. We are going to use the flag contextAjaxError - "get /wsapi/session_context ajaxError": contextInfo, - "get /wsapi/session_context complete": contextInfo, - "get /wsapi/session_context throttle": contextInfo, - "get /wsapi/session_context multiple": contextInfo, - "get /wsapi/session_context no_identities": contextInfo, + // the flag contextAjaxError. "get /wsapi/session_context contextAjaxError": undefined, "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" }, "get /wsapi/email_for_token?token=token invalid": { success: false }, @@ -115,7 +109,10 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/list_emails no_identities": [], "get /wsapi/list_emails ajaxError": undefined, // Used in conjunction with registration to do a complete userflow - "get /wsapi/list_emails complete": {"registered@testuser.com":{}} + "get /wsapi/list_emails complete": {"registered@testuser.com":{}}, + "post /wsapi/update_password valid": { success: true }, + "post /wsapi/update_password incorrectPassword": { success: false }, + "post /wsapi/update_password invalid": undefined }, setContextInfo: function(field, value) { @@ -145,7 +142,18 @@ BrowserID.Mocks.xhr = (function() { ok(false, "missing csrf token on POST request"); } - var resName = req.type + " " + req.url + " " + xhr.resultType; + + var resultType = xhr.resultType; + + // Unless the contextAjaxError is specified, use the "valid" context info. + // This makes it so we do not have to keep adding new items for + // context_info for every possible result type. + if(req.url === "/wsapi/session_context" && resultType !== "contextAjaxError") { + resultType = "valid"; + } + + var resName = req.type + " " + req.url + " " + resultType; + var result = xhr.results[resName]; var type = typeof result; diff --git a/resources/static/test/qunit/pages/manage_account_unit_test.js b/resources/static/test/qunit/pages/manage_account_unit_test.js index 1906a5919202ad69d3eefc8934f9b5e8dfa1edd5..0c18b7e66161c0e440133638c21566a354908d13 100644 --- a/resources/static/test/qunit/pages/manage_account_unit_test.js +++ b/resources/static/test/qunit/pages/manage_account_unit_test.js @@ -44,6 +44,7 @@ testHelpers = bid.TestHelpers, validToken = true, TEST_ORIGIN = "http://browserid.org", + tooltip = bid.Tooltip, mocks = { confirm: function() { return true; }, document: { location: "" } @@ -165,4 +166,73 @@ }); }); }); + + asyncTest("changePassword with missing old password, expect tooltip", function() { + bid.manageAccount(mocks, function() { + $("#old_password").val(""); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on missing old password, status is false"); + equal(tooltip.shown, true, "tooltip is visible"); + start(); + }); + }); + }); + + asyncTest("changePassword with missing new password, expect tooltip", function() { + bid.manageAccount(mocks, function() { + $("#old_password").val("oldpassword"); + $("#new_password").val(""); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on missing new password, status is false"); + equal(tooltip.shown, true, "tooltip is visible"); + start(); + }); + }); + }); + + asyncTest("changePassword with incorrect old password, expect tooltip", function() { + bid.manageAccount(mocks, function() { + xhr.useResult("incorrectPassword"); + + $("#old_password").val("incorrectpassword"); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on incorrect old password, status is false"); + equal(tooltip.shown, true, "tooltip is visible"); + start(); + }); + }); + }); + + asyncTest("changePassword with XHR error, expect error message", function() { + bid.manageAccount(mocks, function() { + xhr.useResult("invalid"); + + $("#old_password").val("oldpassword"); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on xhr error, status is false"); + start(); + }); + }); + }); + + asyncTest("changePassword happy case", function() { + bid.manageAccount(mocks, function() { + $("#old_password").val("oldpassword"); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, true, "on proper completion, status is true"); + equal(tooltip.shown, false, "on proper completion, tooltip is not shown"); + start(); + }); + }); + }); + }()); diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js index d8d3fa46745c3111a52e938a9058458ebe17e288..00c342dc2d26a973c74ae732e0040216b5c8a847 100644 --- a/resources/static/test/qunit/shared/network_unit_test.js +++ b/resources/static/test/qunit/shared/network_unit_test.js @@ -546,63 +546,6 @@ failureCheck(network.requestPasswordReset, "address", "origin"); }); - wrappedAsyncTest("resetPassword", function() { - network.resetPassword("password", function onSuccess() { - // XXX need a test here; - ok(true); - wrappedStart(); - }, function onFailure() { - ok(false); - wrappedStart(); - }); - - }); - - wrappedAsyncTest("resetPassword with XHR failure", function() { - xhr.useResult("ajaxError"); -/* - the body of this function is not yet written - - network.resetPassword("password", function onSuccess() { - ok(false, "XHR failure should never call success"); - wrappedStart(); - }, function onFailure() { - ok(true, "XHR failure should always call failure"); - wrappedStart(); - }); -*/ - start(); - }); - - wrappedAsyncTest("changePassword", function() { - network.changePassword("oldpassword", "newpassword", function onSuccess() { - // XXX need a real wrappedAsyncTest here. - ok(true); - wrappedStart(); - }, function onFailure() { - ok(false); - wrappedStart(); - }); - - }); - - wrappedAsyncTest("changePassword with XHR failure", function() { - xhr.useResult("ajaxError"); - - /* - the body of this function is not yet written. - network.changePassword("oldpassword", "newpassword", function onSuccess() { - ok(false, "XHR failure should never call success"); - wrappedStart(); - }, function onFailure() { - ok(true, "XHR failure should always call failure"); - wrappedStart(); - }); - - */ - start(); - }); - wrappedAsyncTest("serverTime", function() { // I am forcing the server time to be 1.25 seconds off. xhr.setContextInfo("server_time", new Date().getTime() - 1250); @@ -662,4 +605,38 @@ }); }); + + asyncTest("changePassword happy case, expect complete callback with true status", function() { + network.changePassword("oldpassword", "newpassword", function onComplete(status) { + equal(status, true, "calls onComplete with true status"); + start(); + }, function onFailure() { + ok(false, "unexpected failure"); + start(); + }); + }); + + asyncTest("changePassword with incorrect old password, expect complete callback with false status", function() { + xhr.useResult("incorrectPassword"); + + network.changePassword("oldpassword", "newpassword", function onComplete(status) { + equal(status, false, "calls onComplete with false status"); + start(); + }, function onFailure() { + ok(false, "unexpected failure"); + start(); + }); + }); + + asyncTest("changePassword with XHR error, expect error callback", function() { + xhr.useResult("ajaxError"); + + network.changePassword("oldpassword", "newpassword", function onComplete() { + ok(false, "XHR failure should never call complete"); + start(); + }, function onFailure() { + ok(true, "XHR failure should always call failure"); + start(); + }); + }); }()); diff --git a/resources/static/test/qunit/shared/tooltip_unit_test.js b/resources/static/test/qunit/shared/tooltip_unit_test.js index 6c5d7e359aa030f9675044e89f58296794d22bb5..1c3bb4065c284e2c291e615155157e235cfdea0e 100644 --- a/resources/static/test/qunit/shared/tooltip_unit_test.js +++ b/resources/static/test/qunit/shared/tooltip_unit_test.js @@ -76,4 +76,15 @@ }); }); + asyncTest("show tooltip, then reset - hides tooltip, resets shown status", function() { + tooltip.showTooltip("#shortTooltip"); + setTimeout(function() { + tooltip.reset(); + + equal($(".tooltip:visible").length, 0, "after reset, all tooltips are hidden"); + equal(tooltip.shown, false, "after reset, tooltip status is reset"); + start(); + }, 100); + }); + }()); diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js index d7ca0c676660680ba8737649b1fa02ba827b4cfc..aa0601edd49076bb6cc5aef74c52949f440cb6a2 100644 --- a/resources/static/test/qunit/testHelpers/helpers.js +++ b/resources/static/test/qunit/testHelpers/helpers.js @@ -5,6 +5,7 @@ storage = bid.Storage, xhr = bid.Mocks.xhr, screens = bid.Screens, + tooltip = bid.Tooltip, registrations = []; calls = {}; @@ -49,6 +50,7 @@ mediator.reset(); screens.wait.hide(); screens.error.hide(); + tooltip.reset(); }, teardown: function() { @@ -59,6 +61,7 @@ $("#error").html("<div class='contents'></div>").hide(); screens.wait.hide(); screens.error.hide(); + tooltip.reset(); }, register: register, diff --git a/resources/views/index.ejs b/resources/views/index.ejs index 46cae5c26fa0a3bfe6856940f8437bff1614f065..9d9aea739112578bb45d68478ad9551920e336e3 100644 --- a/resources/views/index.ejs +++ b/resources/views/index.ejs @@ -5,15 +5,45 @@ <div id="manage"> <h1 class="serif">Account Manager</h1> - <div class="buttonrow cf"> - <strong>Your Email Addresses</strong> - - <button id="manageAccounts" href="#">edit</button> - <button id="cancelManage" href="#">done</button> - </div> - <ul id="emailList"> - </ul> - <div id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></div> + + <section> + <header class="buttonrow cf"> + <h2>Your Email Addresses</h2> + + <button class="edit">edit</button> + <button class="done">done</button> + </header> + + <ul id="emailList"> + </ul> + </section> + + <section id="edit_password"> + <header class="buttonrow cf"> + <h2>Password</h2> + + <button class="edit">edit</button> + <button class="done">cancel</button> + </header> + + <div class="showedit"> + <label for="old_password">Old Password</label> + <label for="new_password">New Password</label> + </div> + + <form id="edit_password_form" class="showedit"> + <input type="password" id="old_password" name="old_password" placeholder="old password"/> + <input type="password" id="new_password" name="new_password" placeholder="new password"/> + <button id="changePassword">done</button> + + <div class="tooltip" for="old_password" id="tooltipOldRequired">Old password is required</div> + <div class="tooltip" for="old_password" id="tooltipInvalidPassword">Incorrect old password, password not updated</div> + <div class="tooltip" for="new_password" id="tooltipNewRequired">New password is required</div> + </form> + </section> + + + <p id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></p> </div> </div>