diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index 013389cd4..fba5baac4 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -605,7 +605,119 @@ define([ $.ajax(url, settings); }); }; - + + var WrappedError = function(message, error){ + // Wrappable Error class + + // The Error class doesn't actually act on `this`. Instead it always + // returns a new instance of Error. Here we capture that instance so we + // can apply it's properties to `this`. + var tmp = Error.apply(this, [message]); + + // Copy the properties of the error over to this. + var properties = Object.getOwnPropertyNames(tmp); + for (var i = 0; i < properties.length; i++) { + this[properties[i]] = tmp[properties[i]]; + } + + // Keep a stack of the original error messages. + if (error instanceof WrappedError) { + this.error_stack = error.error_stack; + } else { + this.error_stack = [error]; + } + this.error_stack.push(tmp); + + return this; + }; + + WrappedError.prototype = Object.create(Error.prototype, {}); + + + var load_class = function(class_name, module_name, registry) { + // Tries to load a class + // + // Tries to load a class from a module using require.js, if a module + // is specified, otherwise tries to load a class from the global + // registry, if the global registry is provided. + return new Promise(function(resolve, reject) { + + // Try loading the view module using require.js + if (module_name) { + require([module_name], function(module) { + if (module[class_name] === undefined) { + reject(new Error('Class '+class_name+' not found in module '+module_name)); + } else { + resolve(module[class_name]); + } + }, reject); + } else { + if (registry && registry[class_name]) { + resolve(registry[class_name]); + } else { + reject(new Error('Class '+class_name+' not found in registry ')); + } + } + }); + }; + + var resolve_promises_dict = function(d) { + // Resolve a promiseful dictionary. + // Returns a single Promise. + var keys = Object.keys(d); + var values = []; + keys.forEach(function(key) { + values.push(d[key]); + }); + return Promise.all(values).then(function(v) { + d = {}; + for(var i=0; i'); + cell.widget_subarea.append(dummy); + that.create_view(model, {cell: cell}).then(function(view) { + that._handle_display_view(view); + dummy.replaceWith(view.$el); + view.trigger('displayed'); + resolve(view); + }, function(error) { + reject(new utils.WrappedError('Could not display view', error)); + }); + } + }); }; WidgetManager.prototype._handle_display_view = function (view) { @@ -70,52 +76,39 @@ define([ if (this.keyboard_manager) { this.keyboard_manager.register_events(view.$el); - if (view.additional_elements) { - for (var i = 0; i < view.additional_elements.length; i++) { + if (view.additional_elements) { + for (var i = 0; i < view.additional_elements.length; i++) { this.keyboard_manager.register_events(view.additional_elements[i]); - } - } + } + } } }; - WidgetManager.prototype.create_view = function(model, options) { - // Creates a view for a particular model. - - var view_name = model.get('_view_name'); - var view_mod = model.get('_view_module'); - var error = options.error || function(error) { console.log(error); }; + // Creates a promise for a view of a given model + + // Make sure the view creation is not out of order with + // any state updates. + model.state_change = model.state_change.then(function() { + + return utils.load_class(model.get('_view_name'), model.get('_view_module'), + WidgetManager._view_types).then(function(ViewType) { - var instantiate_view = function(ViewType) { - if (ViewType) { // If a view is passed into the method, use that view's cell as // the cell for the view that is created. options = options || {}; if (options.parent !== undefined) { options.cell = options.parent.options.cell; } - // Create and render the view... var parameters = {model: model, options: options}; var view = new ViewType(parameters); + view.listenTo(model, 'destroy', view.remove); view.render(); - model.on('destroy', view.remove, view); - if (options.success) { - options.success(view); - } - } else { - error({unknown_view: true, view_name: view_name, - view_module: view_mod}); - } - }; - - if (view_mod) { - require([view_mod], function(module) { - instantiate_view(module[view_name]); - }, error); - } else { - instantiate_view(WidgetManager._view_types[view_name]); - } + return view; + }, utils.reject("Couldn't create a view for model id '" + String(model.id) + "'")); + }); + return model.state_change; }; WidgetManager.prototype.get_msg_cell = function (msg_id) { @@ -179,24 +172,20 @@ define([ }; WidgetManager.prototype.get_model = function (model_id) { - // Look-up a model instance by its id. - var model = this._models[model_id]; - if (model !== undefined && model.id == model_id) { - return model; - } - return null; + // Get a promise for a model by model id. + return this._models[model_id]; }; WidgetManager.prototype._handle_comm_open = function (comm, msg) { // Handle when a comm is opened. - this.create_model({ + return this.create_model({ model_name: msg.content.data.model_name, model_module: msg.content.data.model_module, - comm: comm}); + comm: comm}).catch(utils.reject("Couldn't create a model.")); }; WidgetManager.prototype.create_model = function (options) { - // Create and return a new widget model. + // Create and return a promise for a new widget model // // Minimally, one must provide the model_name and widget_class // parameters to create a model from Javascript. @@ -206,8 +195,9 @@ define([ // JS: // IPython.notebook.kernel.widget_manager.create_model({ // model_name: 'WidgetModel', - // widget_class: 'IPython.html.widgets.widget_int.IntSlider', - // init_state_callback: function(model) { console.log('Create success!', model); }}); + // widget_class: 'IPython.html.widgets.widget_int.IntSlider'}) + // .then(function(model) { console.log('Create success!', model); }, + // $.proxy(console.error, console)); // // Parameters // ---------- @@ -220,60 +210,33 @@ define([ // widget_class: (optional) string // Target name of the widget in the back-end. // comm: (optional) Comm - // success: (optional) callback - // Callback for when the model was created successfully. - // error: (optional) callback - // Callback for when the model wasn't created. - // init_state_callback: (optional) callback - // Called when the first state push from the back-end is - // recieved. Allows you to modify the model after it's - // complete state is filled and synced. - - // Make default callbacks if not specified. - var error = options.error || function(error) { console.log(error); }; - + // Create a comm if it wasn't provided. var comm = options.comm; if (!comm) { comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class}); } - // Create a new model that is connected to the comm. var that = this; - var instantiate_model = function(ModelType) { - var model_id = comm.comm_id; - var widget_model = new ModelType(that, model_id, comm, options.init_state_callback); - widget_model.on('comm:close', function () { - delete that._models[model_id]; + var model_id = comm.comm_id; + var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types) + .then(function(ModelType) { + var widget_model = new ModelType(that, model_id, comm); + widget_model.once('comm:close', function () { + delete that._models[model_id]; + }); + return widget_model; + + }, function(error) { + delete that._models[model_id]; + var wrapped_error = new utils.WrappedError("Couldn't create model", error); + return Promise.reject(wrapped_error); }); - that._models[model_id] = widget_model; - if (options.success) { - options.success(widget_model); - } - }; - - // Get the model type using require or through the registry. - var widget_type_name = options.model_name; - var widget_module = options.model_module; - if (widget_module) { - - // Load the module containing the widget model - require([widget_module], function(mod) { - if (mod[widget_type_name]) { - instantiate_model(mod[widget_type_name]); - } else { - error("Error creating widget model: " + widget_type_name - + " not found in " + widget_module); - } - }, error); - } else { - - // No module specified, load from the global models registry - instantiate_model(WidgetManager._model_types[widget_type_name]); - } + this._models[model_id] = model_promise; + return model_promise; }; - // Backwards compatability. + // Backwards compatibility. IPython.WidgetManager = WidgetManager; return {'WidgetManager': WidgetManager}; diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js index ea5c062c5..00a4ca740 100644 --- a/IPython/html/static/widgets/js/widget.js +++ b/IPython/html/static/widgets/js/widget.js @@ -3,13 +3,14 @@ define(["widgets/js/manager", "underscore", - "backbone", - "jquery", + "backbone", + "jquery", + "base/js/utils", "base/js/namespace", -], function(widgetmanager, _, Backbone, $, IPython){ +], function(widgetmanager, _, Backbone, $, utils, IPython){ var WidgetModel = Backbone.Model.extend({ - constructor: function (widget_manager, model_id, comm, init_state_callback) { + constructor: function (widget_manager, model_id, comm) { // Constructor // // Creates a WidgetModel instance. @@ -20,11 +21,8 @@ define(["widgets/js/manager", // model_id : string // An ID unique to this model. // comm : Comm instance (optional) - // init_state_callback : callback (optional) - // Called once when the first state message is recieved from - // the back-end. this.widget_manager = widget_manager; - this.init_state_callback = init_state_callback; + this.state_change = Promise.resolve(); this._buffered_state_diff = {}; this.pending_msgs = 0; this.msg_buffer = null; @@ -71,13 +69,12 @@ define(["widgets/js/manager", _handle_comm_msg: function (msg) { // Handle incoming comm msg. var method = msg.content.data.method; + var that = this; switch (method) { case 'update': - this.set_state(msg.content.data.state); - if (this.init_state_callback) { - this.init_state_callback.apply(this, [this]); - delete this.init_state_callback; - } + this.state_change = this.state_change.then(function() { + return that.set_state(msg.content.data.state); + }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true)); break; case 'custom': this.trigger('msg:custom', msg.content.data.content); @@ -89,17 +86,17 @@ define(["widgets/js/manager", }, set_state: function (state) { + var that = this; // Handle when a widget is updated via the python side. - this.state_lock = state; - try { - var that = this; - WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) { - obj[key] = that._unpack_models(state[key]); - return obj; - }, {})]); - } finally { - this.state_lock = null; - } + return this._unpack_models(state).then(function(state) { + that.state_lock = state; + try { + WidgetModel.__super__.set.call(that, state); + } finally { + that.state_lock = null; + } + return Promise.resolve(); + }, utils.reject("Couldn't set model state", true)); }, _handle_status: function (msg, callbacks) { @@ -259,24 +256,18 @@ define(["widgets/js/manager", _.each(value, function(sub_value, key) { unpacked.push(that._unpack_models(sub_value)); }); - return unpacked; - + return Promise.all(unpacked); } else if (value instanceof Object) { unpacked = {}; _.each(value, function(sub_value, key) { unpacked[key] = that._unpack_models(sub_value); }); - return unpacked; - + return utils.resolve_promises_dict(unpacked); } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") { - var model = this.widget_manager.get_model(value.slice(10, value.length)); - if (model) { - return model; - } else { - return value; - } + // get_model returns a promise already + return this.widget_manager.get_model(value.slice(10, value.length)); } else { - return value; + return Promise.resolve(value); } }, @@ -304,7 +295,7 @@ define(["widgets/js/manager", this.options = parameters.options; this.child_model_views = {}; this.child_views = {}; - this.id = this.id || IPython.utils.uuid(); + this.id = this.id || utils.uuid(); this.model.views[this.id] = this; this.on('displayed', function() { this.is_displayed = true; @@ -318,29 +309,19 @@ define(["widgets/js/manager", }, create_child_view: function(child_model, options) { - // Create and return a child view. - // - // -given a model and (optionally) a view name if the view name is - // not given, it defaults to the model's default view attribute. - - // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior - // it would be great to have the widget manager add the cell metadata - // to the subview without having to add it here. + // Create and promise that resolves to a child view of a given model var that = this; - var old_callback = options.callback || function(view) {}; - options = $.extend({ parent: this, success: function(child_view) { + options = $.extend({ parent: this }, options || {}); + return this.model.widget_manager.create_view(child_model, options).then(function(child_view) { // Associate the view id with the model id. if (that.child_model_views[child_model.id] === undefined) { that.child_model_views[child_model.id] = []; } that.child_model_views[child_model.id].push(child_view.id); - // Remember the view by id. that.child_views[child_view.id] = child_view; - old_callback(child_view); - }}, options || {}); - - this.model.widget_manager.create_view(child_model, options); + return child_view; + }, utils.reject("Couldn't create child view")); }, pop_child_view: function(child_model) { diff --git a/IPython/html/static/widgets/js/widget_box.js b/IPython/html/static/widgets/js/widget_box.js index 1d2edcbf0..abcc8babd 100644 --- a/IPython/html/static/widgets/js/widget_box.js +++ b/IPython/html/static/widgets/js/widget_box.js @@ -4,8 +4,9 @@ define([ "widgets/js/widget", "jqueryui", + "base/js/utils", "bootstrap", -], function(widget, $){ +], function(widget, $, utils){ var BoxView = widget.DOMWidgetView.extend({ initialize: function(){ @@ -75,14 +76,17 @@ define([ add_child_model: function(model) { // Called when a model is added to the children list. var that = this; - this.create_child_view(model, {callback: function(view) { - that.$box.append(view.$el); + var dummy = $('
'); + that.$box.append(dummy); + return this.create_child_view(model).then(function(view) { + dummy.replaceWith(view.el); // Trigger the displayed event of the child view. that.after_displayed(function() { view.trigger('displayed'); }); - }}); + return view; + }, utils.reject("Couldn't add child view to box", true)); }, }); diff --git a/IPython/html/static/widgets/js/widget_selectioncontainer.js b/IPython/html/static/widgets/js/widget_selectioncontainer.js index 40b6e4307..f7cdd0758 100644 --- a/IPython/html/static/widgets/js/widget_selectioncontainer.js +++ b/IPython/html/static/widgets/js/widget_selectioncontainer.js @@ -114,9 +114,10 @@ define([ accordion_group.container_index = container_index; this.model_containers[model.id] = accordion_group; - this.create_child_view(model, {callback: function(view) { - accordion_inner.append(view.$el); - + var dummy = $('
'); + accordion_inner.append(dummy); + return this.create_child_view(model).then(function(view) { + dummy.replaceWith(view.$el); that.update(); that.update_titles(); @@ -124,7 +125,8 @@ define([ that.after_displayed(function() { view.trigger('displayed'); }); - }}); + return view; + }, utils.reject("Couldn't add child view to box", true)); }, }); @@ -186,36 +188,40 @@ define([ .css('list-style-type', 'none') .appendTo(this.$tabs); - this.create_child_view(model, {callback: function(view) { - view.parent_tab = tab; - var tab_text = $('') - .attr('href', '#' + uuid) - .attr('data-toggle', 'tab') - .text('Page ' + index) - .appendTo(tab) - .click(function (e) { - - // Calling model.set will trigger all of the other views of the - // model to update. - that.model.set("selected_index", index, {updated_view: that}); - that.touch(); - that.select_page(index); - }); - tab.tab_text_index = that.containers.push(tab_text) - 1; + var tab_text = $('') + .attr('href', '#' + uuid) + .attr('data-toggle', 'tab') + .text('Page ' + index) + .appendTo(tab) + .click(function (e) { + + // Calling model.set will trigger all of the other views of the + // model to update. + that.model.set("selected_index", index, {updated_view: that}); + that.touch(); + that.select_page(index); + }); + tab.tab_text_index = that.containers.push(tab_text) - 1; + + var dummy = $('
'); + var contents_div = $('
', {id: uuid}) + .addClass('tab-pane') + .addClass('fade') + .append(dummy) + .appendTo(that.$tab_contents); - var contents_div = $('
', {id: uuid}) - .addClass('tab-pane') - .addClass('fade') - .append(view.$el) - .appendTo(that.$tab_contents); + return this.create_child_view(model).then(function(view) { + dummy.replaceWith(view.$el); + view.parent_tab = tab; view.parent_container = contents_div; // Trigger the displayed event of the child view. that.after_displayed(function() { view.trigger('displayed'); }); - }}); + return view; + }, utils.reject("Couldn't add child view to box", true)); }, update: function(options) { diff --git a/IPython/html/templates/page.html b/IPython/html/templates/page.html index 93ba4b5f7..f024bb53e 100644 --- a/IPython/html/templates/page.html +++ b/IPython/html/templates/page.html @@ -27,9 +27,9 @@ bootstrap: 'components/bootstrap/js/bootstrap.min', bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min', jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min', - moment: "components/moment/moment", + moment: 'components/moment/moment', codemirror: 'components/codemirror', - termjs: "components/term.js/src/term", + termjs: 'components/term.js/src/term', contents: '{{ contents_js_source }}', }, shim: { diff --git a/IPython/html/tests/util.js b/IPython/html/tests/util.js index 464751cae..61f0a23c0 100644 --- a/IPython/html/tests/util.js +++ b/IPython/html/tests/util.js @@ -22,6 +22,35 @@ casper.open_new_notebook = function () { }); 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 @@ -151,10 +180,31 @@ casper.wait_for_widget = function (widget_info) { // 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 (m) { - return IPython.notebook.kernel.widget_manager.get_model(m).pending_msgs; - }, {m: widget_info.model_id}); + 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; @@ -285,6 +335,15 @@ casper.execute_cell_then = function(index, then_callback, expect_failure) { 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. @@ -655,7 +714,10 @@ casper.print_log = function () { casper.on("page.error", function onError(msg, trace) { // show errors in the browser - this.echo("Page Error!"); + 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; @@ -664,12 +726,15 @@ casper.on("page.error", function onError(msg, trace) { if (file === "phantomjs://webpage.evaluate()") { file = "evaluate"; } - this.echo("line " + frame.line + " of " + file); - if (frame.function.length > 0) { - this.echo("in " + frame.function); + // 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); } - this.echo(msg); }); @@ -680,7 +745,8 @@ casper.capture_log = function () { 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) @@ -696,12 +762,38 @@ casper.capture_log = function () { if (current_errors > seen_errors && captured_log.length > 0) { casper.echo("\nCaptured console.log:"); for (var i = 0; i < captured_log.length; i++) { - casper.echo(" " + captured_log[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(); diff --git a/IPython/html/tests/widgets/manager.js b/IPython/html/tests/widgets/manager.js index fed79fd85..3a0120325 100644 --- a/IPython/html/tests/widgets/manager.js +++ b/IPython/html/tests/widgets/manager.js @@ -18,12 +18,11 @@ casper.notebook_test(function () { this.evaluate(function() { IPython.notebook.kernel.widget_manager.create_model({ model_name: 'WidgetModel', - widget_class: 'IPython.html.widgets.widget_int.IntSlider', - init_state_callback: function(model) { + widget_class: 'IPython.html.widgets.widget_int.IntSlider'}) + .then(function(model) { console.log('Create success!', model); window.slider_id = model.id; - } - }); + }, function(error) { console.log(error); }); }); }); diff --git a/IPython/html/tests/widgets/widget.js b/IPython/html/tests/widgets/widget.js index e79a91544..d3c6b6d2a 100644 --- a/IPython/html/tests/widgets/widget.js +++ b/IPython/html/tests/widgets/widget.js @@ -59,13 +59,27 @@ casper.notebook_test(function () { JSON.stringify(input) + ' passed through Model._pack_model unchanged'); }; var test_unpack = function (input) { - var output = that.evaluate(function(input) { + that.thenEvaluate(function(input) { + window.results = undefined; var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined); - var results = model._unpack_models(input); - return results; + model._unpack_models(input).then(function(results) { + window.results = results; + }); }, {input: input}); - that.test.assert(recursive_compare(input, output), - JSON.stringify(input) + ' passed through Model._unpack_model unchanged'); + + that.waitFor(function check() { + return that.evaluate(function() { + return window.results; + }); + }); + + that.then(function() { + var results = that.evaluate(function() { + return window.results; + }); + that.test.assert(recursive_compare(input, results), + JSON.stringify(input) + ' passed through Model._unpack_model unchanged'); + }); }; var test_packing = function(input) { test_pack(input); @@ -84,7 +98,7 @@ casper.notebook_test(function () { test_packing([String('hi'), Date("Thu Nov 13 2014 13:46:21 GMT-0500")]) // Test multi-set, single touch code. First create a custom widget. - this.evaluate(function() { + this.thenEvaluate(function() { var MultiSetView = IPython.DOMWidgetView.extend({ render: function(){ this.model.set('a', 1); diff --git a/IPython/html/tests/widgets/widget_bool.js b/IPython/html/tests/widgets/widget_bool.js index 35e755be2..3586271cb 100644 --- a/IPython/html/tests/widgets/widget_bool.js +++ b/IPython/html/tests/widgets/widget_bool.js @@ -1,85 +1,90 @@ // Test widget bool class casper.notebook_test(function () { - index = this.append_cell( - 'from IPython.html import widgets\n' + - 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); + // Create a checkbox and togglebutton. var bool_index = this.append_cell( + 'from IPython.html import widgets\n' + + 'from IPython.display import display, clear_output\n' + 'bool_widgets = [widgets.Checkbox(description="Title", value=True),\n' + ' widgets.ToggleButton(description="Title", value=True)]\n' + 'display(bool_widgets[0])\n' + 'display(bool_widgets[1])\n' + 'print("Success")'); this.execute_cell_then(bool_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create bool widget cell executed with correct output.'); + }); - this.test.assert(this.cell_element_exists(index, + // Wait for the widgets to actually display. + var widget_checkbox_selector = '.widget-area .widget-subarea .widget-hbox input'; + var widget_togglebutton_selector = '.widget-area .widget-subarea button'; + this.wait_for_element(bool_index, widget_checkbox_selector); + this.wait_for_element(bool_index, widget_togglebutton_selector); + + // Continue the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(bool_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea .widget-hbox input'), + this.test.assert(this.cell_element_exists(bool_index, + widget_checkbox_selector), 'Checkbox exists.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea .widget-hbox input', 'prop', ['checked']), + this.test.assert(this.cell_element_function(bool_index, + widget_checkbox_selector, 'prop', ['checked']), 'Checkbox is checked.'); - this.test.assert(this.cell_element_exists(index, + this.test.assert(this.cell_element_exists(bool_index, '.widget-area .widget-subarea .widget-hbox .widget-label'), 'Checkbox label exists.'); - this.test.assert(this.cell_element_function(index, + this.test.assert(this.cell_element_function(bool_index, '.widget-area .widget-subarea .widget-hbox .widget-label', 'html')=="Title", 'Checkbox labeled correctly.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea button'), + this.test.assert(this.cell_element_exists(bool_index, + widget_togglebutton_selector), 'Toggle button exists.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea button', 'html')=="Title", + this.test.assert(this.cell_element_function(bool_index, + widget_togglebutton_selector, 'html')=="Title", 'Toggle button labeled correctly.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea button', 'hasClass', ['active']), + this.test.assert(this.cell_element_function(bool_index, + widget_togglebutton_selector, 'hasClass', ['active']), 'Toggle button is toggled.'); - }); + // Try changing the state of the widgets programatically. index = this.append_cell( 'bool_widgets[0].value = False\n' + 'bool_widgets[1].value = False\n' + 'print("Success")'); this.execute_cell_then(index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Change bool widget value cell executed with correct output.'); this.test.assert(! this.cell_element_function(bool_index, - '.widget-area .widget-subarea .widget-hbox input', 'prop', ['checked']), + widget_checkbox_selector, 'prop', ['checked']), 'Checkbox is not checked. (1)'); this.test.assert(! this.cell_element_function(bool_index, - '.widget-area .widget-subarea button', 'hasClass', ['active']), + widget_togglebutton_selector, 'hasClass', ['active']), 'Toggle button is not toggled. (1)'); // Try toggling the bool by clicking on the checkbox. - this.cell_element_function(bool_index, '.widget-area .widget-subarea .widget-hbox input', 'click'); + this.cell_element_function(bool_index, widget_checkbox_selector, 'click'); this.test.assert(this.cell_element_function(bool_index, - '.widget-area .widget-subarea .widget-hbox input', 'prop', ['checked']), + widget_checkbox_selector, 'prop', ['checked']), 'Checkbox is checked. (2)'); // Try toggling the bool by clicking on the toggle button. - this.cell_element_function(bool_index, '.widget-area .widget-subarea button', 'click'); + this.cell_element_function(bool_index, widget_togglebutton_selector, 'click'); this.test.assert(this.cell_element_function(bool_index, - '.widget-area .widget-subarea button', 'hasClass', ['active']), + widget_togglebutton_selector, 'hasClass', ['active']), 'Toggle button is toggled. (3)'); }); diff --git a/IPython/html/tests/widgets/widget_box.js b/IPython/html/tests/widgets/widget_box.js index 6e1f242b7..617423d0c 100644 --- a/IPython/html/tests/widgets/widget_box.js +++ b/IPython/html/tests/widgets/widget_box.js @@ -1,12 +1,10 @@ // Test container class casper.notebook_test(function () { - index = this.append_cell( - 'from IPython.html import widgets\n' + - 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); + // Create a box widget. var container_index = this.append_cell( + 'from IPython.html import widgets\n' + + 'from IPython.display import display, clear_output\n' + 'container = widgets.Box()\n' + 'button = widgets.Button()\n'+ 'container.children = [button]\n'+ @@ -14,24 +12,32 @@ casper.notebook_test(function () { 'container._dom_classes = ["my-test-class"]\n'+ 'print("Success")\n'); this.execute_cell_then(container_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create container cell executed with correct output.'); + }); - this.test.assert(this.cell_element_exists(index, + // Wait for the widgets to actually display. + var widget_box_selector = '.widget-area .widget-subarea .widget-box'; + var widget_box_button_selector = '.widget-area .widget-subarea .widget-box button'; + this.wait_for_element(container_index, widget_box_selector); + this.wait_for_element(container_index, widget_box_button_selector); + + // Continue with the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(container_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea .widget-box'), + this.test.assert(this.cell_element_exists(container_index, + widget_box_selector), 'Widget container exists.'); - this.test.assert(this.cell_element_exists(index, + this.test.assert(this.cell_element_exists(container_index, '.widget-area .widget-subarea .my-test-class'), '_dom_classes works.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea .my-test-class button'), + this.test.assert(this.cell_element_exists(container_index, + widget_box_button_selector), 'Container parent/child relationship works.'); }); @@ -61,20 +67,26 @@ casper.notebook_test(function () { '_dom_classes can be used to remove a class.'); }); - index = this.append_cell( + var boxalone_index = this.append_cell( 'display(button)\n'+ 'print("Success")\n'); - this.execute_cell_then(index, function(index){ - + this.execute_cell_then(boxalone_index, function(index){ this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Display container child executed with correct output.'); + }); + + // Wait for the widget to actually display. + var widget_button_selector = '.widget-area .widget-subarea button'; + this.wait_for_element(boxalone_index, widget_button_selector); - this.test.assert(! this.cell_element_exists(index, - '.widget-area .widget-subarea .widget-box'), + // Continue with the tests. + this.then(function() { + this.test.assert(! this.cell_element_exists(boxalone_index, + widget_box_selector), 'Parent container not displayed.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea button'), + this.test.assert(this.cell_element_exists(boxalone_index, + widget_button_selector), 'Child displayed.'); }); }); \ No newline at end of file diff --git a/IPython/html/tests/widgets/widget_button.js b/IPython/html/tests/widgets/widget_button.js index 3329a89bd..80f86732a 100644 --- a/IPython/html/tests/widgets/widget_button.js +++ b/IPython/html/tests/widgets/widget_button.js @@ -1,12 +1,8 @@ // Test widget button class casper.notebook_test(function () { - index = this.append_cell( + var button_index = this.append_cell( 'from IPython.html import widgets\n' + 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); - - var button_index = this.append_cell( 'button = widgets.Button(description="Title")\n' + 'display(button)\n' + 'print("Success")\n' + @@ -14,24 +10,30 @@ casper.notebook_test(function () { ' display("Clicked")\n' + 'button.on_click(handle_click)'); this.execute_cell_then(button_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create button cell executed with correct output.'); + }); + + // Wait for the widgets to actually display. + var widget_button_selector = '.widget-area .widget-subarea button'; + this.wait_for_element(button_index, widget_button_selector); - this.test.assert(this.cell_element_exists(index, + // Continue with the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(button_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea button'), + this.test.assert(this.cell_element_exists(button_index, + widget_button_selector), 'Widget button exists.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea button', 'html')=='Title', + this.test.assert(this.cell_element_function(button_index, + widget_button_selector, 'html')=='Title', 'Set button description.'); - this.cell_element_function(index, - '.widget-area .widget-subarea button', 'click'); + this.cell_element_function(button_index, + widget_button_selector, 'click'); }); this.wait_for_output(button_index, 1); diff --git a/IPython/html/tests/widgets/widget_float.js b/IPython/html/tests/widgets/widget_float.js index 291efb314..d253a80b7 100644 --- a/IPython/html/tests/widgets/widget_float.js +++ b/IPython/html/tests/widgets/widget_float.js @@ -1,26 +1,28 @@ // Test widget float class casper.notebook_test(function () { - index = this.append_cell( - 'from IPython.html import widgets\n' + - 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); - var float_text = {}; float_text.query = '.widget-area .widget-subarea .my-second-float-text input'; float_text.index = this.append_cell( + 'from IPython.html import widgets\n' + + 'from IPython.display import display, clear_output\n' + 'float_widget = widgets.FloatText()\n' + 'display(float_widget)\n' + 'float_widget._dom_classes = ["my-second-float-text"]\n' + 'print(float_widget.model_id)\n'); this.execute_cell_then(float_text.index, function(index){ float_text.model_id = this.get_output_cell(index).text.trim(); - - this.test.assert(this.cell_element_exists(index, + }); + + // Wait for the widget to actually display. + this.wait_for_element(float_text.index, float_text.query); + + // Continue with the tests + this.then(function(){ + this.test.assert(this.cell_element_exists(float_text.index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, float_text.query), + this.test.assert(this.cell_element_exists(float_text.index, float_text.query), 'Widget float textbox exists.'); this.cell_element_function(float_text.index, float_text.query, 'val', ['']); @@ -64,18 +66,23 @@ casper.notebook_test(function () { '[display(floatrange[i]) for i in range(2)]\n' + 'print("Success")\n'); this.execute_cell_then(slider.index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create float range cell executed with correct output.'); + }); + + // Wait for the widgets to actually display. + this.wait_for_element(slider.index, slider.query); + this.wait_for_element(slider.index, float_text_query); - this.test.assert(this.cell_element_exists(index, + this.then(function(){ + this.test.assert(this.cell_element_exists(slider.index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, slider.query), + this.test.assert(this.cell_element_exists(slider.index, slider.query), 'Widget slider exists.'); - this.test.assert(this.cell_element_exists(index, float_text_query), + this.test.assert(this.cell_element_exists(slider.index, float_text_query), 'Widget float textbox exists.'); }); diff --git a/IPython/html/tests/widgets/widget_image.js b/IPython/html/tests/widgets/widget_image.js index ba10d3543..507215543 100644 --- a/IPython/html/tests/widgets/widget_image.js +++ b/IPython/html/tests/widgets/widget_image.js @@ -26,19 +26,23 @@ casper.notebook_test(function () { 'display(image)\n' + 'print("Success")\n'); this.execute_cell_then(image_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create image executed with correct output.'); + }); + + // Wait for the widget to actually display. + var img_selector = '.widget-area .widget-subarea img'; + this.wait_for_element(image_index, img_selector); - this.test.assert(this.cell_element_exists(index, + this.then(function(){ + this.test.assert(this.cell_element_exists(image_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - var img_sel = '.widget-area .widget-subarea img'; - this.test.assert(this.cell_element_exists(index, img_sel), 'Image exists.'); + this.test.assert(this.cell_element_exists(image_index, img_selector), 'Image exists.'); // Verify that the image's base64 data has made it into the DOM. - var img_src = this.cell_element_function(image_index, img_sel, 'attr', ['src']); + var img_src = this.cell_element_function(image_index, img_selector, 'attr', ['src']); this.test.assert(img_src.indexOf(test_jpg) > -1, 'Image src data exists.'); }); }); \ No newline at end of file diff --git a/IPython/html/tests/widgets/widget_int.js b/IPython/html/tests/widgets/widget_int.js index 086235198..8ca6ca81a 100644 --- a/IPython/html/tests/widgets/widget_int.js +++ b/IPython/html/tests/widgets/widget_int.js @@ -1,26 +1,28 @@ // Test widget int class casper.notebook_test(function () { - index = this.append_cell( - 'from IPython.html import widgets\n' + - 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); - var int_text = {}; int_text.query = '.widget-area .widget-subarea .my-second-int-text input'; int_text.index = this.append_cell( + 'from IPython.html import widgets\n' + + 'from IPython.display import display, clear_output\n' + 'int_widget = widgets.IntText()\n' + 'display(int_widget)\n' + 'int_widget._dom_classes = ["my-second-int-text"]\n' + 'print(int_widget.model_id)\n'); this.execute_cell_then(int_text.index, function(index){ int_text.model_id = this.get_output_cell(index).text.trim(); - - this.test.assert(this.cell_element_exists(index, + }); + + // Wait for the widget to actually display. + this.wait_for_element(int_text.index, int_text.query); + + // Continue with the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(int_text.index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, int_text.query), + this.test.assert(this.cell_element_exists(int_text.index, int_text.query), 'Widget int textbox exists.'); this.cell_element_function(int_text.index, int_text.query, 'val', ['']); @@ -54,13 +56,6 @@ casper.notebook_test(function () { this.test.assertEquals(this.get_output_cell(index).text, '12\n', 'Invald int textbox value caught and filtered.'); }); - - index = this.append_cell( - 'from IPython.html import widgets\n' + - 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); - var slider_query = '.widget-area .widget-subarea .slider'; var int_text2 = {}; @@ -73,15 +68,22 @@ casper.notebook_test(function () { 'print(intrange[0].model_id)\n'); this.execute_cell_then(int_text2.index, function(index){ int_text2.model_id = this.get_output_cell(index).text.trim(); + }); + + // Wait for the widgets to actually display. + this.wait_for_element(int_text2.index, int_text2.query); + this.wait_for_element(int_text2.index, slider_query); - this.test.assert(this.cell_element_exists(index, + // Continue with the tests. + this.then(function(){ + this.test.assert(this.cell_element_exists(int_text2.index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, slider_query), + this.test.assert(this.cell_element_exists(int_text2.index, slider_query), 'Widget slider exists.'); - this.test.assert(this.cell_element_exists(index, int_text2.query), + this.test.assert(this.cell_element_exists(int_text2.index, int_text2.query), 'Widget int textbox exists.'); }); diff --git a/IPython/html/tests/widgets/widget_selection.js b/IPython/html/tests/widgets/widget_selection.js index e4e4524f2..ea87a3830 100644 --- a/IPython/html/tests/widgets/widget_selection.js +++ b/IPython/html/tests/widgets/widget_selection.js @@ -58,21 +58,30 @@ casper.notebook_test(function () { this.execute_cell_then(selection_index, function(index){ this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create selection cell executed with correct output.'); + }); + + // Wait for the widgets to actually display. + this.wait_for_element(selection_index, combo_selector); + this.wait_for_element(selection_index, multibtn_selector); + this.wait_for_element(selection_index, radio_selector); + this.wait_for_element(selection_index, list_selector); - this.test.assert(this.cell_element_exists(index, + // Continue with the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(selection_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, combo_selector), + this.test.assert(this.cell_element_exists(selection_index, combo_selector), 'Widget combobox exists.'); - this.test.assert(this.cell_element_exists(index, multibtn_selector), + this.test.assert(this.cell_element_exists(selection_index, multibtn_selector), 'Widget multibutton exists.'); - this.test.assert(this.cell_element_exists(index, radio_selector), + this.test.assert(this.cell_element_exists(selection_index, radio_selector), 'Widget radio buttons exists.'); - this.test.assert(this.cell_element_exists(index, list_selector), + this.test.assert(this.cell_element_exists(selection_index, list_selector), 'Widget list exists.'); // Verify that no items are selected. diff --git a/IPython/html/tests/widgets/widget_selectioncontainer.js b/IPython/html/tests/widgets/widget_selectioncontainer.js index 57e9bd1c2..d8680addd 100644 --- a/IPython/html/tests/widgets/widget_selectioncontainer.js +++ b/IPython/html/tests/widgets/widget_selectioncontainer.js @@ -18,20 +18,22 @@ casper.notebook_test(function () { 'multicontainer.selected_index = 0\n' + 'print("Success")\n'); this.execute_cell_then(multicontainer1_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create multicontainer cell executed with correct output. (1)'); + }); + + // Wait for the widget to actually display. + this.wait_for_element(multicontainer1_index, multicontainer1_query); - this.test.assert(this.cell_element_exists(index, + // Continue with the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(multicontainer1_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, multicontainer1_query), + this.test.assert(this.cell_element_exists(multicontainer1_index, multicontainer1_query), 'Widget tab list exists.'); - this.test.assert(this.cell_element_exists(index, multicontainer1_query), - 'First widget tab list exists.'); - // JQuery selector is 1 based this.click(multicontainer1_query + ' li:nth-child(2) a'); }); @@ -74,23 +76,28 @@ casper.notebook_test(function () { 'multicontainer.selected_index = 0\n' + 'print("Success")\n'); this.execute_cell_then(multicontainer2_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create multicontainer cell executed with correct output. (2)'); + }); + + // Wait for the widget to actually display. + this.wait_for_element(multicontainer2_index, multicontainer2_query); - this.test.assert(this.cell_element_exists(index, + // Continue with the tests. + this.then(function() { + this.test.assert(this.cell_element_exists(multicontainer2_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, multicontainer2_query), + this.test.assert(this.cell_element_exists(multicontainer2_index, multicontainer2_query), 'Widget accordion exists.'); - this.test.assert(this.cell_element_exists(index, multicontainer2_query + + this.test.assert(this.cell_element_exists(multicontainer2_index, multicontainer2_query + ' .panel:nth-child(1) .panel-collapse'), 'First accordion page exists.'); // JQuery selector is 1 based - this.test.assert(this.cell_element_function(index, multicontainer2_query + + this.test.assert(this.cell_element_function(multicontainer2_index, multicontainer2_query + ' .panel.panel-default:nth-child(3) .panel-heading .accordion-toggle', 'html')=='good', 'Accordion page title set (before display).'); diff --git a/IPython/html/tests/widgets/widget_string.js b/IPython/html/tests/widgets/widget_string.js index 54e669d10..c01afb749 100644 --- a/IPython/html/tests/widgets/widget_string.js +++ b/IPython/html/tests/widgets/widget_string.js @@ -1,12 +1,8 @@ // Test widget string class casper.notebook_test(function () { - index = this.append_cell( + var string_index = this.append_cell( 'from IPython.html import widgets\n' + 'from IPython.display import display, clear_output\n' + - 'print("Success")'); - this.execute_cell_then(index); - - var string_index = this.append_cell( 'string_widget = [widgets.Text(value = "xyz", placeholder = "abc"),\n' + ' widgets.Textarea(value = "xyz", placeholder = "def"),\n' + ' widgets.HTML(value = "xyz"),\n' + @@ -14,40 +10,50 @@ casper.notebook_test(function () { '[display(widget) for widget in string_widget]\n'+ 'print("Success")'); this.execute_cell_then(string_index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', 'Create string widget cell executed with correct output.'); + }); + + // Wait for the widget to actually display. + var textbox_selector = '.widget-area .widget-subarea .widget-hbox input[type=text]'; + var textarea_selector = '.widget-area .widget-subarea .widget-hbox textarea'; + var latex_selector = '.widget-area .widget-subarea div span.MathJax_Preview'; + this.wait_for_element(string_index, textbox_selector); + this.wait_for_element(string_index, textarea_selector); + this.wait_for_element(string_index, latex_selector); - this.test.assert(this.cell_element_exists(index, + // Continue with the tests. + this.then(function(){ + this.test.assert(this.cell_element_exists(string_index, '.widget-area .widget-subarea'), 'Widget subarea exists.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea .widget-hbox input[type=text]'), + this.test.assert(this.cell_element_exists(string_index, + textbox_selector), 'Textbox exists.'); - this.test.assert(this.cell_element_exists(index, - '.widget-area .widget-subarea .widget-hbox textarea'), + this.test.assert(this.cell_element_exists(string_index, + textarea_selector), 'Textarea exists.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea .widget-hbox textarea', 'val')=='xyz', + this.test.assert(this.cell_element_function(string_index, + textarea_selector, 'val')=='xyz', 'Python set textarea value.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea .widget-hbox input[type=text]', 'val')=='xyz', + this.test.assert(this.cell_element_function(string_index, + textbox_selector, 'val')=='xyz', 'Python set textbox value.'); this.test.assert(this.cell_element_exists(string_index, - '.widget-area .widget-subarea div span.MathJax_Preview'), + latex_selector), 'MathJax parsed the LaTeX successfully.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea .widget-hbox textarea', 'attr', ['placeholder'])=='def', + this.test.assert(this.cell_element_function(string_index, + textarea_selector, 'attr', ['placeholder'])=='def', 'Python set textarea placeholder.'); - this.test.assert(this.cell_element_function(index, - '.widget-area .widget-subarea .widget-hbox input[type=text]', 'attr', ['placeholder'])=='abc', + this.test.assert(this.cell_element_function(string_index, + textbox_selector, 'attr', ['placeholder'])=='abc', 'Python set textbox placehoder.'); }); });