kookaburra 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm use ree-1.8.7@kookaburra --create --install
2
+ bundle check
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'activesupport', '>= 3.0'
4
+ gem 'rack'
5
+
6
+
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem 'minitest', '>= 0'
12
+ gem 'yard', '~> 0.6.0'
13
+ gem 'bundler', '~> 1.0.0'
14
+ gem 'jeweler', '~> 1.6.4'
15
+ gem 'rcov', '>= 0'
16
+ gem 'reek', '~> 1.2.8'
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,39 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.1.3)
5
+ multi_json (~> 1.0)
6
+ git (1.2.5)
7
+ jeweler (1.6.4)
8
+ bundler (~> 1.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ minitest (2.9.0)
12
+ multi_json (1.0.4)
13
+ rack (1.3.5)
14
+ rake (0.9.2.2)
15
+ rcov (0.9.11)
16
+ reek (1.2.8)
17
+ ruby2ruby (~> 1.2)
18
+ ruby_parser (~> 2.0)
19
+ sexp_processor (~> 3.0)
20
+ ruby2ruby (1.3.1)
21
+ ruby_parser (~> 2.0)
22
+ sexp_processor (~> 3.0)
23
+ ruby_parser (2.3.1)
24
+ sexp_processor (~> 3.0)
25
+ sexp_processor (3.0.9)
26
+ yard (0.6.8)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ activesupport (>= 3.0)
33
+ bundler (~> 1.0.0)
34
+ jeweler (~> 1.6.4)
35
+ minitest
36
+ rack
37
+ rcov
38
+ reek (~> 1.2.8)
39
+ yard (~> 0.6.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Renewable Funding, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = kookaburra
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to kookaburra
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Renewable Funding, LLC. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "kookaburra"
18
+ gem.homepage = "http://github.com/projectdx/kookaburra"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{WindowDriver testing pattern for Ruby apps}
21
+ gem.description = %Q{Cucumber + Capybara = Kookaburra? It made sense at the time.}
22
+ gem.email = "devteam@renewfund.com"
23
+ gem.authors = ["Renewable Funding, LLC"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/*_test.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ require 'reek/rake/task'
44
+ Reek::Rake::Task.new do |t|
45
+ t.fail_on_error = true
46
+ t.verbose = false
47
+ t.source_files = 'lib/**/*.rb'
48
+ end
49
+
50
+ task :default => :test
51
+
52
+ require 'yard'
53
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,84 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "kookaburra"
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Renewable Funding, LLC"]
12
+ s.date = "2012-01-15"
13
+ s.description = "Cucumber + Capybara = Kookaburra? It made sense at the time."
14
+ s.email = "devteam@renewfund.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rvmrc",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "kookaburra.gemspec",
29
+ "lib/kookaburra.rb",
30
+ "lib/kookaburra/api_driver.rb",
31
+ "lib/kookaburra/given_driver.rb",
32
+ "lib/kookaburra/test_data.rb",
33
+ "lib/kookaburra/test_data/factory.rb",
34
+ "lib/kookaburra/ui_driver.rb",
35
+ "lib/kookaburra/ui_driver/mixins/has_browser.rb",
36
+ "lib/kookaburra/ui_driver/mixins/has_fields.rb",
37
+ "lib/kookaburra/ui_driver/mixins/has_strategies.rb",
38
+ "lib/kookaburra/ui_driver/mixins/has_subcomponents.rb",
39
+ "lib/kookaburra/ui_driver/mixins/has_ui_component.rb",
40
+ "lib/kookaburra/ui_driver/ui_component.rb",
41
+ "lib/kookaburra/world_setup.rb",
42
+ "lib/requires.rb",
43
+ "test/helper.rb"
44
+ ]
45
+ s.homepage = "http://github.com/projectdx/kookaburra"
46
+ s.licenses = ["MIT"]
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = "1.8.15"
49
+ s.summary = "WindowDriver testing pattern for Ruby apps"
50
+
51
+ if s.respond_to? :specification_version then
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0"])
56
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
57
+ s.add_development_dependency(%q<minitest>, [">= 0"])
58
+ s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
59
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
60
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
61
+ s.add_development_dependency(%q<rcov>, [">= 0"])
62
+ s.add_development_dependency(%q<reek>, ["~> 1.2.8"])
63
+ else
64
+ s.add_dependency(%q<activesupport>, [">= 3.0"])
65
+ s.add_dependency(%q<rack>, [">= 0"])
66
+ s.add_dependency(%q<minitest>, [">= 0"])
67
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
68
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
69
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
70
+ s.add_dependency(%q<rcov>, [">= 0"])
71
+ s.add_dependency(%q<reek>, ["~> 1.2.8"])
72
+ end
73
+ else
74
+ s.add_dependency(%q<activesupport>, [">= 3.0"])
75
+ s.add_dependency(%q<rack>, [">= 0"])
76
+ s.add_dependency(%q<minitest>, [">= 0"])
77
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
78
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
79
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
80
+ s.add_dependency(%q<rcov>, [">= 0"])
81
+ s.add_dependency(%q<reek>, ["~> 1.2.8"])
82
+ end
83
+ end
84
+
@@ -0,0 +1,43 @@
1
+ module Kookaburra
2
+ # Pattern:
3
+ # - Get some data from test_data.factory
4
+ # - Post it to the API
5
+ # - Remember the response in test_data
6
+ class APIDriver
7
+ include Rack::Test::Methods
8
+ attr_reader :app, :test_data
9
+ protected :app, :test_data
10
+
11
+ def initialize(opts)
12
+ @app = opts.fetch(:app)
13
+ @test_data = opts.fetch(:test_data)
14
+ end
15
+
16
+ protected
17
+
18
+ def raise_unless_status(expected_status, short_description)
19
+ message = "%s failed (#{last_response.status})\n#{last_response.body}" % short_description
20
+ raise message unless last_response.status == expected_status
21
+ end
22
+
23
+ ##### JSON Tools #####
24
+
25
+ def post_as_json(short_description, path, data = {}, options = {})
26
+ header 'Content-Type', 'application/json'
27
+ header 'Accept', 'application/json'
28
+ post path, data.to_json
29
+ raise_unless_status options[:expected_status] || 201, short_description
30
+ end
31
+
32
+ def put_as_json(short_description, path, data = {}, options = {})
33
+ header 'Content-Type', 'application/json'
34
+ header 'Accept', 'application/json'
35
+ put path, data.to_json
36
+ raise_unless_status options[:expected_status] || 201, short_description
37
+ end
38
+
39
+ def hash_from_response_json
40
+ HashWithIndifferentAccess.new( JSON.parse(last_response.body) )
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ module Kookaburra
2
+ class GivenDriver
3
+ def initialize(opts)
4
+ @api = opts.fetch(:api_driver)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # Factories for setting up attribute hashes
2
+ module Kookaburra
3
+ class TestData
4
+ class Factory
5
+ attr_reader :test_data
6
+ def initialize(test_data)
7
+ @test_data = test_data
8
+ end
9
+
10
+ protected
11
+ def hash_for_merging(overrides = {})
12
+ HashWithIndifferentAccess.new( overrides.with_indifferent_access ).tap do |hash_to_merge|
13
+ yield hash_to_merge if block_given?
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ # This is the mechanism for sharing state between Cucumber steps.
2
+ # If you're using instance variables, YOU'RE DOING IT WRONG.
3
+ module Kookaburra
4
+ class TestData
5
+ def initialize
6
+ @data = Hash.new do |hash, key|
7
+ hash[key] = Hash.new { |hash, key| hash[key] = HashWithIndifferentAccess.new }
8
+ end
9
+ end
10
+
11
+ def __collection(collection_key)
12
+ @data[collection_key]
13
+ end
14
+ def __fetch_data(collection_key, value_key)
15
+ __collection(collection_key).fetch(value_key)
16
+ rescue IndexError => e
17
+ raise e.exception("Key #{value_key.inspect} not found in #{collection_key}")
18
+ end
19
+ def __get_data(collection_key, value_key)
20
+ __collection(collection_key)[value_key]
21
+ end
22
+ def __set_data(collection_key, value_key, value_hash = {})
23
+ __collection(collection_key)[value_key] = HashWithIndifferentAccess.new(value_hash)
24
+ end
25
+
26
+ def self.provide_collection(name)
27
+ class_eval <<-RUBY
28
+ def #{name}(key = :default)
29
+ __get_data(:#{name}, key)
30
+ end
31
+ def fetch_#{name}(key = :default)
32
+ __fetch_data(:#{name}, key)
33
+ end
34
+ def set_#{name}(key, value_hash = {})
35
+ __set_data(:#{name}, key, value_hash)
36
+ end
37
+ RUBY
38
+ end
39
+
40
+ Defaults = HashWithIndifferentAccess.new
41
+ def default(key)
42
+ # NOTE: Marshal seems clunky, but gives us a deep copy.
43
+ # This keeps mutations from being preserved between test runs.
44
+ ( @default ||= Marshal::load(Marshal.dump(Defaults)) )[key]
45
+ end
46
+
47
+ def factory
48
+ @factory ||= Kookaburra::TestData::Factory.new(self)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ module HasBrowser
4
+ Unexpected500 = Class.new(StandardError)
5
+
6
+ # This will fail if the options hash does not include a value for the key :browser
7
+ def initialize(options = {})
8
+ super()
9
+ @browserish = options.fetch(:browser)
10
+ end
11
+
12
+ def browser
13
+ @browserish
14
+ end
15
+
16
+ def visit(*args)
17
+ browser.visit *args
18
+ no_500_error!
19
+ end
20
+
21
+ def no_500_error!
22
+ if browser.has_css?('head title', :text => 'Internal Server Error')
23
+ sleep 30 if ENV['GIMME_CRAP']
24
+ raise Unexpected500
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,178 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ module HasFields
4
+ module ClassMethods
5
+ def submit_button_locator(locator)
6
+ define_method(:submit_button_locator) { locator }
7
+ end
8
+
9
+ def has_radio_button_for(*names)
10
+ names.each do |n|
11
+ input_xpath_builder_names[n] = :build_radio_input_xpath
12
+ end
13
+ end
14
+
15
+ def has_select_for(*names)
16
+ names.each do |n|
17
+ input_xpath_builder_names[n] = :build_select_input_xpath
18
+ end
19
+ end
20
+
21
+ def input_xpath_builder_names
22
+ # Inputs without a type attribute, or with a custom or unknown type are
23
+ # textual, so we need to exclude the types that we don't want.
24
+ @input_xpath_builder_names ||= HashWithIndifferentAccess.new(:build_textual_input_xpath)
25
+ end
26
+
27
+ def is_select?(name)
28
+ input_xpath_builder_names[name] == :build_select_input_xpath
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ def build_input_xpath(attribute)
34
+ "(" +
35
+ "(.//textarea)|" +
36
+ "(.//select)|" +
37
+ "(.//input[(not(@type='button')) and (not(@type='reset')) and (not(@type='submit'))])" +
38
+ ")[contains(@id,'_#{attribute}')]"
39
+ # TODO: Avoid false positives by matching to the end of the id
40
+ # string for everything but radio buttons and checkboxes
41
+ # ... or something better than that.
42
+ end
43
+
44
+ def build_textual_input_xpath(attribute, value=nil)
45
+ # Since the type attribute might not be present or might contain
46
+ # just about any value, check for and exclude nodes with type
47
+ # containing a non-textual value.
48
+ "(" +
49
+ "(.//textarea)|" +
50
+ "(.//input[" +
51
+ "(not(@type='button')) and (not(@type='checkbox')) and (not(@type='radio')) and " +
52
+ "(not(@type='reset')) and (not(@type='submit'))" +
53
+ "])" +
54
+ ")[" +
55
+ "substring(@id,string-length(@id)-#{attribute.length-1},#{attribute.length})='#{attribute}'" +
56
+ "]"
57
+ end
58
+
59
+ def build_radio_input_xpath(attribute, value=nil)
60
+ # The id will be the attribute suffixed by something representative of
61
+ # the button value, with an underscore in between, but we don't necessarily
62
+ # know the correct suffix, so ensure that the "_" is in the id, and then
63
+ # find the correct match using the value attribute.
64
+ xpath = ".//input[@type='radio'][contains(@id, #{attribute}_)]"
65
+ xpath = "(#{xpath})[@value='#{value}']"
66
+ end
67
+
68
+ def build_select_input_xpath(attribute, value=nil)
69
+ xpath = ".//select[contains(@id, #{attribute})]/option[text()='#{value}']"
70
+ end
71
+
72
+ # Find the specified input by its field name. The value argument is
73
+ # used to find the correct input if (as in the case of a radio button)
74
+ # there might be a separate input element for each value the field might
75
+ # contain. Otherwise, it is ignored. When the name value will be used,
76
+ # its #to_s value must be valid as an xpath string body expression.
77
+ def find_input(attribute, value=nil, nth=1, msg=nil)
78
+ method = self.class.input_xpath_builder_names[attribute]
79
+ #TODO: Build a qualified attribute name, and pass that to the xpath builder
80
+ # to handle forms with multiple models that may have common attribute
81
+ # names.
82
+ xpath = send(method, attribute.to_s, value.to_s)
83
+ xpath = "(#{xpath})[#{nth}]" if nth > 1
84
+ browser.find(:xpath, xpath, :message => msg)
85
+ end
86
+
87
+ def submit_button_locator
88
+ raise "Subclass responsibility!"
89
+ end
90
+
91
+ def submit!
92
+ click_on submit_button_locator
93
+ no_500_error!
94
+ end
95
+
96
+ def fill_in_fields(hash, idx = 0)
97
+ @body = nil
98
+
99
+ # If not explicitly ordered, then we must think the fill-in order doesn't
100
+ # matter, so make the order more assuredly arbitrary to find out sooner
101
+ # if that's not a correct thought.
102
+ hash = hash.map.shuffle if Hash === hash && ! ActiveSupport::OrderedHash === hash
103
+
104
+ hash.each do |field, value|
105
+ fill_in_form_element(field, value, idx)
106
+ end
107
+ end
108
+
109
+ private
110
+ def fill_in_form_element(attribute, value, idx = 0, opts = {})
111
+ msg = "cannot find the field for '#{attribute}' with value '#{value}' to fill it in."
112
+ input = find_input(attribute, value, idx+1, msg)
113
+ set_value(input, value, attribute)
114
+ end
115
+ def set_value(input, value, attribute)
116
+ if self.class.is_select?(attribute)
117
+ input.select_option
118
+ else
119
+ input.set value
120
+ end
121
+ end
122
+ public
123
+
124
+ def tag_visible?(css)
125
+ browser.has_css?(css, :visible => true)
126
+ end
127
+
128
+ def no_tag_visible?(css)
129
+ # use of wait_until is supposed to be redundant with #has_no_css?, but
130
+ # getting intermittent Selenium::WebDriver::Error::StaleElementReferenceError.
131
+ # Just wrapping in an explicit wait_until did not capture the exception,
132
+ # so apparently that one is explicitly passed through rather than being
133
+ # retried.
134
+ browser.wait_until do
135
+ begin
136
+ browser.has_no_css?(css, :visible => true)
137
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError
138
+ next false # Keep trying if not yet timed out.
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+
146
+ def find_disableable_inputs(attribute)
147
+ inputs = []
148
+ browser.wait_until do
149
+ inputs = browser.all(:xpath, build_input_xpath(attribute))
150
+ inputs.present?
151
+ end
152
+ inputs
153
+ end
154
+
155
+ def form_element_disabled?(attribute, *_)
156
+ # TODO (SLG): this will only check the currently-selected radio button, not all of them
157
+ find_disableable_inputs(attribute).all? { |input| !!input[:disabled] }
158
+ rescue Capybara::TimeoutError
159
+ puts "Timed out trying to find disableable inputs for #{attribute}"
160
+ end
161
+
162
+ def all_form_elements_disabled?(desc, attr_names, &b)
163
+ errs = attr_names.inject([]) do |mem, attr_name|
164
+ (mem << "#{desc} #{attr_name} is not read-only") unless form_element_disabled?(attr_name)
165
+ mem
166
+ end
167
+ raise errs.join("\n") if errs.present?
168
+ true
169
+ end
170
+ end
171
+
172
+ def self.included(receiver)
173
+ receiver.extend ClassMethods
174
+ receiver.send :include, InstanceMethods
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,50 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ module HasStrategies
4
+ class Strategy
5
+ class_attribute :tag
6
+ attr_reader :ui_component
7
+ def initialize(ui_component)
8
+ @ui_component, @tag = ui_component
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def strategy(tag, &proc)
14
+ Class.new(::Kookaburra::UIDriver::HasStrategies::Strategy).tap { |klass|
15
+ klass.tag = tag
16
+ klass.module_eval &proc
17
+ self.strategy_classes << klass
18
+ }
19
+ end
20
+
21
+ def use_strategy_for(*method_names)
22
+ def_delegators :current_strategy, *method_names
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+ def strategies
28
+ @strategies ||= strategy_classes.map { |klass| klass.new(self) }
29
+ end
30
+
31
+ def current_strategy
32
+ strategies.detect(&:applies?) or raise 'No applicable strategy!'
33
+ end
34
+
35
+ def strategy_tag
36
+ current_strategy.tag
37
+ end
38
+ end
39
+
40
+ def self.included(receiver)
41
+ receiver.class_attribute :strategy_classes
42
+ receiver.strategy_classes = []
43
+
44
+ receiver.extend Forwardable
45
+ receiver.extend ClassMethods
46
+ receiver.send :include, InstanceMethods
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,10 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ module HasSubcomponents
4
+ # Nothing to see here, really -- this just gives us a way to logically group subsections in a file
5
+ def subcomponent(name, &proc)
6
+ class_eval &proc
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ module HasUIComponent
4
+ module ClassMethods
5
+ def ui_component(component_name)
6
+ component_class = component_name.to_s.camelize.constantize
7
+
8
+ self.ui_component_names << component_name
9
+
10
+ define_method(component_name) do
11
+ options = { :browser => browser, :test_data => test_data }
12
+ # TODO: memoize the following line?
13
+ component_class.new(options)
14
+ end
15
+
16
+ define_method("has_#{component_name}?") do
17
+ send(component_name).visible?
18
+ end
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def ui_components
24
+ ui_component_names.map { |name| self.send(name) }
25
+ end
26
+ end
27
+
28
+ def self.included(receiver)
29
+ receiver.class_attribute :ui_component_names
30
+ receiver.ui_component_names = []
31
+
32
+ receiver.extend ClassMethods
33
+ receiver.send :include, InstanceMethods
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,103 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ class UIComponent
4
+ include HasBrowser
5
+ include HasFields
6
+ include HasStrategies
7
+ extend HasSubcomponents
8
+
9
+ attr_reader :test_data
10
+
11
+ # This will fail if the options hash does not include a value for the key :test_data
12
+ def initialize(options = {})
13
+ super
14
+ @test_data = options.fetch(:test_data)
15
+ end
16
+
17
+ ##### Class macros #####
18
+ def self.component_locator(locator)
19
+ define_method(:component_locator) { locator }
20
+ end
21
+
22
+ def self.component_path(path)
23
+ case path
24
+ when Symbol
25
+ alias_method :component_path, path
26
+ else
27
+ define_method(:component_path) { path }
28
+ end
29
+ end
30
+
31
+ def self.path_id_regex(regex)
32
+ define_method(:path_id_regex) { regex }
33
+ end
34
+
35
+ ##### Instance methods #####
36
+
37
+ def visible!
38
+ raise "#{self.class} not currently visible!" unless visible?
39
+ end
40
+
41
+ def visible?
42
+ no_500_error!
43
+ _visible?
44
+ end
45
+
46
+ def _visible?
47
+ component_visible?
48
+ end
49
+ private :_visible?
50
+
51
+ def show(opts = {})
52
+ return if visible?
53
+ raise "Subclass responsibility!" unless self.respond_to?(:component_path)
54
+ path = component_path
55
+ path << ( '?' + opts[:query_params].map{|kv| "%s=%s" % kv}.join('&') ) if opts[:query_params]
56
+ visit path
57
+ end
58
+
59
+ def refresh
60
+ visit component_path
61
+ end
62
+
63
+ def show!(opts = {})
64
+ show opts
65
+ visible!
66
+ end
67
+
68
+ def at_path?
69
+ (component_path.to_a + alternate_paths.to_a).include?(browser.current_path)
70
+ end
71
+
72
+ def component_visible?
73
+ at_path? && browser.has_css?(component_locator)
74
+ end
75
+
76
+ def alternate_paths
77
+ []
78
+ end
79
+
80
+ private
81
+ def id_from_path
82
+ browser.current_path =~ path_id_regex
83
+ $1.present? ? $1.to_i : nil
84
+ end
85
+
86
+ def fill_in(locator, options)
87
+ in_component { browser.fill_in(locator, options) }
88
+ end
89
+
90
+ def click_on(locator)
91
+ in_component { browser.find(locator).click }
92
+ end
93
+
94
+ def choose(locator)
95
+ in_component { browser.choose(locator) }
96
+ end
97
+
98
+ def in_component(&blk)
99
+ browser.within(component_locator, &blk)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,13 @@
1
+ module Kookaburra
2
+ class UIDriver
3
+ include HasBrowser
4
+ include HasUIComponent
5
+
6
+ attr_reader :test_data
7
+
8
+ def initialize(opts = {})
9
+ super
10
+ @test_data = opts.fetch(:test_data)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # Can't use a custom World class, because cucumber-rails is already doing that
2
+ module Kookaburra
3
+ module WorldSetup
4
+ def ui; @drivers[:ui_driver ]; end
5
+ def api; @drivers[:api_driver ]; end
6
+ def given; @drivers[:given_driver]; end
7
+
8
+ def kookaburra_world_setup
9
+ @drivers = Kookaburra.drivers
10
+ end
11
+ end
12
+ end
data/lib/kookaburra.rb ADDED
@@ -0,0 +1,19 @@
1
+ require File.join(File.dirname(__FILE__), *%w[requires])
2
+
3
+ module Kookaburra
4
+ def self.drivers
5
+ test_data = Kookaburra::TestData.new
6
+ api_driver = Kookaburra::APIDriver.new({
7
+ :app => Capybara.app,
8
+ :test_data => test_data,
9
+ })
10
+ given_driver = Kookaburra::GivenDriver.new({
11
+ :api_driver => api_driver,
12
+ })
13
+ ui_driver = Kookaburra::UIDriver.new({
14
+ :browser => Capybara.current_session,
15
+ :test_data => test_data,
16
+ })
17
+ { :api_driver => api_driver, :given_driver => given_driver, :ui_driver => ui_driver }
18
+ end
19
+ end
data/lib/requires.rb ADDED
@@ -0,0 +1,17 @@
1
+ def kookaburra_require_glob(path_glob)
2
+ Dir.glob(path_glob).each do |file|
3
+ require file
4
+ end
5
+ end
6
+
7
+ def kookaburra_require_all_relative_to(base_path, *relative_path_array)
8
+ path = File.join(base_path, *relative_path_array.flatten)
9
+ kookaburra_require_glob File.join(path, '*.rb')
10
+ end
11
+
12
+ # Require specific paths from the bottom up. Hooray for dependency graphs!
13
+ base = File.dirname(__FILE__)
14
+ kookaburra_require_all_relative_to base, %w[kookaburra ui_driver mixins]
15
+ kookaburra_require_all_relative_to base, %w[kookaburra ui_driver]
16
+ kookaburra_require_all_relative_to base, %w[kookaburra test_data]
17
+ kookaburra_require_all_relative_to base, %w[kookaburra]
data/test/helper.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'minitest/unit'
11
+
12
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ require 'kookaburra'
15
+
16
+ class MiniTest::Unit::TestCase
17
+ end
18
+
19
+ MiniTest::Unit.autorun
metadata ADDED
@@ -0,0 +1,209 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kookaburra
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Renewable Funding, LLC
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ none: false
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ hash: 7
27
+ segments:
28
+ - 3
29
+ - 0
30
+ version: "3.0"
31
+ name: activesupport
32
+ prerelease: false
33
+ type: :runtime
34
+ requirement: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ version_requirements: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ hash: 3
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ name: rack
46
+ prerelease: false
47
+ type: :runtime
48
+ requirement: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ version_requirements: &id003 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ name: minitest
60
+ prerelease: false
61
+ type: :development
62
+ requirement: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ version_requirements: &id004 !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ hash: 7
70
+ segments:
71
+ - 0
72
+ - 6
73
+ - 0
74
+ version: 0.6.0
75
+ name: yard
76
+ prerelease: false
77
+ type: :development
78
+ requirement: *id004
79
+ - !ruby/object:Gem::Dependency
80
+ version_requirements: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ hash: 23
86
+ segments:
87
+ - 1
88
+ - 0
89
+ - 0
90
+ version: 1.0.0
91
+ name: bundler
92
+ prerelease: false
93
+ type: :development
94
+ requirement: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ version_requirements: &id006 !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ hash: 7
102
+ segments:
103
+ - 1
104
+ - 6
105
+ - 4
106
+ version: 1.6.4
107
+ name: jeweler
108
+ prerelease: false
109
+ type: :development
110
+ requirement: *id006
111
+ - !ruby/object:Gem::Dependency
112
+ version_requirements: &id007 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ name: rcov
122
+ prerelease: false
123
+ type: :development
124
+ requirement: *id007
125
+ - !ruby/object:Gem::Dependency
126
+ version_requirements: &id008 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ hash: 15
132
+ segments:
133
+ - 1
134
+ - 2
135
+ - 8
136
+ version: 1.2.8
137
+ name: reek
138
+ prerelease: false
139
+ type: :development
140
+ requirement: *id008
141
+ description: Cucumber + Capybara = Kookaburra? It made sense at the time.
142
+ email: devteam@renewfund.com
143
+ executables: []
144
+
145
+ extensions: []
146
+
147
+ extra_rdoc_files:
148
+ - LICENSE.txt
149
+ - README.rdoc
150
+ files:
151
+ - .document
152
+ - .rvmrc
153
+ - Gemfile
154
+ - Gemfile.lock
155
+ - LICENSE.txt
156
+ - README.rdoc
157
+ - Rakefile
158
+ - VERSION
159
+ - kookaburra.gemspec
160
+ - lib/kookaburra.rb
161
+ - lib/kookaburra/api_driver.rb
162
+ - lib/kookaburra/given_driver.rb
163
+ - lib/kookaburra/test_data.rb
164
+ - lib/kookaburra/test_data/factory.rb
165
+ - lib/kookaburra/ui_driver.rb
166
+ - lib/kookaburra/ui_driver/mixins/has_browser.rb
167
+ - lib/kookaburra/ui_driver/mixins/has_fields.rb
168
+ - lib/kookaburra/ui_driver/mixins/has_strategies.rb
169
+ - lib/kookaburra/ui_driver/mixins/has_subcomponents.rb
170
+ - lib/kookaburra/ui_driver/mixins/has_ui_component.rb
171
+ - lib/kookaburra/ui_driver/ui_component.rb
172
+ - lib/kookaburra/world_setup.rb
173
+ - lib/requires.rb
174
+ - test/helper.rb
175
+ homepage: http://github.com/projectdx/kookaburra
176
+ licenses:
177
+ - MIT
178
+ post_install_message:
179
+ rdoc_options: []
180
+
181
+ require_paths:
182
+ - lib
183
+ required_ruby_version: !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ hash: 3
189
+ segments:
190
+ - 0
191
+ version: "0"
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ hash: 3
198
+ segments:
199
+ - 0
200
+ version: "0"
201
+ requirements: []
202
+
203
+ rubyforge_project:
204
+ rubygems_version: 1.8.15
205
+ signing_key:
206
+ specification_version: 3
207
+ summary: WindowDriver testing pattern for Ruby apps
208
+ test_files: []
209
+