// ========================================================================== // Project: SproutCore
- JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // portions copyright @2011 Apple Inc. // License: Licensed under MIT license (see license.js) // ========================================================================== /*global module, test, ok, equals, stop, start */
(function () {
var pane = SC.ControlTestPane.design() .add("empty", SC.TextFieldView, { hint: "Full Name", value: '' }) .add("empty - no hint on focus", SC.TextFieldView, { hint: "Full Name", hintOnFocus: false, value: '' }) .add("with value", SC.TextFieldView, { hint: "Full Name", value: 'John Doe' }) .add("password", SC.TextFieldView, { type: "password", value: "I'm so secret" }) .add("password-hint", SC.TextFieldView, { hint: "Passwerd", type: "password", value: "I'm so secret" }) .add("disabled - empty", SC.TextFieldView, { hint: "Full Name", value: null, isEnabled: NO, isEditable: NO }) .add("disabled - with value", SC.TextFieldView, { hint: "Full Name", value: 'John Doe', isEnabled: NO, isEditable: NO }) .add("enabled - not editable - with value", SC.TextFieldView, { hint: "Full Name", value: 'John Doe', isEnabled: YES, isEditable: NO }) .add("textarea - empty", SC.TextFieldView, { hint: "Full Name", value: '', isTextArea: YES }) .add("textarea - with value", SC.TextFieldView, { hint: "Full Name", value: 'John Doe', isTextArea: YES }) .add("textarea - disabled - empty", SC.TextFieldView, { hint: "Full Name", value: '', isTextArea: YES, isEnabled: NO }) .add("textarea - disabled - with value", SC.TextFieldView, { hint: "Full Name", value: 'John Doe', isTextArea: YES, isEnabled: NO }) .add("aria-readonly", SC.TextFieldView, { hint: "Full Name", value: 'John Doe', isTextArea: YES, isEnabled: YES, isEditable: NO }); // .......................................................... // VERIFY STANDARD STATES // pane.verifyEmpty = function verifyEmpty(view, expectedHint) { var input = view.$('input'); var layer = view.$(); ok(!layer.hasClass('not-empty'), 'layer should not have not-empty class'); equals(input.val(), '', 'input should have empty value'); if (expectedHint) { var hint = view.$('.hint'); if (hint.length === 1) { hint = hint.text(); } else { hint = view.$('input'); hint = hint.attr('placeholder'); } equals(hint, expectedHint, 'hint span should have expected hint'); } }; pane.verifyNotEmpty = function verifyNotEmpty(view, expectedValue, expectedHint) { var input = view.$('input'); var layer = view.$(); ok(layer.hasClass('not-empty'), 'layer should have not-empty class'); equals(input.val(), expectedValue, 'input should have value'); if (expectedHint) { var hint = view.$('.hint'); if (hint.length === 1) { hint = hint.text(); } else { hint = view.$('input'); hint = hint.attr('placeholder'); } equals(hint, expectedHint, 'hint span should have expected hint'); } }; pane.verifyDisabled = function verifyDisabled(view, isDisabled) { var layer = view.$(); var input = view.$('input'); if (isDisabled) { ok(layer.hasClass('disabled'), 'layer should have disabled class'); ok(input.attr('disabled'), 'input should have disabled attr'); } else { ok(!layer.hasClass('disabled'), 'layer should not have disabled class'); ok(!input.attr('disabled'), 'input should not have disabled attr'); } }; pane.verifyReadOnly = function verifyReadonly(view, isReadOnly) { var input = view.$('input'); if (isReadOnly) { ok(input.attr('readOnly'), 'input should have readOnly attr'); } else { ok(!input.attr('readOnly'), 'input should not have readOnly attr'); } }; // .......................................................... // TEST INITIAL STATES // module('SC.TextFieldView: Initial States', pane.standardSetup()); test("empty", function () { var view = pane.view('empty'); pane.verifyEmpty(view, 'Full Name'); pane.verifyDisabled(view, NO); }); test("empty - no hint on focus", function () { var view = pane.view('empty - no hint on focus'); pane.verifyEmpty(view, 'Full Name'); pane.verifyDisabled(view, NO); }); test("with value", function () { var view = pane.view('with value'); pane.verifyNotEmpty(view, 'John Doe', 'Full Name'); pane.verifyDisabled(view, NO); }); test("password", function () { var view = pane.view('password'); pane.verifyNotEmpty(view, 'I\'m so secret'); pane.verifyDisabled(view, NO); }); test("password with hint", function () { var view = pane.view('password-hint'); pane.verifyNotEmpty(view, 'I\'m so secret', 'Passwerd'); pane.verifyDisabled(view, NO); }); test("disabled - empty", function () { var view = pane.view('disabled - empty'); pane.verifyEmpty(view, 'Full Name'); pane.verifyDisabled(view, YES); }); test("disabled - with value", function () { var view = pane.view('disabled - with value'); pane.verifyNotEmpty(view, 'John Doe', 'Full Name'); pane.verifyDisabled(view, YES); }); test("enabled - not editable - with value", function () { var view = pane.view('enabled - not editable - with value'); pane.verifyNotEmpty(view, 'John Doe', 'Full Name'); pane.verifyReadOnly(view, YES); }); test("textarea - empty", function () { var view = pane.view('empty'); pane.verifyEmpty(view, 'Full Name'); pane.verifyDisabled(view, NO); }); test("textarea - with value", function () { var view = pane.view('with value'); pane.verifyNotEmpty(view, 'John Doe', 'Full Name'); pane.verifyDisabled(view, NO); }); test("textarea - disabled - empty", function () { var view = pane.view('disabled - empty'); pane.verifyEmpty(view, 'Full Name'); pane.verifyDisabled(view, YES); }); test("textarea - disabled - with value", function () { var view = pane.view('disabled - with value'); pane.verifyNotEmpty(view, 'John Doe', 'Full Name'); pane.verifyDisabled(view, YES); }); // .......................................................... // TEST CHANGING VIEWS // module('SC.TextFieldView: Changing Values', pane.standardSetup()); test("changing value from empty -> value", function () { var view = pane.view('empty'); // test changing value updates like it should SC.run(function () { view.set('value', 'John Doe'); }); pane.verifyNotEmpty(view, 'John Doe', 'Full Name'); }); test("disabling view", function () { var view = pane.view('empty'); // test changing enabled state updates like it should SC.run(function () { view.set('isEnabled', NO); }); pane.verifyDisabled(view, YES); }); test("changing value to null", function () { var view = pane.view('with value'); // test changing value updates like it should SC.run(function () { view.set('value', null); }); equals(view.get('fieldValue'), null, 'should have empty fieldValue'); pane.verifyEmpty(view, 'Full Name'); }); test("enabling disabled view", function () { var view = pane.view('disabled - empty'); // test changing enabled state updates like it should SC.run(function () { view.set('isEnabled', YES); }); pane.verifyDisabled(view, NO); }); test("changing isEditable", function () { var view = pane.view('enabled - not editable - with value'); // test changing isEditable state updates like it should SC.run(function () { view.set('isEditable', YES); }); pane.verifyReadOnly(view, NO); // test changing isEditable state updates like it should SC.run(function () { view.set('isEditable', NO); }); pane.verifyReadOnly(view, YES); }); test("changing value from not a textarea to a textarea", function () { // test the the SC.Event for 'change' gets wired up properly to the DOM element when it changes from input to textarea var view = pane.view('empty'); SC.run(function () { view.set('value', 'Original'); view.set('isTextArea', YES); }); var $textarea = view.$('textarea'); SC.Event.trigger($textarea, 'focus'); // simulate typing a letter SC.Event.trigger($textarea, 'keydown'); $textarea.val("My New Value"); SC.Event.trigger($textarea, 'keyup'); SC.Event.trigger($textarea, 'change'); SC.run(function () { view.fieldValueDidChange(); }); // wait a little bit to let text field propogate changes stop(); setTimeout(function () { start(); equals(view.get("value"), "My New Value", "SC.Event for change should get wired up properly"); }, 100); SC.run(function () { }); }); /** There was a bug that when a text field view has a value before it is appended, the hint line-height gets set to 0px. So if the value is removed, the hint is in the wrong spot. */ test("When a manual hint is visible, the line-height of the hint should be correct", function () { var view1 = pane.view('empty'), view2 = pane.view('with value'), hint = view1.$('.hint'); equals(hint.css('line-height'), "14px", "The line-height of the hint of an empty text field should be"); SC.run(function () { view2.set('value', null); }); hint = view2.$('.hint'); equals(hint.css('line-height'), "14px", "The line-height of the hint of a non-empty text field should be"); }); test("Changing maxLength", function () { var view = pane.view('with value'), input = view.$input(); equals(input.attr('maxLength'), 5096, 'Max length should begin at') SC.run(function () { view.set('maxLength', 1234); }); equals(input.attr('maxLength'), 1234, 'Max length should now be'); }); if (!SC.browser.isIE && !SC.platform.input.placeholder) { test("Changing value to null -- password field", function () { var view = pane.view('password-hint'), input = view.$('input'); SC.run(function () { view.set('value', null); }); equals(input.attr('type'), 'text', "When nulled out, field was converted to type text"); equals(input.val(), view.get('hint'), "When nulled out, field was given value equal to hint"); }); } // .......................................................... // TEST SELECTION SUPPORT // module('SC.TextFieldView: Selection Support', pane.standardSetup()); test("Setting the selection to a null value should fail", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.size = 10; // Avoid Firefox 3.5 issue var thrownException = null; try { view.set('selection', null); } catch (e) { thrownException = e.message; } ok(thrownException.indexOf !== undefined, 'an exception should have been thrown'); if (thrownException.indexOf !== undefined) { ok(thrownException.indexOf('must specify an SC.TextSelection instance') !== -1, 'the exception should be about not specifying an SC.TextSelection instance'); } }); test("Setting the selection to a non-SC.TextSelection value should fail", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.size = 10; // Avoid Firefox 3.5 issue var thrownException = null; try { view.set('selection', {start: 0, end: 0}); } catch (e) { thrownException = e.message; } ok(thrownException.indexOf !== undefined, 'an exception should have been thrown'); if (thrownException.indexOf !== undefined) { ok(thrownException.indexOf('must specify an SC.TextSelection instance') !== -1, 'the exception should be about not specifying an SC.TextSelection instance'); } }); test("Setting and then getting back the selection", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue var newSelection = SC.TextSelection.create({start: 2, end: 5}); view.set('selection', newSelection); var fetchedSelection = view.get('selection'); ok(fetchedSelection.get('start') === 2, 'the selection should start at index 2'); ok(fetchedSelection.get('end') === 5, 'the selection should end at index 4'); ok(fetchedSelection.get('length') === 3, 'the selection should have length 3'); }); test("Setting no selection direction", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue var newSelection = SC.TextSelection.create({ start: 2, end: 5, direction: 'none' }); view.set('selection', newSelection); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'none', 'the selection direction should be none'); }); test("Setting forward selection direction", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue var newSelection = SC.TextSelection.create({ start: 2, end: 5, direction: 'forward' }); view.set('selection', newSelection); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'forward', 'the selection direction should be forward'); }); test("Setting backward selection direction", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue var newSelection = SC.TextSelection.create({ start: 2, end: 5, direction: 'backward' }); view.set('selection', newSelection); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'backward', 'the selection direction should be backward'); }); test("Getting no selection direction", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue fieldElement.setSelectionRange(2, 5, 'none'); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'none', 'the selection direction should be none'); }); test("Getting forward selection direction", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue fieldElement.setSelectionRange(2, 5, 'forward'); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'forward', 'the selection direction should be forward'); }); test("Getting backward selection direction", function () { var view = pane.view('with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.size = 10; // Avoid Firefox 3.5 issue fieldElement.setSelectionRange(2, 5, 'backward'); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'backward', 'the selection direction should be backward'); }); test("Setting textarea selection direction", function () { var view = pane.view('textarea - with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); var newSelection = SC.TextSelection.create({ start: 2, end: 5, direction: 'backward' }); view.set('selection', newSelection); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'backward', 'the selection direction should be backward'); }); test("Getting textarea selection direction", function () { var view = pane.view('textarea - with value'); var fieldElement = view.$input()[0]; fieldElement.focus(); fieldElement.setSelectionRange(2, 5, 'backward'); var fetchedSelection = view.get('selection'); ok(!SC.platform.input.selectionDirection || fetchedSelection.get('direction') === 'backward', 'the selection direction should be backward'); }); // .......................................................... // TEST ACCESSORY VIEWS // module('SC.TextFieldView: Accessory Views', pane.standardSetup()); test("Adding left accessory view", function () { var view = pane.view('with value'), accessoryView; // test adding accessory view adds the view like it should SC.run(function () { accessoryView = SC.View.create({ layout: { top: 1, left: 2, width: 16, height: 16 } }); view.set('leftAccessoryView', accessoryView); }); ok(view.get('leftAccessoryView') === accessoryView, 'left accessory view should be set to ' + accessoryView.toString()); ok(view.get('childViews').length === 1, 'there should only be one child view'); ok(view.get('childViews')[0] === accessoryView, 'first child view should be set to ' + accessoryView.toString()); // The hint and padding elements should automatically have their 'left' // values set to the accessory view's offset + width // (18 = 2 left offset + 16 width) var paddingElement = view.$('.padding')[0]; ok(paddingElement.style.left === '18px', 'padding element should get 18px left'); // Test removing the accessory view. SC.run(function () { view.set('leftAccessoryView', null); }); ok(view.get('childViews').length === 0, 'after removing the left accessory view there should be no child views left'); ok(!paddingElement.style.left, 'after removing the left accessory view the padding element should have no left style'); }); test("Adding left accessory view changes style -- using design()", function () { var view = pane.view('with value'); // test adding accessory view adds the view like it should SC.run(function () { var accessoryView = SC.View.design({ layout: { top: 1, left: 2, width: 16, height: 16 } }); view.set('leftAccessoryView', accessoryView); }); // The hint and padding elements should automatically have their 'left' // values set to the accessory view's offset + width // (18 = 2 left offset + 16 width) var paddingElement = view.$('.padding')[0]; ok(paddingElement.style.left === '18px', 'padding element should get 18px left'); // Test removing the accessory view. SC.run(function () { view.set('leftAccessoryView', null); }); ok(!paddingElement.style.left, 'after removing the left accessory view the padding element should have no left style'); }); test("Adding right accessory view", function () { var view = pane.view('with value'), accessoryView; // test adding accessory view adds the view like it should SC.run(function () { accessoryView = SC.View.create({ layout: { top: 1, right: 3, width: 17, height: 16 } }); view.set('rightAccessoryView', accessoryView); }); ok(view.get('rightAccessoryView') === accessoryView, 'right accessory view should be set to ' + accessoryView.toString()); ok(view.get('childViews').length === 1, 'there should only be one child view'); ok(view.get('childViews')[0] === accessoryView, 'first child view should be set to ' + accessoryView.toString()); // The hint and padding elements should automatically have their 'right' // values set to the accessory view's offset + width // (20 = 3 right offset + 17 width) var paddingElement = view.$('.padding')[0]; ok(paddingElement.style.right === '20px', 'padding element should get 20px right'); // If a right accessory view is set with only 'left' (and not 'right') // defined in its layout, 'left' should be cleared out and 'right' should // be set to 0. SC.run(function () { accessoryView = SC.View.create({ layout: { top: 1, left: 2, width: 16, height: 16 } }); view.set('rightAccessoryView', accessoryView); }); ok(SC.none(view.get('rightAccessoryView').get('layout').left), "right accessory view created with 'left' rather than 'right' in layout should not have a value for layout.left"); ok(view.get('rightAccessoryView').get('layout').right === 0, "right accessory view created with 'left' rather than 'right' in layout should have layout.right set to 0"); // Test removing the accessory view. SC.run(function () { view.set('rightAccessoryView', null); }); ok(view.get('childViews').length === 0, 'after removing the right accessory view there should be no child views left'); ok(!paddingElement.style.right, 'after removing the right accessory view the padding element should have no right style'); }); test("Adding right accessory view changes style -- using design()", function () { var view = pane.view('with value'); // test adding accessory view adds the view like it should SC.run(function () { var accessoryView = SC.View.design({ layout: { top: 1, right: 3, width: 17, height: 16 } }); view.set('rightAccessoryView', accessoryView); }); // The hint and padding elements should automatically have their 'right' // values set to the accessory view's offset + width // (20 = 3 right offset + 17 width) var paddingElement = view.$('.padding')[0]; ok(paddingElement.style.right === '20px', 'padding element should get 20px right'); // Test removing the accessory view. SC.run(function () { view.set('rightAccessoryView', null); }); ok(!paddingElement.style.right, 'after removing the right accessory view the padding element should have no right style'); }); test("Adding both left and right accessory views", function () { var view = pane.view('with value'); // test adding accessory view adds the view like it should SC.run(function () { var leftAccessoryView = SC.View.create({ layout: { top: 1, left: 2, width: 16, height: 16 } }); view.set('leftAccessoryView', leftAccessoryView); var rightAccessoryView = SC.View.create({ layout: { top: 1, right: 3, width: 17, height: 16 } }); view.set('rightAccessoryView', rightAccessoryView); }); ok(view.get('childViews').length === 2, 'we should have two child views since we added both a left and a right accessory view'); // The hint and padding elements should automatically have their 'left' and // 'right' values set to the accessory views' offset + width // * left: 18 = 2 left offset + 16 width) // * right: 20 = 3 left offset + 17 width) var paddingElement = view.$('.padding')[0]; ok(paddingElement.style.left === '18px', 'padding element should get 18px left'); ok(paddingElement.style.right === '20px', 'padding element should get 20px right'); // Test removing the accessory views. SC.run(function () { view.set('rightAccessoryView', null); }); ok(view.get('childViews').length === 1, 'after removing the right accessory view there should be one child view left (the left accessory view)'); ok(!paddingElement.style.right, 'after removing the right accessory view the padding element should have no right style'); SC.run(function () { view.set('leftAccessoryView', null); }); ok(view.get('childViews').length === 0, 'after removing both accessory views there should be no child views left'); ok(!paddingElement.style.left, 'after removing the left accessory view the padding element should have no left style'); }); test("Adding both left and right accessory views changes style -- using design()", function () { var view = pane.view('with value'); // test adding accessory view adds the view like it should SC.run(function () { var leftAccessoryView = SC.View.design({ layout: { top: 1, left: 2, width: 16, height: 16 } }); view.set('leftAccessoryView', leftAccessoryView); var rightAccessoryView = SC.View.design({ layout: { top: 1, right: 3, width: 17, height: 16 } }); view.set('rightAccessoryView', rightAccessoryView); }); // The hint and padding elements should automatically have their 'left' and // 'right' values set to the accessory views' offset + width // * left: 18 = 2 left offset + 16 width) // * right: 20 = 3 left offset + 17 width) var paddingElement = view.$('.padding')[0]; ok(paddingElement.style.left === '18px', 'padding element should get 18px left'); ok(paddingElement.style.right === '20px', 'padding element should get 20px right'); // Test removing the accessory views. SC.run(function () { view.set('rightAccessoryView', null); }); ok(!paddingElement.style.right, 'after removing the right accessory view the padding element should have no right style'); SC.run(function () { view.set('leftAccessoryView', null); }); ok(!paddingElement.style.left, 'after removing the left accessory view the padding element should have no left style'); }); test("Accessory views should only be instantiated once", function () { var view = pane.view('with value'); var leftAccessoryViewInitCount = 0; var rightAccessoryViewInitCount = 0; // Test the left accessory view SC.run(function () { var leftAccessoryView = SC.View.design({ layout: { top: 1, left: 2, width: 16, height: 16 }, init: function () { sc_super(); leftAccessoryViewInitCount++; } }); view.set('leftAccessoryView', leftAccessoryView); }); // Check it equals(leftAccessoryViewInitCount, 1, 'the left accessory view should only be initialized once'); // Reset to null so it isn't created a second time when rightAccessoryView is set SC.run(function () { view.set('leftAccessoryView', null); }); // Test the right accessory view SC.run(function () { var rightAccessoryView = SC.View.design({ layout: { top: 1, right: 3, width: 17, height: 16 }, init: function () { sc_super(); rightAccessoryViewInitCount++; } }); view.set('rightAccessoryView', rightAccessoryView); }); // Check it equals(rightAccessoryViewInitCount, 1, 'the right accessory view should only be initialized once'); }); // .......................................................... // TEST EVENTS // module('SC.TextFieldView: Events', pane.standardSetup()); test("focus and blurring text field", function () { var view = pane.view('empty'); var input = view.$('input'); // attempt to focus... SC.Event.trigger(input, 'focus'); // verify editing state changed... ok(view.get('isEditing'), 'view.isEditing should be YES'); ok(view.$().hasClass('focus'), 'view layer should have focus class'); // simulate typing a letter SC.Event.trigger(input, 'keydown'); SC.Event.trigger(input, 'keyup'); input.val('f'); SC.Event.trigger(input, 'change'); // wait a little bit to let text field propagate changes stop(); setTimeout(function () { start(); equals(view.get('value'), 'f', 'view should have new value'); ok(view.$().hasClass('not-empty'), 'should have not-empty class'); // attempt to blur... SC.Event.trigger(input, 'blur'); // verify editing state changed... ok(!view.get('isEditing'), 'view.isEditing should be NO'); ok(!view.$().hasClass('focus'), 'view layer should NOT have focus class'); }, 100); }); test("focus and blur an empty text field", function () { var view = pane.view('empty'); var input = view.$('input'); // verify the field is empty and the hint is properly set pane.verifyEmpty(view, 'Full Name'); // focus and blur the text field SC.Event.trigger(input, 'focus'); SC.Event.trigger(input, 'blur'); // field should still be still be empty with hint properly set pane.verifyEmpty(view, 'Full Name'); }); test("losing first responder should blur", function () { var view = pane.view('empty'); var input = view.$('input'); var testResponder = SC.Responder.create(SC.ResponderContext, {}); // preliminary setup view.get('pane').becomeKeyPane(); SC.Event.trigger(input, 'focus'); // verify it did receive focus ok(view.get('focused'), 'view should have focus'); // tell the pane to make our test responder the first responder view.get('pane').makeFirstResponder(testResponder); // verify it no longer has focus ok(!view.get('focused'), 'view should no longer have focus (Warning: this test will fail if the browser window doesn\'t have focus)'); }); test("editing a field should not change the cursor position", function () { var textField = pane.view('empty'); var input = textField.$('input'); input.val('John Doe'); textField.set('selection', SC.TextSelection.create({start: 2, end: 3})); SC.Event.trigger(input, 'change'); ok(input.val() === 'John Doe', 'input value should be \'John Doe\''); var selection = textField.get('selection'); ok(selection.get('start') == 2 && selection.get('end') == 3, 'cursor position should be unchanged'); });
})();