mediawiki_selenium 0.4.3 → 1.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitreview +1 -1
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +1 -1
  6. data/README.md +108 -55
  7. data/bin/mediawiki-selenium-init +5 -0
  8. data/lib/mediawiki_selenium.rb +10 -19
  9. data/lib/mediawiki_selenium/browser_factory.rb +24 -0
  10. data/lib/mediawiki_selenium/browser_factory/base.rb +212 -0
  11. data/lib/mediawiki_selenium/browser_factory/chrome.rb +27 -0
  12. data/lib/mediawiki_selenium/browser_factory/firefox.rb +34 -0
  13. data/lib/mediawiki_selenium/browser_factory/phantomjs.rb +21 -0
  14. data/lib/mediawiki_selenium/configuration_error.rb +4 -0
  15. data/lib/mediawiki_selenium/environment.rb +494 -0
  16. data/lib/mediawiki_selenium/initializer.rb +19 -0
  17. data/lib/mediawiki_selenium/page_factory.rb +38 -0
  18. data/lib/mediawiki_selenium/remote_browser_factory.rb +87 -0
  19. data/lib/mediawiki_selenium/step_definitions.rb +5 -0
  20. data/lib/mediawiki_selenium/support.rb +3 -0
  21. data/lib/mediawiki_selenium/support/env.rb +3 -127
  22. data/lib/mediawiki_selenium/support/hooks.rb +23 -34
  23. data/lib/mediawiki_selenium/support/modules/api_helper.rb +44 -5
  24. data/lib/mediawiki_selenium/support/pages.rb +4 -0
  25. data/lib/mediawiki_selenium/support/pages/api_page.rb +1 -0
  26. data/lib/mediawiki_selenium/support/pages/login_page.rb +3 -12
  27. data/lib/mediawiki_selenium/support/pages/random_page.rb +2 -12
  28. data/lib/mediawiki_selenium/support/pages/reset_preferences_page.rb +3 -12
  29. data/lib/mediawiki_selenium/version.rb +1 -1
  30. data/mediawiki_selenium.gemspec +9 -3
  31. data/spec/api_helper_spec.rb +84 -0
  32. data/spec/browser_factory/base_spec.rb +211 -0
  33. data/spec/browser_factory/chrome_spec.rb +36 -0
  34. data/spec/browser_factory/firefox_spec.rb +60 -0
  35. data/spec/browser_factory/phantomjs_spec.rb +38 -0
  36. data/spec/environment_spec.rb +474 -0
  37. data/spec/page_factory_spec.rb +61 -0
  38. data/spec/remote_browser_factory_spec.rb +50 -0
  39. data/spec/spec_helper.rb +4 -0
  40. data/templates/tests/browser/environments.yml +35 -0
  41. data/templates/tests/browser/features/support/env.rb +6 -0
  42. metadata +122 -20
  43. data/lib/mediawiki_selenium/support/modules/sauce_helper.rb +0 -13
  44. data/lib/mediawiki_selenium/support/modules/url_module.rb +0 -21
  45. data/spec/README +0 -2
@@ -0,0 +1,27 @@
1
+ module MediawikiSelenium
2
+ module BrowserFactory
3
+ # Constructs new Chrome browser instances. The following configuration is
4
+ # supported.
5
+ #
6
+ # - browser_language
7
+ # - browser_user_agent
8
+ #
9
+ # @see Base
10
+ #
11
+ class Chrome < Base
12
+ bind(:browser_language) do |language, options|
13
+ options[:prefs]["intl.accept_languages"] = language
14
+ end
15
+
16
+ bind(:browser_user_agent) do |user_agent, options|
17
+ options[:args] << "--user-agent=#{user_agent}"
18
+ end
19
+
20
+ protected
21
+
22
+ def default_browser_options
23
+ super.merge(args: [], prefs: {})
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ module MediawikiSelenium
2
+ module BrowserFactory
3
+ # Constructs new Firefox browser instances. The following configuration is
4
+ # supported.
5
+ #
6
+ # - browser_language
7
+ # - browser_timeout
8
+ # - browser_user_agent
9
+ #
10
+ # @see Base
11
+ #
12
+ class Firefox < Base
13
+ bind(:browser_timeout) do |timeout, options|
14
+ timeout = timeout.to_i
15
+ options[:profile]["dom.max_script_run_time"] = timeout
16
+ options[:profile]["dom.max_chrome_script_run_time"] = timeout
17
+ end
18
+
19
+ bind(:browser_language) do |language, options|
20
+ options[:profile]["intl.accept_languages"] = language
21
+ end
22
+
23
+ bind(:browser_user_agent) do |user_agent, options|
24
+ options[:profile]["general.useragent.override"] = user_agent
25
+ end
26
+
27
+ protected
28
+
29
+ def default_browser_options
30
+ super.merge(profile: Selenium::WebDriver::Firefox::Profile.new)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module MediawikiSelenium
2
+ module BrowserFactory
3
+ # Constructs new Phantomjs browser instances. The following configuration is
4
+ # supported.
5
+ #
6
+ # - browser_language
7
+ # - browser_user_agent
8
+ #
9
+ # @see Base
10
+ #
11
+ class Phantomjs < Base
12
+ bind(:browser_language) do |language, options|
13
+ options[:desired_capabilities]["phantomjs.page.customHeaders.Accept-Language"] = language
14
+ end
15
+
16
+ bind(:browser_user_agent) do |user_agent, options|
17
+ options[:desired_capabilities]["phantomjs.page.settings.userAgent"] = user_agent
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ module MediawikiSelenium
2
+ class ConfigurationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,494 @@
1
+ require "yaml"
2
+
3
+ module MediawikiSelenium
4
+ # Provides an interface that unifies environmental configuration, page
5
+ # objects, and browser setup. Additionally, it provides a DSL for switching
6
+ # between user/wiki/browser contexts in ways that help to decouple test
7
+ # implementation from the target wikis.
8
+ #
9
+ # Default configuration for various resources (wiki URLs, users, etc.) is
10
+ # typically loaded from an `environments.yml` YAML file in the current
11
+ # working directory. It should contain defaults for each environment in
12
+ # which the tests are expected to run, indexed by environment name.
13
+ #
14
+ # beta:
15
+ # mediawiki_url: http://en.wikipedia.beta.wmflabs.org/wiki/
16
+ # mediawiki_user: Selenium_user
17
+ # test2:
18
+ # mediawiki_url: http://test2.wikipedia.org/wiki/
19
+ # mediawiki_user: Selenium_user
20
+ #
21
+ # Which default set to use is determined by the value of the
22
+ # `MEDIAWIKI_ENVIRONMENT` environment variable. (See {load} and
23
+ # {load_default}.)
24
+ #
25
+ # Any additional configuration specified via environment variables overrides
26
+ # what is specified in the YAML file. For example, the following would use
27
+ # the default configuration as specified under `beta` in the YAML file but
28
+ # define `mediawiki_user` as `Other_user` instead of `Selenium_user`.
29
+ #
30
+ # export MEDIAWIKI_ENVIRONMENT=beta MEDIAWIKI_USER=Other_user
31
+ # bundle exec cucumber ...
32
+ #
33
+ # There are various methods that allow you to perform actions in the context
34
+ # of some alternative resource, for example as a different user using
35
+ # {#as_user}, or on different wiki using {#on_wiki}. Instead of referencing
36
+ # the exact user names or URLs for these resources, you reference them by an
37
+ # ID which corresponds to configuration made in `environments.yml`.
38
+ #
39
+ # # environments.yml:
40
+ # beta:
41
+ # # ...
42
+ # mediawiki_user_b: Selenium_user2
43
+ #
44
+ # # step definition:
45
+ # Given(/^user B has linked to a page I created$/) do
46
+ # as_user(:b) { api.create_page(...) }
47
+ # end
48
+ #
49
+ # This level of abstraction is intended to reduce coupling between tests
50
+ # and test environments, and should promote step definitions that are more
51
+ # readable and congruent with the natural-language steps they implement.
52
+ #
53
+ class Environment
54
+ include Comparable
55
+
56
+ class << self
57
+ attr_accessor :default_configuration
58
+
59
+ # Instantiates a new environment using the given set of default
60
+ # configuration from `environments.yml` in the current working
61
+ # directory, and the additional hash of environment variables.
62
+ #
63
+ # @param name [String] Name of the environment.
64
+ # @param extra [Hash] Additional configuration to use.
65
+ #
66
+ def load(name, extra = {})
67
+ name = name.to_s
68
+ configs = []
69
+
70
+ unless name.empty?
71
+ envs = YAML.load_file(default_configuration)
72
+ raise ConfigurationError, "unknown environment `#{name}`" unless envs.include?(name)
73
+ configs << envs[name]
74
+ end
75
+
76
+ configs << extra
77
+
78
+ new(*configs)
79
+ end
80
+
81
+ # Instantiates a new environment from the values of `ENV` and the
82
+ # default configuration corresponding to `ENV["MEDIAWIKI_ENVIRONMENT"]`,
83
+ # if one is defined.
84
+ #
85
+ # @see load
86
+ #
87
+ def load_default
88
+ load(ENV["MEDIAWIKI_ENVIRONMENT"], ENV)
89
+ end
90
+ end
91
+
92
+ self.default_configuration = "environments.yml"
93
+
94
+ def initialize(*configs)
95
+ @_config = configs.map { |config| normalize_config(config) }.reduce(:merge)
96
+ @_factory_cache = {}
97
+ end
98
+
99
+ # Whether the given environment is equal to this one. Two environments are
100
+ # considered equal if they have identical configuration.
101
+ #
102
+ # @param other [Environment]
103
+ #
104
+ # @return [Boolean]
105
+ #
106
+ def ==(other)
107
+ config == other.config
108
+ end
109
+
110
+ # Returns the configured value for the given env variable name.
111
+ #
112
+ # @see #lookup
113
+ #
114
+ # @param key [Symbol] Environment variable name.
115
+ #
116
+ # @return [String]
117
+ #
118
+ def [](key)
119
+ lookup(key)
120
+ end
121
+
122
+ # Executes the given block within the context of an environment that's
123
+ # using the given alternative user and its password.
124
+ #
125
+ # @example
126
+ # Given(/^user B has linked to a page I created$/) do
127
+ # as_user(:b) { api.create_page(...) }
128
+ # end
129
+ #
130
+ # @param id [Symbol] Alternative user ID.
131
+ #
132
+ # @yield [user, password]
133
+ # @yieldparam user [String] Alternative MediaWiki user.
134
+ # @yieldparam password [String] Alternative MediaWiki password.
135
+ #
136
+ def as_user(id, &blk)
137
+ user = lookup(:mediawiki_user, id: id)
138
+ password = lookup(:mediawiki_password, id: id, default: -> { lookup(:mediawiki_password) })
139
+
140
+ with(mediawiki_user: user, mediawiki_password: password, &blk)
141
+ end
142
+
143
+ # Browser with which to drive tests.
144
+ #
145
+ # @return [Watir::Browser]
146
+ #
147
+ def browser
148
+ browser_factory.browser_for(browser_config)
149
+ end
150
+
151
+ # Factory used to instantiate and open new browsers.
152
+ #
153
+ # @param browser [Symbol] Browser name.
154
+ #
155
+ # @return [BrowserFactory::Base]
156
+ #
157
+ def browser_factory(browser = browser_name)
158
+ browser = browser.to_s.downcase.to_sym
159
+
160
+ @_factory_cache[[remote?, browser]] ||= BrowserFactory.new(browser).tap do |factory|
161
+ factory.bind(:_browser_session)
162
+ factory.extend(RemoteBrowserFactory) if remote?
163
+ end
164
+ end
165
+
166
+ # Name of the browser we're using.
167
+ #
168
+ # @return [Symbol]
169
+ #
170
+ def browser_name
171
+ lookup(:browser, default: "firefox").downcase.to_sym
172
+ end
173
+
174
+ # A reference to this environment. Can be used in conjunction with {#[]}
175
+ # for syntactic sugar in looking up environment configuration where `self`
176
+ # would otherwise seem ambiguous.
177
+ #
178
+ # @example
179
+ # Then(/^I see my username on the page$/) do
180
+ # expect(on(SomePage).html).to include(env[:mediawiki_user])
181
+ # end
182
+ #
183
+ # @return [self]
184
+ #
185
+ def env
186
+ self
187
+ end
188
+
189
+ # Executes the given block within the context of an environment that uses
190
+ # a unique browser session and possibly different configuration. Note that
191
+ # any given configuration overrides are scoped with a `:browser_` prefix.
192
+ #
193
+ # @example Implement a "logged out" step following some authenticated one
194
+ # When(/^I do something while logged in$/) do
195
+ # in_browser(:a) do
196
+ # # perform action in logged in session
197
+ # end
198
+ # end
199
+ #
200
+ # When(/^I do something else after logging out$/) do
201
+ # in_browser(:b) do
202
+ # # perform action in logged out session without actually logging
203
+ # # out since that would affect all auth sessions for the user
204
+ # end
205
+ # end
206
+ #
207
+ # @example Perform a subsequent step requiring a different browser language
208
+ # When(/^I visit the same page with my browser in Spanish$/) do |scenario, block|
209
+ # in_browser(:a, language: "es") do
210
+ # # test that it now serves up Spanish text
211
+ # end
212
+ # end
213
+ #
214
+ # @param id [Symbol] Browser session ID.
215
+ # @param overrides [Hash] Browser configuration overrides.
216
+ #
217
+ # @yield [*args] Overridden browser configuration.
218
+ #
219
+ def in_browser(id, overrides = {}, &blk)
220
+ overrides = overrides.each.with_object({}) do |(name, value), hash|
221
+ hash["browser_#{name}".to_sym] = value
222
+ end
223
+
224
+ with(overrides.merge(_browser_session: id), &blk)
225
+ end
226
+
227
+ # Whether browsers should be left open after each scenario completes.
228
+ #
229
+ def keep_browser_open?
230
+ lookup(:keep_browser_open, default: "false") == "true"
231
+ end
232
+
233
+ # Returns the configured value for the given env variable name.
234
+ #
235
+ # @example Value of `:browser_language` and fail if it wasn't provided
236
+ # env.lookup(:browser_language)
237
+ #
238
+ # @example Value of `:browser_language` alternative `:b`
239
+ # env.lookup(:browser_language, id: :b)
240
+ #
241
+ # @example Value of `:browser_language` or try `:browser_lang`
242
+ # env.lookup(:browser_language, default: -> { env.lookup(:browser_lang) })
243
+ #
244
+ # @param key [Symbol] Environment variable name.
245
+ # @param options [Hash] Options.
246
+ # @option options [Symbol] :id Alternative ID.
247
+ # @option options [Object, Proc] :default Default value or promise of a value.
248
+ #
249
+ # @return [String]
250
+ #
251
+ def lookup(key, options = {})
252
+ key = "#{key}_#{options[:id]}" if options.fetch(:id, nil)
253
+ key = normalize_key(key)
254
+
255
+ value = config[key]
256
+
257
+ if value.nil? || value.to_s.empty?
258
+ if options.include?(:default)
259
+ options[:default].is_a?(Proc) ? options[:default].call : options[:default]
260
+ else
261
+ raise ConfigurationError, "missing configuration for `#{key}`"
262
+ end
263
+ else
264
+ value
265
+ end
266
+ end
267
+
268
+ # Returns the configured values for the given env variable names.
269
+ #
270
+ # @param keys [Array<Symbol>] Environment variable names.
271
+ # @param options [Hash] Options.
272
+ # @option options [Symbol] :id Alternative ID.
273
+ # @option options [Object] :default Default if no configuration is found.
274
+ #
275
+ # @return [Array<String>]
276
+ #
277
+ # @see #lookup
278
+ #
279
+ def lookup_all(keys, options = {})
280
+ keys.each.with_object({}) do |key, hash|
281
+ hash[key] = lookup(key, options)
282
+ end
283
+ end
284
+
285
+ # Executes the given block within the context of an environment that's
286
+ # using the given alternative wiki URL and its corresponding API endpoint.
287
+ #
288
+ # If no API URL is explicitly defined for the given alternative, one is
289
+ # constructed relative to the wiki URL.
290
+ #
291
+ # @example Visit a random page on wiki B
292
+ # on_wiki(:b) { visit(RandomPage) }
293
+ #
294
+ # @param id [Symbol] Alternative wiki ID.
295
+ #
296
+ # @yield [wiki_url]
297
+ # @yieldparam wiki_url [String] Alternative wiki URL.
298
+ #
299
+ def on_wiki(id, &blk)
300
+ with_alternative(:mediawiki_url, id, &blk)
301
+ end
302
+
303
+ # Returns the current value for `:mediawiki_password` or the value for the
304
+ # given alternative.
305
+ #
306
+ # @param id [Symbol] Alternative user ID.
307
+ #
308
+ # @return [String]
309
+ #
310
+ def password(id = nil)
311
+ lookup(password_variable, id: id)
312
+ end
313
+
314
+ # Whether this environment has been configured to use remote browser
315
+ # sessions.
316
+ #
317
+ # @return [Boolean]
318
+ #
319
+ def remote?
320
+ RemoteBrowserFactory::REQUIRED_CONFIG.all? { |name| lookup(name, default: false) }
321
+ end
322
+
323
+ # Executes teardown tasks including instructing all browser factories to
324
+ # close any open browsers and perform their own teardown tasks.
325
+ #
326
+ # @example Teardown environment resources after each scenario completes
327
+ # After do
328
+ # teardown(scenario.status)
329
+ # end
330
+ #
331
+ # @param status [Symbol] Status of the executed scenario.
332
+ #
333
+ # @yield [browser]
334
+ # @yieldparam browser [Watir::Browser] Browser object, before it's closed.
335
+ #
336
+ def teardown(status = :passed)
337
+ @_factory_cache.each do |_, factory|
338
+ factory.each do |browser|
339
+ yield browser if block_given?
340
+ browser.close unless keep_browser_open?
341
+ end
342
+
343
+ factory.teardown(self, status)
344
+ end
345
+ end
346
+
347
+ # Returns a name from the given scenario.
348
+ #
349
+ # @param scenario [Cucumber::Ast::Scenario]
350
+ #
351
+ # @return [String]
352
+ #
353
+ def test_name(scenario)
354
+ if scenario.respond_to? :feature
355
+ "#{scenario.feature.title}: #{scenario.title}"
356
+ elsif scenario.respond_to? :scenario_outline
357
+ "#{scenario.scenario_outline.feature.title}: #{scenario.scenario_outline.title}: #{scenario.name}"
358
+ else
359
+ scenario.name
360
+ end
361
+ end
362
+
363
+ # Returns the current value for `:mediawiki_user` or the value for the
364
+ # given alternative.
365
+ #
366
+ # @param id [Symbol] Alternative user ID.
367
+ #
368
+ # @return [String]
369
+ #
370
+ def user(id = nil)
371
+ lookup(:mediawiki_user, id: id)
372
+ end
373
+
374
+ # Returns the current user, or the one for the given alternative, with all
375
+ # "_" replaced with " ".
376
+ #
377
+ # @param id [Symbol] Alternative user ID.
378
+ #
379
+ # @return [String]
380
+ #
381
+ def user_label(id = nil)
382
+ user(id).gsub("_", " ")
383
+ end
384
+
385
+ # Navigates the current browser to the given wiki.
386
+ #
387
+ # @param id [Symbol] Alternative wiki ID.
388
+ #
389
+ # @yield [url]
390
+ # @yieldparam url [String] Wiki URL.
391
+ #
392
+ def visit_wiki(id)
393
+ on_wiki(id) do |url|
394
+ browser.goto url
395
+ yield url if block_given?
396
+ end
397
+ end
398
+
399
+ # Qualifies any given relative path using the configured `:mediawiki_url`.
400
+ # Absolute URLs are left untouched.
401
+ #
402
+ # @example
403
+ # env = Environment.new(mediawiki_url: "http://an.example/wiki/")
404
+ #
405
+ # env.wiki_url # => "http://an.example/wiki/"
406
+ # env.wiki_url("page") # => "http://an.example/wiki/page"
407
+ # env.wiki_url("/page") # => "http://an.example/page"
408
+ # env.wiki_url("http://other.example") # => "http://other.example"
409
+ #
410
+ def wiki_url(path = nil)
411
+ url = lookup(:mediawiki_url)
412
+
413
+ if path
414
+ # Prefixing relative paths with an explicit "./" guarantees proper
415
+ # parsing of paths like "Special:Page" that would otherwise be
416
+ # confused for URI schemes.
417
+ if path.include?(":")
418
+ path_uri = URI.parse(path)
419
+ path = "./#{path}" if path_uri.class == URI::Generic && !path.start_with?("/")
420
+ end
421
+
422
+ url = URI.parse(url).merge(path).to_s
423
+ end
424
+
425
+ url
426
+ end
427
+
428
+ # Executes the given block within the context of a new environment
429
+ # configured using the alternative versions of the given options. The
430
+ # alternative configuration values are resolved using the given ID and
431
+ # passed to the block as arguments.
432
+ #
433
+ # @example Overwrite :foo with the :b alternative
434
+ # # given an environment with config { foo: "x", foo_b: "y", ... }
435
+ # with_alternative(:foo, :b) do |foo|
436
+ # self # => #<Environment @config = { foo: "y", ... }>
437
+ # foo # => "y"
438
+ # end
439
+ #
440
+ # @example Overwrite both :foo and :bar with the :b alternatives
441
+ # # given an environment with config { foo: "x", foo_b: "y", bar: "w", bar_b: "z" }
442
+ # with_alternative([:foo, :bar], :b) do |foo, bar|
443
+ # self # => #<Environment @config = { foo: "y", bar: "z", ... }>
444
+ # foo # => "y"
445
+ # bar # => "z"
446
+ # end
447
+ #
448
+ # @param names [Symbol|Array<Symbol>] Configuration option or options.
449
+ # @param id [Symbol] Alternative user ID.
450
+ #
451
+ # @yield [*args] Values of the overridden configuration.
452
+ #
453
+ def with_alternative(names, id, &blk)
454
+ with(lookup_all(Array(names), id: id), &blk)
455
+ end
456
+
457
+ protected
458
+
459
+ def config
460
+ @_config
461
+ end
462
+
463
+ private
464
+
465
+ def browser_config
466
+ lookup_all(browser_factory.all_binding_keys, default: nil).reject { |k, v| v.nil? }
467
+ end
468
+
469
+ def password_variable
470
+ name = lookup(:mediawiki_password_variable, default: "")
471
+ name.empty? ? :mediawiki_password : normalize_key(name)
472
+ end
473
+
474
+ def normalize_config(hash)
475
+ hash.each.with_object({}) { |(k, v), acc| acc[normalize_key(k)] = v }
476
+ end
477
+
478
+ def normalize_key(key)
479
+ key.to_s.downcase.to_sym
480
+ end
481
+
482
+ def with(overrides = {})
483
+ overrides = normalize_config(overrides)
484
+ original_config = @_config.dup
485
+
486
+ begin
487
+ @_config = @_config.merge(overrides)
488
+ yield *overrides.values if block_given?
489
+ ensure
490
+ @_config = original_config
491
+ end
492
+ end
493
+ end
494
+ end