diff --git a/README.md b/README.md index eb2f651..23c9e79 100644 --- a/README.md +++ b/README.md @@ -672,6 +672,7 @@ Options: -b, [--bin=BIN] # The location of the PhantomJS binary -d, [--spec-dir=SPEC_DIR] # The directory with the Jasmine specs # Default: spec/javascripts + -l, [--line-number=N] # The line which identifies the spec to be run -u, [--url=URL] # The url of the Jasmine test runner_options # Default: nil -t, [--timeout=N] # The maximum time in seconds to wait for the spec diff --git a/lib/guard/jasmine/cli.rb b/lib/guard/jasmine/cli.rb index 76caa26..1737ffc 100644 --- a/lib/guard/jasmine/cli.rb +++ b/lib/guard/jasmine/cli.rb @@ -65,6 +65,11 @@ class CLI < Thor aliases: '-d', desc: 'The directory with the Jasmine specs' + method_option :line_number, + type: :numeric, + aliases: '-l', + desc: 'The line which identifies the spec to be run' + method_option :url, type: :string, aliases: '-u', @@ -166,6 +171,7 @@ def spec(*paths) runner_options = {} runner_options[:port] = options.port || CLI.find_free_server_port runner_options[:spec_dir] = options.spec_dir || (File.exists?(File.join('spec', 'javascripts')) ? File.join('spec', 'javascripts') : 'spec') + runner_options[:line_number] = options.line_number runner_options[:server] = options.server.to_sym == :auto ? ::Guard::Jasmine::Server.detect_server(runner_options[:spec_dir]) : options.server.to_sym runner_options[:server_mount] = options.mount || (defined?(JasmineRails) ? '/specs' : '/jasmine') runner_options[:jasmine_url] = options.url || "http://localhost:#{ runner_options[:port] }#{ options.server.to_sym == :jasmine_gem ? '/' : runner_options[:server_mount] }" diff --git a/lib/guard/jasmine/runner.rb b/lib/guard/jasmine/runner.rb index ea274ab..e49c9b9 100644 --- a/lib/guard/jasmine/runner.rb +++ b/lib/guard/jasmine/runner.rb @@ -41,7 +41,7 @@ def run(paths, options = { }) notify_start_message(paths, options) results = paths.inject([]) do |results, file| - results << evaluate_response(run_jasmine_spec(file, options), file, options) if File.exist?(file) + results << evaluate_response(run_jasmine_spec(file, options), file, options) if File.exist?(file_and_line_number_parts(file)[0]) results end.compact @@ -141,9 +141,7 @@ def phantomjs_script end # The suite name must be extracted from the spec that - # will be run. This is done by parsing from the head of - # the spec file until the first `describe` function is - # found. + # will be run. # # @param [String] file the spec file # @param [Hash] options the options for the execution @@ -153,16 +151,97 @@ def phantomjs_script def query_string_for_suite(file, options) return '' if file == options[:spec_dir] - query_string = '' + query_string = query_string_for_suite_from_line_number(file, options) + + unless query_string + query_string = query_string_for_suite_from_first_describe(file, options) + end + + query_string = query_string ? "?spec=#{ query_string }" : '' + + URI.encode(query_string) + end + + # When providing a line number by either the option or by + # a number directly after the file name the suite is extracted + # fromt the corresponding line number in the file. + # + # @param [String] file the spec file + # @param [Hash] options the options for the execution + # @option options [Fixnum] :line_number the line number to run + # @return [String] the suite name + # + def query_string_for_suite_from_line_number(file, options) + file_name, line_number = file_and_line_number_parts(file) + line_number ||= options[:line_number] + + if line_number + lines = it_and_describe_lines(file_name, 0, line_number) + last = lines.pop + + last_indentation = last[/^\s*/].length + # keep only lines with lower indentation + lines.delete_if { |x| x[/^\s*/].length >= last_indentation } + # remove all 'it' + lines.delete_if { |x| x =~ /^\s*it/ } + lines << last + lines.map { |x| spec_title(x) }.join(' ') + end + end + + # The suite name must be extracted from the spec that + # will be run. This is done by parsing from the head of + # the spec file until the first `describe` function is + # found. + # + # @param [String] file the spec file + # @param [Hash] options the options for the execution + # @return [String] the suite name + # + def query_string_for_suite_from_first_describe(file, options) File.foreach(file) do |line| if line =~ /describe\s*[("']+(.*?)["')]+/ - query_string = "?spec=#{ $1 }" - break + return $1 end end + end - URI.encode(query_string) + # Splits the file name into the physical file name + # and the line number if present. E.g.: + # 'some_spec.js.coffee:10' -> ['some_spec.js.coffee', 10]. + # + # If the line number is missing the second part of the + # returned array is `nil`. + # + # @param [String] file the spec file + # @return [Array] `[file_name, line_number]` + # + def file_and_line_number_parts(file) + match = file.match(/^(.+?)(?::(\d+))?$/) + [match[1], match[2].nil? ? nil : match[2].to_i] + end + + # Returns all lines of the file that are either a + # 'describe' or a 'it' declaration. + # + # @param [String] file the spec file + # @param [Numeric] from the first line in the range + # @param [Numeric] to the last line in the range + # @Return [Array] the line contents + # + def it_and_describe_lines(file, from, to) + File.readlines(file)[from, to]. + select { |x| x =~ /^\s*(it|describe)/ } + end + + # Extracts the title of a 'description' or a 'it' declaration. + # + # @param [String] the line content + # @return [String] the extracted title + # + def spec_title(line) + line[/['"](.+?)['"]/, 1] end # Evaluates the JSON response that the PhantomJS script diff --git a/spec/guard/jasmine/cli_spec.rb b/spec/guard/jasmine/cli_spec.rb index 39542a9..0a0e91e 100644 --- a/spec/guard/jasmine/cli_spec.rb +++ b/spec/guard/jasmine/cli_spec.rb @@ -45,6 +45,11 @@ cli.start(['spec', '--spec-dir', 'specs']) end + it 'sets the line number' do + runner.should_receive(:run).with(anything(), hash_including(line_number: 1)).and_return [true, []] + cli.start(['spec', '--line-number', 1]) + end + it 'detects the server type' do server.should_receive(:detect_server).with('specs') cli.start(['spec', '--spec-dir', 'specs']) @@ -274,6 +279,11 @@ cli.start(['spec']) end + it 'sets the line number' do + runner.should_receive(:run).with(anything(), hash_including(line_number: nil)).and_return [true, []] + cli.start(['spec']) + end + it 'disables the focus mode' do runner.should_receive(:run).with(anything(), hash_including(focus: false)).and_return [true, []] cli.start(['spec', '-f', 'false']) diff --git a/spec/guard/jasmine/runner_spec.rb b/spec/guard/jasmine/runner_spec.rb index 772df8c..176fa66 100644 --- a/spec/guard/jasmine/runner_spec.rb +++ b/spec/guard/jasmine/runner_spec.rb @@ -211,6 +211,47 @@ end end + context 'when passed a line number' do + before do + File.stub(:readlines).and_return([ + 'describe "TestContext", ->', # 1 + ' describe "Inner TestContext", ->', # 2 + ' describe "Unrelated TestContext", ->', # 3 + ' it "does something", ->', # 4 + ' # some code', # 5 + ' # some assertion', # 6 + ' it "does something else", ->', # 7 + ' # some assertion', # 8 + ' it "does something a lot else", ->', # 9 + ' # some assertion' # 10 + ]) + end + + context 'with the spec file name' do + it 'executes the example for line number on example' do + IO.should_receive(:popen).with("#{ phantomjs_command } \"http://localhost:8888/jasmine?spec=TestContext%20Inner%20TestContext%20does%20something%20else\" 60000 failure true failure failure false true ''", "r:UTF-8") + runner.run(['spec/javascripts/a.js.coffee:7'], defaults) + end + + it 'executes the example for line number within example' do + IO.should_receive(:popen).with("#{ phantomjs_command } \"http://localhost:8888/jasmine?spec=TestContext%20Inner%20TestContext%20does%20something%20else\" 60000 failure true failure failure false true ''", "r:UTF-8") + runner.run(['spec/javascripts/a.js.coffee:8'], defaults) + end + + it 'executes all examples within describe' do + IO.should_receive(:popen).with("#{ phantomjs_command } \"http://localhost:8888/jasmine?spec=TestContext\" 60000 failure true failure failure false true ''", "r:UTF-8") + runner.run(['spec/javascripts/a.js.coffee:1'], defaults) + end + end + + context 'with the cli argument' do + it 'executes the example for line number on example' do + IO.should_receive(:popen).with("#{ phantomjs_command } \"http://localhost:8888/jasmine?spec=TestContext%20Inner%20TestContext%20does%20something%20else\" 60000 failure true failure failure false true ''", "r:UTF-8") + runner.run(['spec/javascripts/a.js.coffee'], defaults.merge(line_number: 7)) + end + end + end + context 'when passed the spec directory' do it 'requests all jasmine specs from the server' do IO.should_receive(:popen).with("#{ phantomjs_command } \"http://localhost:8888/jasmine\" 60000 failure true failure failure false true ''", "r:UTF-8")