From d42d509eb107d2285a3d7f052238176a3a2861ae Mon Sep 17 00:00:00 2001 From: Hiroyuki Sano Date: Tue, 12 Jul 2016 07:30:42 +0000 Subject: [PATCH 1/5] Add Session#context(obj) method --- lib/web_console.rb | 1 + lib/web_console/context.rb | 46 ++++++++++++++++++++++++++++++++ lib/web_console/middleware.rb | 2 +- lib/web_console/session.rb | 9 +++++-- test/web_console/context_test.rb | 28 +++++++++++++++++++ 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 lib/web_console/context.rb create mode 100644 test/web_console/context_test.rb diff --git a/lib/web_console.rb b/lib/web_console.rb index c1c549df..832bfb74 100644 --- a/lib/web_console.rb +++ b/lib/web_console.rb @@ -15,6 +15,7 @@ module WebConsole autoload :Whitelist autoload :Template autoload :Middleware + autoload :Context autoload_at 'web_console/errors' do autoload :Error diff --git a/lib/web_console/context.rb b/lib/web_console/context.rb new file mode 100644 index 00000000..b2249649 --- /dev/null +++ b/lib/web_console/context.rb @@ -0,0 +1,46 @@ +module WebConsole + # A context lets you get object names related to the current session binding. + class Context + def initialize(binding) + @binding = binding + end + + # Extracts entire objects which can be called by the current session unless the objpath is present. + # Otherwise, it extracts methods and constants of the object specified by the objpath. + def extract(objpath) + if objpath.present? + local(objpath) + else + global + end + end + + private + + GLOBAL_OBJECTS = [ + 'global_variables', + 'local_variables', + 'instance_variables', + 'instance_methods', + 'class_variables', + 'methods', + 'Object.constants', + 'Kernel.methods', + ] + + def global + GLOBAL_OBJECTS.map { |cmd| eval(cmd) }.flatten + end + + def local(objpath) + [ + eval("#{objpath}.methods").map { |m| "#{objpath}.#{m}" }, + eval("#{objpath}.constants").map { |c| "#{objpath}::#{c}" }, + ].flatten + end + + def eval(cmd) + @binding.eval(cmd) rescue [] + end + end +end diff --git a/lib/web_console/middleware.rb b/lib/web_console/middleware.rb index d0c36006..632eec71 100644 --- a/lib/web_console/middleware.rb +++ b/lib/web_console/middleware.rb @@ -103,7 +103,7 @@ def id_for_repl_session_stack_frame_change(request) def update_repl_session(id, request) json_response_with_session(id, request) do |session| - { output: session.eval(request.params[:input]) } + { output: session.eval(request.params[:input]), context: session.context(request.params[:context]) } end end diff --git a/lib/web_console/session.rb b/lib/web_console/session.rb index bf88e8cc..f6accd80 100644 --- a/lib/web_console/session.rb +++ b/lib/web_console/session.rb @@ -43,7 +43,7 @@ def from(storage) def initialize(bindings) @id = SecureRandom.hex(16) @bindings = bindings - @evaluator = Evaluator.new(bindings.first) + @evaluator = Evaluator.new(@current_binding = bindings.first) store_into_memory end @@ -59,7 +59,12 @@ def eval(input) # # Returns nothing. def switch_binding_to(index) - @evaluator = Evaluator.new(@bindings[index.to_i]) + @evaluator = Evaluator.new(@current_binding = @bindings[index.to_i]) + end + + # Returns context of the current binding + def context(objpath) + Context.new(@current_binding).extract(objpath) end private diff --git a/test/web_console/context_test.rb b/test/web_console/context_test.rb new file mode 100644 index 00000000..555366ed --- /dev/null +++ b/test/web_console/context_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +module WebConsole + class ContextTest < ActiveSupport::TestCase + test '#extract(empty) includes local variables' do + local_var = 'local' + assert Context.new(binding).extract('').include?(:local_var) + end + + test '#extract(empty) includes instance variables' do + @instance_var = 'instance' + assert Context.new(binding).extract('').include?(:@instance_var) + end + + test '#extract(empty) includes global variables' do + $global_var = 'global' + assert Context.new(binding).extract('').include?(:$global_var) + end + + test '#extract(obj) returns methods' do + assert Context.new(binding).extract('Rails').include?('Rails.root') + end + + test '#extract(obj) returns constants' do + assert Context.new(binding).extract('WebConsole').include?('WebConsole::Middleware') + end + end +end From 73561153eff16b8533fb0dc5657d296ba7a12060 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sano Date: Sat, 16 Jul 2016 08:46:04 +0000 Subject: [PATCH 2/5] Add Autocomplete class to console.js --- lib/web_console/templates/console.js.erb | 128 +++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/lib/web_console/templates/console.js.erb b/lib/web_console/templates/console.js.erb index c513e805..9cb3bcb3 100644 --- a/lib/web_console/templates/console.js.erb +++ b/lib/web_console/templates/console.js.erb @@ -44,6 +44,134 @@ function CommandStorage() { } } +function Autocomplete(words, prefix) { + this.words = words.concat(); // to clone object + this.words.sort(); // by alphabetically order + this.current = -1; + this.left = 0; // [left, right) + this.right = words.length; + this.confirmed = false; + + function createKeyword(label) { + var el = document.createElement('span'); + el.innerText = label; + addClass(el, 'keyword'); + return el; + } + + this.view = document.createElement('pre'); + addClass(this.view, 'console-message'); + addClass(this.view, 'auto-complete'); + for (var key in this.words) { + this.view.appendChild(createKeyword(this.words[key])); + } + + this.refine(prefix || ''); +} + +Autocomplete.prototype.onFinished = function(callback) { + this.onFinishedCallback = callback; + if (this.confirmed) callback(this.confirmed); +}; + +Autocomplete.prototype.onKeyDown = function(ev) { + var self = this; + + function move(nextCurrent) { + if (0 <= self.current && self.current < self.words.length) removeClass(self.view.children[self.current], 'selected'); + addClass(self.view.children[nextCurrent], 'selected'); + self.current = nextCurrent; + } + + switch (ev.keyCode) { + case 9: // Tab + if (ev.shiftKey) { // move back + move(self.current - 1 < self.left ? self.right - 1 : self.current - 1); + } else { // move next + move(self.current + 1 >= self.right ? self.left : self.current + 1); + } + return true; + case 13: // Enter + this.finish(); + return true; + case 27: // Esc + this.cancel(); + return true; + } + + return false; +}; + +Autocomplete.prototype.refine = function(prefix) { + var inc = !this.prev || (prefix.length >= this.prev.length); + this.prev = prefix; + var self = this; + + function toggle(el) { + if (hasClass(el, 'selected')) removeClass(el, 'selected'); + return inc ? addClass(el, 'hidden') : removeClass(el, 'hidden'); + } + + function startsWith(str, prefix) { + return !prefix || str.substr(0, prefix.length) === prefix; + } + + function moveRight(l, r) { + while (l < r && inc !== startsWith(self.words[l], prefix)) { + toggle(self.view.children[l]); + ++ l; + } + return l; + } + + function moveLeft(l, r) { + while (l < r - 1 && inc !== startsWith(self.words[r - 1], prefix)) { + toggle(self.view.children[r - 1]); + -- r; + } + return r; + } + + if (inc) { + self.left = moveRight(self.left, self.right); + self.right = moveLeft(self.left, self.right); + } else { + self.left = moveLeft(-1, self.left); + self.right = moveRight(self.right, self.words.length); + } + + if (self.current < self.left) { + self.current = self.left - 1; + } else if (self.current > self.right) { + self.current = self.right; + } + + if (self.left + 1 >= self.right) { + self.current = self.left; + self.finish(); + } +}; + +Autocomplete.prototype.finish = function() { + if (this.left <= this.current && this.current < this.right) { + if (this.onFinishedCallback) this.onFinishedCallback(this.words[this.current]); + this.removeView(); + this.confirmed = this.words[this.current]; + } else { + this.cancel(); + } +}; + +Autocomplete.prototype.cancel = function() { + if (this.onFinishedCallback) this.onFinishedCallback(); + this.removeView(); +}; + +Autocomplete.prototype.removeView = function() { + if (this.view.parentNode) this.view.parentNode.removeChild(this.view); + removeAllChildren(this.view); +} + // HTML strings for dynamic elements. var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup.html' %>; var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup.html' %>; From c5a90d508b32dc2224a6467db265f51d53340697 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sano Date: Tue, 12 Jul 2016 07:37:54 +0000 Subject: [PATCH 3/5] Let's use auto completion on console by TAB key --- lib/web_console/templates/console.js.erb | 73 ++++++++++++++++++++++++ lib/web_console/templates/style.css.erb | 4 ++ 2 files changed, 77 insertions(+) diff --git a/lib/web_console/templates/console.js.erb b/lib/web_console/templates/console.js.erb index 9cb3bcb3..37793c2c 100644 --- a/lib/web_console/templates/console.js.erb +++ b/lib/web_console/templates/console.js.erb @@ -190,6 +190,7 @@ function REPLConsole(config) { this.prompt = getConfig('promptLabel', ' >>'); this.mountPoint = getConfig('mountPoint'); this.sessionId = getConfig('sessionId'); + this.autocomplete = false; } REPLConsole.prototype.getSessionUrl = function(path) { @@ -226,6 +227,19 @@ REPLConsole.prototype.commandHandle = function(line, callback) { } } + function getContext() { + var s = self.getCurrentWord(); + var methodOp = s.lastIndexOf('.'); + var moduleOp = s.lastIndexOf('::'); + var x = methodOp > moduleOp ? methodOp : moduleOp; + if (x !== -1) return s.substr(0, x); + } + + if (this.autocomplete) { + var c = getContext(); + params += c ? '&context=' + c : ''; + } + putRequest(self.getSessionUrl(), params, function(xhr) { var response = parseJSON(xhr.responseText); var result = isSuccess(xhr.status); @@ -396,6 +410,7 @@ REPLConsole.prototype.removeCaretFromPrompt = function() { }; REPLConsole.prototype.setInput = function(input, caretPos) { + if (input == null) return; // keep value if input is undefined this._caretPos = caretPos === undefined ? input.length : caretPos; this._input = input; this.renderInput(); @@ -444,6 +459,7 @@ REPLConsole.prototype.renderInput = function() { }; REPLConsole.prototype.writeOutput = function(output) { + if (this.autocomplete) return; var consoleMessage = document.createElement('pre'); consoleMessage.className = "console-message"; consoleMessage.innerHTML = escapeHTML(output); @@ -453,6 +469,7 @@ REPLConsole.prototype.writeOutput = function(output) { }; REPLConsole.prototype.writeError = function(output) { + if (this.autocomplete) return; var consoleMessage = this.writeOutput(output); addClass(consoleMessage, "error-message"); return consoleMessage; @@ -468,6 +485,24 @@ REPLConsole.prototype.onEnterKey = function() { this.commandHandle(input); }; +REPLConsole.prototype.onTabKey = function() { + var self = this; + + if (self.autocomplete) return; + self.autocomplete = new Autocomplete([]); + + self.commandHandle("nil", function(ok, obj) { + if (!ok) return self.autocomplete = false; + self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord()); + self.inner.appendChild(self.autocomplete.view); + self.autocomplete.onFinished(function(word) { + self.swapCurrentWord(word); + self.autocomplete = false; + }); + self.scrollToBottom(); + }); +}; + REPLConsole.prototype.onNavigateHistory = function(offset) { var command = this.commandStorage.navigate(offset) || ""; this.setInput(command); @@ -477,7 +512,18 @@ REPLConsole.prototype.onNavigateHistory = function(offset) { * Handle control keys like up, down, left, right. */ REPLConsole.prototype.onKeyDown = function(ev) { + if (this.autocomplete && this.autocomplete.onKeyDown(ev)) { + ev.preventDefault(); + ev.stopPropagation(); + return; + } + switch (ev.keyCode) { + case 9: + // Tab + this.onTabKey(); + ev.preventDefault(); + break; case 13: // Enter key this.onEnterKey(); @@ -515,6 +561,7 @@ REPLConsole.prototype.onKeyDown = function(ev) { case 8: // Delete this.deleteAtCurrent(); + if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord()); ev.preventDefault(); break; default: @@ -547,6 +594,7 @@ REPLConsole.prototype.onKeyPress = function(ev) { if (ev.ctrlKey || ev.metaKey) { return; } var keyCode = ev.keyCode || ev.which; this.insertAtCurrent(String.fromCharCode(keyCode)); + if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord()); ev.stopPropagation(); ev.preventDefault(); }; @@ -572,6 +620,31 @@ REPLConsole.prototype.insertAtCurrent = function(char) { this.setInput(before + char + after, this._caretPos + 1); }; +REPLConsole.prototype.swapCurrentWord = function(next) { + function right(s, pos) { + var x = s.indexOf(' ', pos); + return x === -1 ? s.length : x; + } + + function swap(s, pos) { + return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos)) + } + + if (!next) return; + var swapped = swap(this._input, this._caretPos); + this.setInput(swapped, this._caretPos + swapped.length - this._input.length); +}; + +REPLConsole.prototype.getCurrentWord = function() { + return (function(s, pos) { + var left = s.lastIndexOf(' ', pos); + if (left === -1) left = 0; + var right = s.indexOf(' ', pos) + if (right === -1) right = s.length - 1; + return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,''); + })(this._input, this._caretPos); +}; + REPLConsole.prototype.scrollToBottom = function() { this.outer.scrollTop = this.outer.scrollHeight; }; diff --git a/lib/web_console/templates/style.css.erb b/lib/web_console/templates/style.css.erb index 156225bf..e181a669 100644 --- a/lib/web_console/templates/style.css.erb +++ b/lib/web_console/templates/style.css.erb @@ -11,6 +11,10 @@ .console .console-prompt-box { color: #FFF; } .console .console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; } .console .console-message.error-message { color: #fc9; } +.console .console-message.auto-complete { word-break: break-all; } +.console .console-message.auto-complete .keyword { margin-right: 11px; } +.console .console-message.auto-complete .keyword.selected { background: #FFF; color: #000; } +.console .console-message.auto-complete .hidden { display: none; } .console .console-focus .console-cursor { background: #FEFEFE; color: #333; font-weight: bold; } .console .resizer { background: #333; width: 100%; height: 4px; cursor: ns-resize; } .console .console-actions { padding-right: 3px; } From 7cc76ad645017dbd84b70647a3250b2e31bca08b Mon Sep 17 00:00:00 2001 From: Hiroyuki Sano Date: Sat, 16 Jul 2016 08:59:48 +0000 Subject: [PATCH 4/5] Test auto completion feature --- test/templates/test/auto_complete_test.js | 55 +++++++++++++++++++++++ test/templates/test/repl_console_test.js | 42 +++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 test/templates/test/auto_complete_test.js create mode 100644 test/templates/test/repl_console_test.js diff --git a/test/templates/test/auto_complete_test.js b/test/templates/test/auto_complete_test.js new file mode 100644 index 00000000..7d666ccc --- /dev/null +++ b/test/templates/test/auto_complete_test.js @@ -0,0 +1,55 @@ +suite('Autocomplete', function() { + setup(function() { + this.ac = new Autocomplete(['other', 'other2', 'something', 'somewhat', 'somewhere', 'test']); + this.refine = function(prefix) { this.ac.refine(prefix); }; + }); + + test('empty string', function() { + this.refine(''); + + assert(!this.ac.confirmed); + assertRange(this, 0, this.ac.words.length, 0); + }); + + test('some', function() { + this.refine('some'); + + assert(!this.ac.confirmed); + assertRange(this, 2, this.ac.words.length - 1, 3); + }); + + test('confirmable', function() { + this.refine('somet'); + + assert.equal(this.ac.confirmed, 'something'); + assertRange(this, 2, 3); + }); + + test('decrement', function() { + this.refine('o'); + this.refine(''); + + assertRange(this, 0, this.ac.words.length, 0); + }); + + test('other => empty => some', function() { + this.refine('other'); + this.refine(''); + this.refine('some'); + + assertRange(this, 2, this.ac.words.length - 1, 3); + }); + + test('some => empty', function() { + this.refine('some'); + this.refine(''); + + assertRange(this, 0, this.ac.words.length); + }); + + function assertRange(self, left, right, hiddenCnt) { + assert.equal(left, self.ac.left, 'left'); + assert.equal(right, self.ac.right, 'right'); + if (hiddenCnt) assert.equal(hiddenCnt, self.ac.view.getElementsByClassName('hidden').length, 'hiddenCnt'); + } +}); diff --git a/test/templates/test/repl_console_test.js b/test/templates/test/repl_console_test.js new file mode 100644 index 00000000..015dcbf9 --- /dev/null +++ b/test/templates/test/repl_console_test.js @@ -0,0 +1,42 @@ +suite('REPLCosnole', function() { + suiteSetup(function() { + this.stage = document.createElement('div'); + document.body.appendChild(this.stage); + }); + + setup(function() { + var elm = document.createElement('div'); + elm.innerHTML = '
'; + this.stage.appendChild(elm); + var consoleOptions = { mountPoint: '/mock', sessionId: 'result' }; + this.console = REPLConsole.installInto('console', consoleOptions); + }); + + teardown(function() { removeAllChildren(this.stage); }); + + test('swap last word', function() { + this.console.setInput('hello world'); + this.console.swapCurrentWord('word'); + + assert.equal(this.console._input, 'hello word'); + }); + + test('swap first word', function() { + this.console.setInput('hello world'); + this.console._caretPos = 0; + this.console.swapCurrentWord('my'); + + assert.equal(this.console._input, 'my world'); + }); + + test('current word', function() { + this.console.setInput('hello world'); + assert.equal(this.console.getCurrentWord(), 'world'); + this.console._caretPos = 0; + assert.equal(this.console.getCurrentWord(), 'hello'); + this.console._caretPos = 5; + assert.equal(this.console.getCurrentWord(), ''); + this.console._caretPos = 6; + assert.equal(this.console.getCurrentWord(), 'world'); + }); +}); From 2be719dca25beefef01b43dc6ae170936f7b6e8d Mon Sep 17 00:00:00 2001 From: Hiroyuki Sano Date: Tue, 12 Jul 2016 07:29:27 +0000 Subject: [PATCH 5/5] WIP: Enhance template testing --- .travis.yml | 2 +- lib/web_console/tasks/test_templates.rake | 24 ++++++++----- lib/web_console/testing/fake_middleware.rb | 2 +- test/templates/config.ru | 7 ++++ test/templates/html/spec_runner.html.erb | 4 +-- test/templates/html/test_runner.html.erb | 36 +++++++++++++++++++ test/templates/spec/auto_complete_spec.js | 37 +++++++++++++++++++ test/templates/spec/repl_console_spec.js | 42 ++++++++++++++++++++++ test/web_console/middleware_test.rb | 4 +-- 9 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 test/templates/html/test_runner.html.erb create mode 100644 test/templates/spec/auto_complete_spec.js diff --git a/.travis.yml b/.travis.yml index edfff988..6eff9fde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,4 +23,4 @@ matrix: - env: TEST_SUITE=test:templates include: - env: TEST_SUITE=test:templates - rvm: 2.2 + rvm: 2.2.5 diff --git a/lib/web_console/tasks/test_templates.rake b/lib/web_console/tasks/test_templates.rake index a5b6d5c4..f00e5636 100644 --- a/lib/web_console/tasks/test_templates.rake +++ b/lib/web_console/tasks/test_templates.rake @@ -3,13 +3,15 @@ namespace :test do task templates: "templates:all" namespace :templates do - task all: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ] + task all: [ :daemonize, :npm, :rackup, :wait, :spec, :test, :kill, :exit ] task serve: [ :npm, :rackup ] - work_dir = Pathname(EXPANDED_CWD).join("test/templates") + workdir = Pathname(EXPANDED_CWD).join("test/templates") pid_file = Pathname(Dir.tmpdir).join("web_console.#{SecureRandom.uuid}.pid") - runner_uri = URI.parse("http://localhost:29292/html/spec_runner.html") - rackup_opts = "-p #{runner_uri.port}" + html_uri = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/") + spec_runner = 'spec_runner.html' + test_runner = 'test_runner.html' + rackup_opts = "--host #{html_uri.host} --port #{html_uri.port}" test_result = nil def need_to_wait?(uri) @@ -23,20 +25,24 @@ namespace :test do end task :npm do - Dir.chdir(work_dir) { system "npm install --silent" } + Dir.chdir(workdir) { system "npm install --silent" } end task :rackup do - Dir.chdir(work_dir) { system "bundle exec rackup #{rackup_opts}" } + Dir.chdir(workdir) { system "bundle exec rackup #{rackup_opts}" } end task :wait do cnt = 0 - need_to_wait?(runner_uri) { sleep 1; cnt += 1; cnt < 5 } + need_to_wait?(URI.join(html_uri, spec_runner)) { sleep 1; cnt += 1; cnt < 5 } end - task :mocha do - Dir.chdir(work_dir) { test_result = system("$(npm bin)/mocha-phantomjs #{runner_uri}") } + task :spec do + Dir.chdir(workdir) { test_result = system("./node_modules/.bin/mocha-phantomjs #{URI.join(html_uri, spec_runner)}") } + end + + task :test do + Dir.chdir(workdir) { test_result = system("./node_modules/.bin/mocha-phantomjs #{URI.join(html_uri, test_runner)}") } end task :kill do diff --git a/lib/web_console/testing/fake_middleware.rb b/lib/web_console/testing/fake_middleware.rb index 95a75cc1..b58731a6 100644 --- a/lib/web_console/testing/fake_middleware.rb +++ b/lib/web_console/testing/fake_middleware.rb @@ -21,7 +21,7 @@ def call(env) end def view - @view ||= View.new(@view_path) + @view = View.new(@view_path) end private diff --git a/test/templates/config.ru b/test/templates/config.ru index 474169c0..e8bd6cb8 100644 --- a/test/templates/config.ru +++ b/test/templates/config.ru @@ -27,6 +27,13 @@ map "/spec" do ) end +map "/test" do + run WebConsole::Testing::FakeMiddleware.new( + req_path_regex: %r{^/test/(.*)}, + view_path: TEST_ROOT.join("test"), + ) +end + map "/templates" do run WebConsole::Testing::FakeMiddleware.new( req_path_regex: %r{^/templates/(.*)}, diff --git a/test/templates/html/spec_runner.html.erb b/test/templates/html/spec_runner.html.erb index b577d20f..436291be 100644 --- a/test/templates/html/spec_runner.html.erb +++ b/test/templates/html/spec_runner.html.erb @@ -20,8 +20,8 @@ - <% Pathname.glob(TEST_ROOT.join "spec/**/*_spec.js") do |spec| %> - + <% Pathname.glob(TEST_ROOT.join "spec/**/*_spec.js") do |t| %> + <% end %> diff --git a/test/templates/html/test_runner.html.erb b/test/templates/html/test_runner.html.erb new file mode 100644 index 00000000..c3d4beb7 --- /dev/null +++ b/test/templates/html/test_runner.html.erb @@ -0,0 +1,36 @@ + + + + + Templates Test + + + + +
+ + + + + + + + + + <% Pathname.glob(TEST_ROOT.join "test/**/*_test.js") do |t| %> + + <% end %> + + + + + diff --git a/test/templates/spec/auto_complete_spec.js b/test/templates/spec/auto_complete_spec.js new file mode 100644 index 00000000..fccf259f --- /dev/null +++ b/test/templates/spec/auto_complete_spec.js @@ -0,0 +1,37 @@ +describe("Auto Complete", function() { + beforeEach(function() { + var self = this.autoComplete = new Autocomplete(["something", "somewhat", "somewhere"], 'some'); + this.moveNext = function(times) { + for (var i = 0; i < times; ++i) self.next(); + }; + this.assertSelect = function(pos) { + assert.equal(self.current, pos); + }; + }); + describe("move functions", function() { + context("set up with three elements", function() { + it("should have three elements", function() { + assert.ok(this.autoComplete.view.children.length === 3) + }); + it("should have no selected element", function() { + assert.ok(this.autoComplete.view.getElementsByClassName('selected').length === 0); + }); + context("move next two times", function() { + beforeEach(function() { this.moveNext(2) }); + it("should point the 1-th element", function() { this.assertSelect(1); }); + context("back once", function() { + beforeEach(function() { this.autoComplete.back(); }); + it("should point the 0-th element", function() { this.assertSelect(0); }); + context("back once again", function() { + beforeEach(function() { this.autoComplete.back(); }); + it("should point the last element", function() { this.assertSelect(2); }); + }); + }); + context("move next two times again", function() { + beforeEach(function() { this.moveNext(2) }); + it("should back to the first of list", function() { this.assertSelect(0); }); + }); + }); + }); + }); +}); diff --git a/test/templates/spec/repl_console_spec.js b/test/templates/spec/repl_console_spec.js index 61587930..860b3e6f 100644 --- a/test/templates/spec/repl_console_spec.js +++ b/test/templates/spec/repl_console_spec.js @@ -1,6 +1,48 @@ describe("REPLConsole", function() { SpecHelper.prepareStageElement(); + describe("#swapWord", function() { + beforeEach(function() { + var elm = document.createElement('div'); + elm.innerHTML = '
'; + this.stageElement.appendChild(elm); + var consoleOptions = { mountPoint: '/mock', sessionId: 'result' }; + this.console = REPLConsole.installInto('console', consoleOptions); + }); + context("caret points to last item", function() { + beforeEach(function() { + this.console.setInput('hello world'); + this.console.swapCurrentWord('swapped'); + }); + it('should be last word', function() { assert.equal(this.console._input, 'hello swapped'); }); + }); + context("points to first item", function() { + beforeEach(function() { + this.console.setInput('hello world', 3); + this.console.swapCurrentWord('swapped'); + }); + it('should be first word', function() { assert.equal(this.console._input, 'swapped world'); }); + }); + }); + + describe("#getCurrentWord", function() { + beforeEach(function() { + var elm = document.createElement('div'); + elm.innerHTML = '
'; + this.stageElement.appendChild(elm); + var consoleOptions = { mountPoint: '/mock', sessionId: 'result' }; + this.console = REPLConsole.installInto('console', consoleOptions); + }); + context("caret points to last item", function() { + beforeEach(function() { this.console.setInput('hello world'); }); + it('should be last word', function() { assert.equal(this.console.getCurrentWord(), 'world'); }); + }); + context("points to first item", function() { + beforeEach(function() { this.console.setInput('hello world', 0); }); + it('should be first word', function() { assert.equal(this.console.getCurrentWord(), 'hello'); }); + }); + }); + describe("#commandHandle", function() { function runCommandHandle(self, consoleOptions, callback) { self.console = REPLConsole.installInto('console', consoleOptions); diff --git a/test/web_console/middleware_test.rb b/test/web_console/middleware_test.rb index 0f7cd779..65909481 100644 --- a/test/web_console/middleware_test.rb +++ b/test/web_console/middleware_test.rb @@ -131,7 +131,7 @@ def body get '/', params: nil put "/repl_sessions/#{session.id}", xhr: true, params: { input: '__LINE__' } - assert_equal({ output: "=> #{line}\n" }.to_json, response.body) + assert_equal("=> #{line}\n", JSON.parse(response.body)["output"]) end test 'can switch bindings on error pages' do @@ -151,7 +151,7 @@ def body session, line = Session.new([binding]), __LINE__ put "/customized/path/repl_sessions/#{session.id}", params: { input: '__LINE__' }, xhr: true - assert_equal({ output: "=> #{line}\n" }.to_json, response.body) + assert_equal("=> #{line}\n", JSON.parse(response.body)["output"]) end test 'unavailable sessions respond to the user with a message' do