mediawiki_selenium 0.4.3 → 1.0.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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