// // Utility functions for the HTML notebook's CasperJS tests. // casper.get_notebook_server = function () { // Get the URL of a notebook server on which to run tests. var port = casper.cli.get("port"); port = (typeof port === 'undefined') ? '8888' : port; return casper.cli.get("url") || ('http://127.0.0.1:' + port); }; casper.open_new_notebook = function () { // Create and open a new notebook. var baseUrl = this.get_notebook_server(); this.start(baseUrl); this.waitFor(this.page_loaded); this.thenClick('#kernel-python2 a, #kernel-python3 a'); this.waitForPopup(''); this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');}); 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); }); // Make sure the kernel has started this.waitFor(this.kernel_running); // track the IPython busy/idle state this.thenEvaluate(function () { require(['base/js/namespace', 'base/js/events'], function (IPython, events) { events.on('kernel_idle.Kernel',function () { IPython._status = 'idle'; }); events.on('kernel_busy.Kernel',function () { IPython._status = 'busy'; }); }); }); // Because of the asynchronous nature of SlimerJS (Gecko), we need to make // sure the notebook has actually been loaded into the IPython namespace // before running any tests. this.waitFor(function() { return this.evaluate(function () { return IPython.notebook; }); }); }; casper.page_loaded = function() { // Return whether or not the kernel is running. return this.evaluate(function() { return typeof IPython !== "undefined" && IPython.page !== undefined; }); }; casper.kernel_running = function() { // Return whether or not the kernel is running. return this.evaluate(function() { return IPython && IPython.notebook && IPython.notebook.kernel && IPython.notebook.kernel.is_connected(); }); }; casper.kernel_disconnected = function() { return this.evaluate(function() { return IPython.notebook.kernel.is_fully_disconnected(); }); }; casper.wait_for_kernel_ready = function () { this.waitFor(this.kernel_running); this.thenEvaluate(function () { IPython._kernel_ready = false; IPython.notebook.kernel.kernel_info( function () { IPython._kernel_ready = true; }); }); this.waitFor(function () { return this.evaluate(function () { return IPython._kernel_ready; }); }); }; casper.shutdown_current_kernel = function () { // Shut down the current notebook's kernel. this.thenEvaluate(function() { IPython.notebook.session.delete(); }); // We close the page right after this so we need to give it time to complete. this.wait(1000); }; casper.delete_current_notebook = function () { // Delete created notebook. // For some unknown reason, this doesn't work?!? this.thenEvaluate(function() { IPython.notebook.delete(); }); }; casper.wait_for_busy = function () { // Waits for the notebook to enter a busy state. this.waitFor(function () { return this.evaluate(function () { return IPython._status == 'busy'; }); }); }; casper.wait_for_idle = function () { // Waits for the notebook to idle. this.waitFor(function () { return this.evaluate(function () { return IPython._status == 'idle'; }); }); }; casper.wait_for_output = function (cell_num, out_num) { // wait for the nth output in a given cell this.wait_for_idle(); out_num = out_num || 0; this.then(function() { this.waitFor(function (c, o) { return this.evaluate(function get_output(c, o) { var cell = IPython.notebook.get_cell(c); return cell.output_area.outputs.length > o; }, // pass parameter from the test suite js to the browser code js {c : cell_num, o : out_num}); }); }, function then() { }, function timeout() { this.echo("wait_for_output timed out!"); }); }; casper.wait_for_widget = function (widget_info) { // wait for a widget msg que to reach 0 // // Parameters // ---------- // widget_info : object // Object which contains info related to the widget. The model_id property // is used to identify the widget. // Clear the results of a previous query, if they exist. Make sure a // dictionary exists to store the async results in. this.thenEvaluate(function(model_id) { if (window.pending_msgs === undefined) { window.pending_msgs = {}; } else { window.pending_msgs[model_id] = -1; } }, {model_id: widget_info.model_id}); // Wait for the pending messages to be 0. this.waitFor(function () { var pending = this.evaluate(function (model_id) { // Get the model. Once the model is had, store it's pending_msgs // count in the window's dictionary. IPython.notebook.kernel.widget_manager.get_model(model_id) .then(function(model) { window.pending_msgs[model_id] = model.pending_msgs; }); // Return the pending_msgs result. return window.pending_msgs[model_id]; }, {model_id: widget_info.model_id}); if (pending === 0) { return true; } else { return false; } }); }; casper.get_output_cell = function (cell_num, out_num) { // return an output of a given cell out_num = out_num || 0; var result = casper.evaluate(function (c, o) { var cell = IPython.notebook.get_cell(c); return cell.output_area.outputs[o]; }, {c : cell_num, o : out_num}); if (!result) { var num_outputs = casper.evaluate(function (c) { var cell = IPython.notebook.get_cell(c); return cell.output_area.outputs.length; }, {c : cell_num}); this.test.assertTrue(false, "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)" ); } else { return result; } }; casper.get_cells_length = function () { // return the number of cells in the notebook var result = casper.evaluate(function () { return IPython.notebook.get_cells().length; }); return result; }; casper.set_cell_text = function(index, text){ // Set the text content of a cell. this.evaluate(function (index, text) { var cell = IPython.notebook.get_cell(index); cell.set_text(text); }, index, text); }; casper.get_cell_text = function(index){ // Get the text content of a cell. return this.evaluate(function (index) { var cell = IPython.notebook.get_cell(index); return cell.get_text(); }, index); }; casper.insert_cell_at_bottom = function(cell_type){ // Inserts a cell at the bottom of the notebook // Returns the new cell's index. return this.evaluate(function (cell_type) { var cell = IPython.notebook.insert_cell_at_bottom(cell_type); return IPython.notebook.find_cell_index(cell); }, cell_type); }; casper.append_cell = function(text, cell_type) { // Insert a cell at the bottom of the notebook and set the cells text. // Returns the new cell's index. var index = this.insert_cell_at_bottom(cell_type); if (text !== undefined) { this.set_cell_text(index, text); } return index; }; casper.execute_cell = function(index, expect_failure){ // Asynchronously executes a cell by index. // Returns the cell's index. if (expect_failure === undefined) expect_failure = false; var that = this; this.then(function(){ that.evaluate(function (index) { var cell = IPython.notebook.get_cell(index); cell.execute(); }, index); }); this.wait_for_idle(); this.then(function () { var error = that.evaluate(function (index) { var cell = IPython.notebook.get_cell(index); var outputs = cell.output_area.outputs; for (var i = 0; i < outputs.length; i++) { if (outputs[i].output_type == 'error') { return outputs[i]; } } return false; }, index); if (error === null) { this.test.fail("Failed to check for error output"); } if (expect_failure && error === false) { this.test.fail("Expected error while running cell"); } else if (!expect_failure && error !== false) { this.test.fail("Error running cell:\n" + error.traceback.join('\n')); } }); return index; }; casper.execute_cell_then = function(index, then_callback, expect_failure) { // Synchronously executes a cell by index. // Optionally accepts a then_callback parameter. then_callback will get called // when the cell has finished executing. // Returns the cell's index. var return_val = this.execute_cell(index, expect_failure); this.wait_for_idle(); var that = this; this.then(function(){ if (then_callback!==undefined) { then_callback.apply(that, [index]); } }); return return_val; }; casper.wait_for_element = function(index, selector){ // Utility function that allows us to easily wait for an element // within a cell. Uses JQuery selector to look for the element. var that = this; this.waitFor(function() { return that.cell_element_exists(index, selector); }); }; casper.cell_element_exists = function(index, selector){ // Utility function that allows us to easily check if an element exists // within a cell. Uses JQuery selector to look for the element. return casper.evaluate(function (index, selector) { var $cell = IPython.notebook.get_cell(index).element; return $cell.find(selector).length > 0; }, index, selector); }; casper.cell_element_function = function(index, selector, function_name, function_args){ // Utility function that allows us to execute a jQuery function on an // element within a cell. return casper.evaluate(function (index, selector, function_name, function_args) { var $cell = IPython.notebook.get_cell(index).element; var $el = $cell.find(selector); return $el[function_name].apply($el, function_args); }, index, selector, function_name, function_args); }; casper.validate_notebook_state = function(message, mode, cell_index) { // Validate the entire dual mode state of the notebook. Make sure no more than // one cell is selected, focused, in edit mode, etc... // General tests. this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(), message + '; keyboard and notebook modes match'); // Is the selected cell the only cell that is selected? if (cell_index!==undefined) { this.test.assert(this.is_only_cell_selected(cell_index), message + '; cell ' + cell_index + ' is the only cell selected'); } // Mode specific tests. if (mode==='command') { // Are the notebook and keyboard manager in command mode? this.test.assertEquals(this.get_keyboard_mode(), 'command', message + '; in command mode'); // Make sure there isn't a single cell in edit mode. this.test.assert(this.is_only_cell_edit(null), message + '; all cells in command mode'); this.test.assert(this.is_cell_editor_focused(null), message + '; no cell editors are focused while in command mode'); } else if (mode==='edit') { // Are the notebook and keyboard manager in edit mode? this.test.assertEquals(this.get_keyboard_mode(), 'edit', message + '; in edit mode'); if (cell_index!==undefined) { // Is the specified cell the only cell in edit mode? this.test.assert(this.is_only_cell_edit(cell_index), message + '; cell ' + cell_index + ' is the only cell in edit mode '+ this.cells_modes()); // Is the specified cell the only cell with a focused code mirror? this.test.assert(this.is_cell_editor_focused(cell_index), message + '; cell ' + cell_index + '\'s editor is appropriately focused'); } } else { this.test.assert(false, message + '; ' + mode + ' is an unknown mode'); } }; casper.select_cell = function(index) { // Select a cell in the notebook. this.evaluate(function (i) { IPython.notebook.select(i); }, {i: index}); }; casper.click_cell_editor = function(index) { // Emulate a click on a cell's editor. // Code Mirror does not play nicely with emulated brower events. // Instead of trying to emulate a click, here we run code similar to // the code used in Code Mirror that handles the mousedown event on a // region of codemirror that the user can focus. this.evaluate(function (i) { var cm = IPython.notebook.get_cell(i).code_mirror; if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input)){ cm.display.input.focus(); } }, {i: index}); }; casper.set_cell_editor_cursor = function(index, line_index, char_index) { // Set the Code Mirror instance cursor's location. this.evaluate(function (i, l, c) { IPython.notebook.get_cell(i).code_mirror.setCursor(l, c); }, {i: index, l: line_index, c: char_index}); }; casper.focus_notebook = function() { // Focus the notebook div. this.evaluate(function (){ $('#notebook').focus(); }, {}); }; casper.trigger_keydown = function() { // Emulate a keydown in the notebook. for (var i = 0; i < arguments.length; i++) { this.evaluate(function (k) { var element = $(document); var event = IPython.keyboard.shortcut_to_event(k, 'keydown'); element.trigger(event); }, {k: arguments[i]}); } }; casper.get_keyboard_mode = function() { // Get the mode of the keyboard manager. return this.evaluate(function() { return IPython.keyboard_manager.mode; }, {}); }; casper.get_notebook_mode = function() { // Get the mode of the notebook. return this.evaluate(function() { return IPython.notebook.mode; }, {}); }; casper.get_cell = function(index) { // Get a single cell. // // Note: Handles to DOM elements stored in the cell will be useless once in // CasperJS context. return this.evaluate(function(i) { var cell = IPython.notebook.get_cell(i); if (cell) { return cell; } return null; }, {i : index}); }; casper.is_cell_editor_focused = function(index) { // Make sure a cell's editor is the only editor focused on the page. return this.evaluate(function(i) { var focused_textarea = $('#notebook .CodeMirror-focused textarea'); if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; } if (i === null) { return focused_textarea.length === 0; } else { var cell = IPython.notebook.get_cell(i); if (cell) { return cell.code_mirror.getInputField() == focused_textarea[0]; } } return false; }, {i : index}); }; casper.is_only_cell_selected = function(index) { // Check if a cell is the only cell selected. // Pass null as the index to check if no cells are selected. return this.is_only_cell_on(index, 'selected', 'unselected'); }; casper.is_only_cell_edit = function(index) { // Check if a cell is the only cell in edit mode. // Pass null as the index to check if all of the cells are in command mode. var cells_length = this.get_cells_length(); for (var j = 0; j < cells_length; j++) { if (j === index) { if (!this.cell_mode_is(j, 'edit')) { return false; } } else { if (this.cell_mode_is(j, 'edit')) { return false; } } } return true; }; casper.is_only_cell_on = function(i, on_class, off_class) { // Check if a cell is the only cell with the `on_class` DOM class applied to it. // All of the other cells are checked for the `off_class` DOM class. // Pass null as the index to check if all of the cells have the `off_class`. var cells_length = this.get_cells_length(); for (var j = 0; j < cells_length; j++) { if (j === i) { if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) { return false; } } else { if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) { return false; } } } return true; }; casper.cells_modes = function(){ return this.evaluate(function(){ return IPython.notebook.get_cells().map(function(x,c){return x.mode}) }, {}); }; casper.cell_mode_is = function(index, mode) { // Check if a cell is in a specific mode return this.evaluate(function(i, m) { var cell = IPython.notebook.get_cell(i); if (cell) { return cell.mode === m; } return false; }, {i : index, m: mode}); }; casper.cell_has_class = function(index, classes) { // Check if a cell has a class. return this.evaluate(function(i, c) { var cell = IPython.notebook.get_cell(i); if (cell) { return cell.element.hasClass(c); } return false; }, {i : index, c: classes}); }; casper.is_cell_rendered = function (index) { return this.evaluate(function(i) { return !!IPython.notebook.get_cell(i).rendered; }, {i:index}); }; casper.assert_colors_equal = function (hex_color, local_color, msg) { // Tests to see if two colors are equal. // // Parameters // hex_color: string // Hexadecimal color code, with or without preceeding hash character. // local_color: string // Local color representation. Can either be hexadecimal (default for // phantom) or rgb (default for slimer). // Remove parentheses, hashes, semi-colons, and space characters. hex_color = hex_color.replace(/[\(\); #]/, ''); local_color = local_color.replace(/[\(\); #]/, ''); // If the local color is rgb, clean it up and replace if (local_color.substr(0,3).toLowerCase() == 'rgb') { var components = local_color.substr(3).split(','); local_color = ''; for (var i = 0; i < components.length; i++) { var part = parseInt(components[i]).toString(16); while (part.length < 2) part = '0' + part; local_color += part; } } this.test.assertEquals(hex_color.toUpperCase(), local_color.toUpperCase(), msg); }; casper.notebook_test = function(test) { // Wrap a notebook test to reduce boilerplate. this.open_new_notebook(); // 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); // Kill the kernel and delete the notebook. this.shutdown_current_kernel(); // This is still broken but shouldn't be a problem for now. // this.delete_current_notebook(); // 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(); }); }; casper.wait_for_dashboard = function () { // Wait for the dashboard list to load. casper.waitForSelector('.list_item'); }; casper.open_dashboard = function () { // Start casper by opening the dashboard page. var baseUrl = this.get_notebook_server(); this.start(baseUrl); this.waitFor(this.page_loaded); this.wait_for_dashboard(); }; casper.dashboard_test = function (test) { // Open the dashboard page and run a test. this.open_dashboard(); this.then(test); 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) { // set up handlers to listen for each of the events this.thenEvaluate(function (events) { var make_handler = function (event) { return function () { IPython._events_triggered.push(event); IPython.notebook.events.off(event, null, IPython._event_handlers[event]); delete IPython._event_handlers[event]; }; }; IPython._event_handlers = {}; IPython._events_triggered = []; for (var i=0; i < events.length; i++) { IPython._event_handlers[events[i]] = make_handler(events[i]); IPython.notebook.events.on(events[i], IPython._event_handlers[events[i]]); } }, [events]); // execute the requested action this.then(action); // wait for all the events to be triggered this.waitFor(function () { return this.evaluate(function (events) { return IPython._events_triggered.length >= events.length; }, [events]); }, undefined, undefined, timeout); // test that the events were triggered in the proper order this.then(function () { var triggered = this.evaluate(function () { return IPython._events_triggered; }); var handlers = this.evaluate(function () { return Object.keys(IPython._event_handlers); }); this.test.assertEquals(triggered.length, events.length, name + ': ' + events.length + ' events were triggered'); this.test.assertEquals(handlers.length, 0, name + ': all handlers triggered'); for (var i=0; i < events.length; i++) { this.test.assertEquals(triggered[i], events[i], name + ': ' + events[i] + ' was triggered'); } }); // turn off any remaining event listeners this.thenEvaluate(function () { for (var event in IPython._event_handlers) { IPython.notebook.events.off(event, null, IPython._event_handlers[event]); delete IPython._event_handlers[event]; } }); }; casper.options.waitTimeout=10000; casper.on('waitFor.timeout', function onWaitForTimeout(timeout) { this.echo("Timeout for " + casper.get_notebook_server()); this.echo("Is the notebook server running?"); }); casper.print_log = function () { // Pass `console.log` calls from page JS to casper. this.on('remote.message', function(msg) { this.echo('Remote message caught: ' + msg); }); }; casper.on("page.error", function onError(msg, trace) { // show errors in the browser this.echo("Page Error"); this.echo(" Message: " + msg.split('\n').join('\n ')); this.echo(" Call stack:"); var local_path = this.get_notebook_server(); for (var i = 0; i < trace.length; i++) { var frame = trace[i]; var file = frame.file; // shorten common phantomjs evaluate url // this will have a different value on slimerjs if (file === "phantomjs://webpage.evaluate()") { file = "evaluate"; } // remove the version tag from the path file = file.replace(/(\?v=[0-9abcdef]+)/, ''); // remove the local address from the beginning of the path if (file.indexOf(local_path) === 0) { file = file.substr(local_path.length); } var frame_text = (frame.function.length > 0) ? " in " + frame.function : ""; this.echo(" line " + frame.line + " of " + file + frame_text); } }); casper.capture_log = function () { // show captured errors var captured_log = []; var seen_errors = 0; this.on('remote.message', function(msg) { captured_log.push(msg); }); var that = this; this.test.on("test.done", function (result) { // test.done runs per-file, // but suiteResults is per-suite (directory) var current_errors; if (this.suiteResults) { // casper 1.1 has suiteResults current_errors = this.suiteResults.countErrors() + this.suiteResults.countFailed(); } else { // casper 1.0 has testResults instead current_errors = this.testResults.failed; } if (current_errors > seen_errors && captured_log.length > 0) { casper.echo("\nCaptured console.log:"); for (var i = 0; i < captured_log.length; i++) { var output = String(captured_log[i]).split('\n'); for (var j = 0; j < output.length; j++) { casper.echo(" " + output[j]); } } } seen_errors = current_errors; captured_log = []; }); }; casper.interact = function() { // Start an interactive Javascript console. var system = require('system'); system.stdout.writeLine('JS interactive console.'); system.stdout.writeLine('Type `exit` to quit.'); function read_line() { system.stdout.writeLine('JS: '); var line = system.stdin.readLine(); return line; } var input = read_line(); while (input.trim() != 'exit') { var output = this.evaluate(function(code) { return String(eval(code)); }, {code: input}); system.stdout.writeLine('\nOut: ' + output); input = read_line(); } }; casper.capture_log();