diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index 6520e45bc7..95d7380797 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -246,15 +246,11 @@ function( }; var that = this; - // record change generation for isClean - // (I don't know what this implies for the editor) - this.generation = this.codemirror.changeGeneration(); - var _save = function () { - // What does this event do? Does this always need to happen, - // even if the file can't be saved? What is dependent on it? that.events.trigger("file_saving.Editor"); return that.contents.save(that.file_path, model).then(function(data) { + // record change generation for isClean + this.generation = this.codemirror.changeGeneration(); that.events.trigger("file_saved.Editor", data); that.last_modified = new Date(data.last_modified); that._clean_state(); @@ -278,8 +274,8 @@ function( console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+ "while the current file seem to have been saved on `"+data.last_modified+"`"); if (that._changed_on_disk_dialog !== null) { - // since the modal's event bindings are removed when destroyed, - // we reinstate save & reload callbacks on the confirmation & reload buttons + // since the modal's event bindings are removed when destroyed, we reinstate + // save & reload callbacks on the confirmation & reload buttons that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save); that._changed_on_disk_dialog.find('.btn-warning').click(function () {window.location.reload()}); diff --git a/notebook/tests/editor/save.js b/notebook/tests/editor/save.js new file mode 100644 index 0000000000..6aee2c0a53 --- /dev/null +++ b/notebook/tests/editor/save.js @@ -0,0 +1,43 @@ +// +// Test prompt when overwriting a file that is modified on disk +// + +casper.editor_test(function () { + var fname = "has#hash and space and unicø∂e.py"; + + this.append_cell("s = '??'", 'code'); + + this.thenEvaluate(function (nbname) { + require(['base/js/events'], function (events) { + IPython.editor.set_notebook_name(nbname); + IPython._save_success = IPython._save_failed = false; + events.on('file_saved.Editor', function () { + IPython._save_success = true; + }); + IPython.notebook.save_notebook(); + }); + }, {nbname:nbname}); + + this.waitFor(function () { + return this.evaluate(function(){ + return IPython._save_failed || IPython._save_success; + }); + }); + + this.thenEvaluate(function(){ + IPython._checkpoint_created = false; + require(['base/js/events'], function (events) { + events.on('checkpoint_created.Notebook', function (evt, data) { + IPython._checkpoint_created = true; + }); + }); + IPython.notebook.save_checkpoint(); + }); + + this.waitFor(function() { + return this.evaluate(function () { + return IPython && IPython.notebook && true; + }); + }); + +}); diff --git a/notebook/tests/util.js b/notebook/tests/util.js index 75969a7cc6..d63f92672b 100644 --- a/notebook/tests/util.js +++ b/notebook/tests/util.js @@ -734,6 +734,103 @@ casper.dashboard_test = function (test) { }); }; +/** + * Editor Tests + * + * The functions below are utilities for setting up an editor and tearing it down + * after the test is over. + */ +caspser.open_new_file = function () { + // load up the jupyter notebook server (it's like running `jupyter notebook` in the shell) + var baseUrl = this.get_notebook_server(); + + // go to the base url, wait for it to load, then make a new file + this.start(baseUrl); + this.waitFor(this.page_loaded); + this.waitForSelector('#new-file a'); + this.thenClick('#new-file a'); + + // when the popup loads, go into that popup and wait until the main text box is loaded + this.withPopup(0, function () {this.waitForSelector('.CodeMirror-sizer');}); + + // now let's open the window where the file editor is displayed & load + this.then(function () { + this.open(this.popups[0].url); + }); + this.waitFor(this.page_loaded); + + // Hook the log and error methods of the console, forcing them to + // serialize their arguments before printing. This allows the + // Objects to cross into the phantom/slimer regime for display. + this.thenEvaluate(function(){ + var serialize_arguments = function(f, context) { + return function() { + var pretty_arguments = []; + for (var i = 0; i < arguments.length; i++) { + var value = arguments[i]; + if (value instanceof Object) { + var name = value.name || 'Object'; + // Print a JSON string representation of the object. + // If we don't do this, [Object object] gets printed + // by casper, which is useless. The long regular + // expression reduces the verbosity of the JSON. + pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ') + .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n') + .replace(/\n(\s+)?\n/g, '\n')); + } else { + pretty_arguments.push(value); + } + } + f.apply(context, pretty_arguments); + }; + }; + console.log = serialize_arguments(console.log, console); + console.error = serialize_arguments(console.error, console); + }); + + console.log('Editor loaded.') + +} + +casper.editor_test = function(test) { + // Wrap a notebook test to reduce boilerplate. + this.open_new_file(); + + // Echo whether or not we are running this test using SlimerJS + if (this.evaluate(function(){ + return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+ + })) { + console.log('This test is running in SlimerJS.'); + this.slimerjs = true; + } + + // Make sure to remove the onbeforeunload callback. This callback is + // responsible for the "Are you sure you want to quit?" type messages. + // PhantomJS ignores these prompts, SlimerJS does not which causes hangs. + this.then(function(){ + this.evaluate(function(){ + window.onbeforeunload = function(){}; + }); + }); + + this.then(test); + + // This is required to clean up the page we just finished with. If we don't call this + // casperjs will leak file descriptors of all the open WebSockets in that page. We + // have to set this.page=null so that next time casper.start runs, it will create a + // new page from scratch. + this.then(function () { + this.page.close(); + this.page = null; + }); + + // Run the browser automation. + this.run(function() { + this.test.done(); + }); +}; + + // note that this will only work for UNIQUE events -- if you want to // listen for the same event twice, this will not work! casper.event_test = function (name, events, action, timeout) {