Dduvall has uploaded a new change for review. https://gerrit.wikimedia.org/r/159644
Change subject: WIP Environment abstraction layer ...................................................................... WIP Environment abstraction layer This is a very early WIP of an environment abstraction layer that binds environment configuration to browser instantiation and provides switching between different wiki, user, and other contexts. Change-Id: I1a3aa64b8fdca73aff2778ec12143a8789e4843f --- A .rspec M Gemfile M lib/mediawiki_selenium.rb A lib/mediawiki_selenium/browser_factory.rb A lib/mediawiki_selenium/browser_factory/base.rb A lib/mediawiki_selenium/browser_factory/chrome.rb A lib/mediawiki_selenium/browser_factory/firefox.rb A lib/mediawiki_selenium/browser_factory/phantomjs.rb A lib/mediawiki_selenium/environment.rb A lib/mediawiki_selenium/step_definitions.rb A lib/mediawiki_selenium/support.rb M lib/mediawiki_selenium/support/env.rb A lib/mediawiki_selenium/support/pages.rb M lib/mediawiki_selenium/support/pages/api_page.rb M lib/mediawiki_selenium/support/pages/login_page.rb M lib/mediawiki_selenium/support/pages/random_page.rb M lib/mediawiki_selenium/support/pages/reset_preferences_page.rb M mediawiki_selenium.gemspec A spec/environment_spec.rb A spec/spec_helper.rb 20 files changed, 560 insertions(+), 20 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/selenium refs/changes/44/159644/1 diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..4e1e0d2 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--color diff --git a/Gemfile b/Gemfile index 6fa2b49..fb7bcfb 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,7 @@ source "https://rubygems.org" gemspec + +group :development do + gem "pry-byebug" +end diff --git a/lib/mediawiki_selenium.rb b/lib/mediawiki_selenium.rb index 612105f..2b59a4a 100644 --- a/lib/mediawiki_selenium.rb +++ b/lib/mediawiki_selenium.rb @@ -9,22 +9,9 @@ https://git.wikimedia.org/blob/mediawiki%2Fselenium/HEAD/CREDITS. =end -require "mediawiki_selenium/version" - -require "mediawiki_selenium/support/env" -require "mediawiki_selenium/support/hooks" -require "mediawiki_selenium/support/sauce" - -require "mediawiki_selenium/step_definitions/login_steps" -require "mediawiki_selenium/step_definitions/navigation_steps" -require "mediawiki_selenium/step_definitions/preferences_steps" -require "mediawiki_selenium/step_definitions/resource_loader_steps" -require "mediawiki_selenium/step_definitions/upload_file_steps" - -require "mediawiki_selenium/support/modules/api_helper" -require "mediawiki_selenium/support/modules/url_module" - -require "mediawiki_selenium/support/pages/api_page" -require "mediawiki_selenium/support/pages/login_page" -require "mediawiki_selenium/support/pages/random_page" -require "mediawiki_selenium/support/pages/reset_preferences_page" +module MediawikiSelenium + autoload :VERSION, "mediawiki_selenium/version" + autoload :ApiHelper, "mediawiki_selenium/support/modules/api_helper" + autoload :BrowserFactory, "mediawiki_selenium/browser_factory" + autoload :Environment, "mediawiki_selenium/environment" +end diff --git a/lib/mediawiki_selenium/browser_factory.rb b/lib/mediawiki_selenium/browser_factory.rb new file mode 100644 index 0000000..cc58f58 --- /dev/null +++ b/lib/mediawiki_selenium/browser_factory.rb @@ -0,0 +1,22 @@ +module MediawikiSelenium + # Browser factory. + # + module BrowserFactory + autoload :Base, "mediawiki_selenium/browser_factory/base" + autoload :Firefox, "mediawiki_selenium/browser_factory/firefox" + autoload :Chrome, "mediawiki_selenium/browser_factory/chrome" + autoload :Phantomjs, "mediawiki_selenium/browser_factory/phantomjs" + + # Resolves a new factory for the given browser name. + # + # @param name [Symbol] Browser name. + # @param options [Hash] Browser options. + # + # @return [BrowserFactory::Base] + # + def self.new(name, options) + factory_class = const_get(name.to_s.split('_').map(&:capitalize).join('')) + factory_class.new(name, options) + end + end +end diff --git a/lib/mediawiki_selenium/browser_factory/base.rb b/lib/mediawiki_selenium/browser_factory/base.rb new file mode 100644 index 0000000..b6e321c --- /dev/null +++ b/lib/mediawiki_selenium/browser_factory/base.rb @@ -0,0 +1,46 @@ +require "watir-webdriver" + +module MediawikiSelenium + module BrowserFactory + class Base + attr_reader :name + + def initialize(name, options) + @name = name + @options = options + end + + def browser + Watir::Browser.new(name, options) + end + + def capabilities + {} + end + + def desired_capabilities + if Selenium::WebDriver::Remote::Capabilities.respond_to?(@name) + Selenium::WebDriver::Remote::Capabilities.send(@name, capabilities) + end + end + + def options + { http_client: http_client, desired_capabilities: desired_capabilities } + end + + def http_client + Selenium::WebDriver::Remote::Http::Default.new.tap do |client| + client.timeout = @options[:browser_timeout] + end + end + + protected + + def bind(option) + unless @options[option].nil? || @options[option].to_s.empty? + yield @options[option] + end + end + end + end +end diff --git a/lib/mediawiki_selenium/browser_factory/chrome.rb b/lib/mediawiki_selenium/browser_factory/chrome.rb new file mode 100644 index 0000000..b81429a --- /dev/null +++ b/lib/mediawiki_selenium/browser_factory/chrome.rb @@ -0,0 +1,28 @@ +module MediawikiSelenium + module BrowserFactory + class Chrome < Base + def arguments + [].tap do |args| + bind(:browser_user_agent) do |user_agent| + args << "--user-agent=#{user_agent}" + end + end + end + + def capabilities + super.merge( + "chrome.profile" => profile.as_json["zip"], + "chromeOptions" => { "args" => arguments }, + ) + end + + def profile + Selenium::WebDriver::Chrome::Profile.new.tap do |profile| + bind(:browser_language) do |language| + profile["intl.accept_languages"] = language + end + end + end + end + end +end diff --git a/lib/mediawiki_selenium/browser_factory/firefox.rb b/lib/mediawiki_selenium/browser_factory/firefox.rb new file mode 100644 index 0000000..c1d5a90 --- /dev/null +++ b/lib/mediawiki_selenium/browser_factory/firefox.rb @@ -0,0 +1,26 @@ +module MediawikiSelenium + module BrowserFactory + class Firefox < Base + def capabilities + super.merge(firefox_profile: profile) + end + + def profile + Selenium::WebDriver::Firefox::Profile.new.tap do |profile| + bind(:browser_timeout) do |timeout| + profile["dom.max_script_run_time"] = timeout + profile["dom.max_chrome_script_run_time"] = timeout + end + + bind(:browser_language) do |language| + profile["intl.accept_languages"] = language + end + + bind(:browser_user_agent) do |user_agent| + profile["general.useragent.override"] = user_agent + end + end + end + end + end +end diff --git a/lib/mediawiki_selenium/browser_factory/phantomjs.rb b/lib/mediawiki_selenium/browser_factory/phantomjs.rb new file mode 100644 index 0000000..5a8309b --- /dev/null +++ b/lib/mediawiki_selenium/browser_factory/phantomjs.rb @@ -0,0 +1,17 @@ +module MediawikiSelenium + module BrowserFactory + class Phantomjs < Base + def capabilities + super.tap do |capabilities| + bind(:browser_language) do |language| + capabilities["phantomjs.page.customHeaders.Accept-Language"] = language + end + + bind(:browser_user_agent) do |user_agent| + capabilities["phantomjs.page.settings.userAgent"] = user_agent + end + end + end + end + end +end diff --git a/lib/mediawiki_selenium/environment.rb b/lib/mediawiki_selenium/environment.rb new file mode 100644 index 0000000..325b5d6 --- /dev/null +++ b/lib/mediawiki_selenium/environment.rb @@ -0,0 +1,172 @@ +module MediawikiSelenium + # Provides an abstraction layer between the environmental configuration and + # step definitions. + # + class Environment + include Comparable + + class ConfigurationError < StandardError + attr_reader :name + + def initialize(name) + @name = name + end + + def to_s + "missing configuration for #{name}" + end + end + + BROWSER_OPTIONS = [ + :browser, + :browser_language, + :browser_timeout, + :browser_user_agent, + :mediawiki_url, + :mediawiki_user, + ] + + REQUIRED_CONFIG = [ + :browser, + :mediawiki_api_url, + :mediawiki_password, + :mediawiki_url, + :mediawiki_user, + ] + + attr_reader :config + + def initialize(config, browsers = {}) + @config = normalize_config(config) + @browsers = browsers + end + + # Two environments are considered equal if they have identical + # configuration. + # + def ==(other) + @config == other.config + end + + # Executes the given block within the context of an environment that's + # using the given alternative user and its password. + # + # @param id [Symbol] Alternative user ID. + # + # @yield [env] + # @yieldparam env [Environment] Environment + # + # @return [Environment] + # + def as_user(id, &blk) + with_alternative([:mediawiki_user, :mediawiki_password], id, &blk) + end + + # Browser with which to drive tests. + # + # @return [Watir::Browser] + # + def browser + options = browser_options + @browsers[options] ||= BrowserFactory.new(browser_name, options).browser + end + + # Name of the browser we're using. + # + # @return [Symbol] + # + def browser_name + lookup(:browser).downcase.to_sym + end + + # Options to use when instantiating a new browser. + # + # @return [Hash] + # + def browser_options + @browser_options ||= lookup_all(BROWSER_OPTIONS) + end + + # Executes the given block within the context of an environment that's + # using the given alternative wiki URL and its corresponding API endpoint. + # + # @param id [Symbol] Alternative wiki ID. + # + # @yield [env] + # @yieldparam env [Environment] Environment + # + # @return [Environment] + # + def on_wiki(id, &blk) + with_alternative([:mediawiki_url, :mediawiki_api_url], id, &blk) + end + + # Yields a new environment using the alternative versions of the given + # configuration options. The alternative values are resolved by looking up + # options that correspond to the given ones but have the given ID + # appended. + # + # @example Overwrite :option with the value from :option_b + # # given an environment with { option: "x", option_b: "y", ... } + # env.with_alternative(:option, :b) do |env| + # env # => { option: "y", ... } + # end + # + # @example Overwrite both :option and :other with :option_b and :other_b + # # given { option: "x", option_b: "y", other: "w", other_b: "z" } + # env.with_alternative([:option, :other], :b) do |env| + # env # => { option: "y", other: "z", ... } + # end + # + # @param names [Array|Symbol] Configuration option or options. + # @param id [Symbol] Alternative user ID. + # + # @yield [env] + # @yieldparam env [Environment] The modified environment. + # + # @return [Environment] The modified environment. + # + def with_alternative(names, id, &blk) + with(lookup_all(Array(names), id), &blk) + end + + private + + def lookup_all(keys, id = nil) + keys.each.with_object({}) do |key, hash| + value = lookup(key, id) + hash[key] = value unless value.nil? + end + end + + def lookup(key, id = nil) + key = "#{key}_#{id}" unless id.nil? + key = key.to_sym + value = @config[key] + + if value.nil? || value.to_s.empty? + if id.nil? + if REQUIRED_CONFIG.include?(key) + raise ConfigurationError, key + else + nil + end + else + lookup(key) + end + else + value + end + end + + def normalize_config(hash) + hash.each.with_object({}) { |(k, v), acc| acc[k.to_s.downcase.to_sym] = v } + end + + def with(overrides = {}) + config = self.class.new(@config.merge(normalize_config(overrides)), @browsers) + yield config + config + end + end +end diff --git a/lib/mediawiki_selenium/step_definitions.rb b/lib/mediawiki_selenium/step_definitions.rb new file mode 100644 index 0000000..b8608f6 --- /dev/null +++ b/lib/mediawiki_selenium/step_definitions.rb @@ -0,0 +1,5 @@ +require "mediawiki_selenium/step_definitions/login_steps" +require "mediawiki_selenium/step_definitions/navigation_steps" +require "mediawiki_selenium/step_definitions/preferences_steps" +require "mediawiki_selenium/step_definitions/resource_loader_steps" +require "mediawiki_selenium/step_definitions/upload_file_steps" diff --git a/lib/mediawiki_selenium/support.rb b/lib/mediawiki_selenium/support.rb new file mode 100644 index 0000000..f5a6cb6 --- /dev/null +++ b/lib/mediawiki_selenium/support.rb @@ -0,0 +1,3 @@ +require "mediawiki_selenium/support/env" +require "mediawiki_selenium/support/hooks" +require "mediawiki_selenium/support/sauce" diff --git a/lib/mediawiki_selenium/support/env.rb b/lib/mediawiki_selenium/support/env.rb index bb7f751..418b24f 100644 --- a/lib/mediawiki_selenium/support/env.rb +++ b/lib/mediawiki_selenium/support/env.rb @@ -11,7 +11,6 @@ # before all require "bundler/setup" -require "page-object" require "page-object/page_factory" require "rest_client" require "watir-webdriver" @@ -21,6 +20,8 @@ World(PageObject::PageFactory) World(MediawikiSelenium::ApiHelper) +World { MediawikiSelenium::Environment.new(ENV) } + def browser(test_name, configuration = nil) if environment == :saucelabs sauce_browser(test_name, configuration) diff --git a/lib/mediawiki_selenium/support/pages.rb b/lib/mediawiki_selenium/support/pages.rb new file mode 100644 index 0000000..f316bcf --- /dev/null +++ b/lib/mediawiki_selenium/support/pages.rb @@ -0,0 +1,4 @@ +require "mediawiki_selenium/support/pages/api_page" +require "mediawiki_selenium/support/pages/login_page" +require "mediawiki_selenium/support/pages/random_page" +require "mediawiki_selenium/support/pages/reset_preferences_page" diff --git a/lib/mediawiki_selenium/support/pages/api_page.rb b/lib/mediawiki_selenium/support/pages/api_page.rb index ba2ef39..53da9ee 100644 --- a/lib/mediawiki_selenium/support/pages/api_page.rb +++ b/lib/mediawiki_selenium/support/pages/api_page.rb @@ -1,3 +1,4 @@ +require "page-object" require "mediawiki_api" class APIPage diff --git a/lib/mediawiki_selenium/support/pages/login_page.rb b/lib/mediawiki_selenium/support/pages/login_page.rb index 89857bf..44fbf78 100644 --- a/lib/mediawiki_selenium/support/pages/login_page.rb +++ b/lib/mediawiki_selenium/support/pages/login_page.rb @@ -8,6 +8,8 @@ mediawiki_selenium top-level directory and at https://git.wikimedia.org/blob/mediawiki%2Fselenium/HEAD/CREDITS. =end +require "page-object" +require "mediawiki_selenium/support/modules/url_module" class LoginPage include PageObject diff --git a/lib/mediawiki_selenium/support/pages/random_page.rb b/lib/mediawiki_selenium/support/pages/random_page.rb index c181ea3..40e33cd 100644 --- a/lib/mediawiki_selenium/support/pages/random_page.rb +++ b/lib/mediawiki_selenium/support/pages/random_page.rb @@ -8,6 +8,8 @@ mediawiki_selenium top-level directory and at https://git.wikimedia.org/blob/mediawiki%2Fselenium/HEAD/CREDITS. =end +require "page-object" +require "mediawiki_selenium/support/modules/url_module" class RandomPage include PageObject diff --git a/lib/mediawiki_selenium/support/pages/reset_preferences_page.rb b/lib/mediawiki_selenium/support/pages/reset_preferences_page.rb index 255c772..76f2281 100644 --- a/lib/mediawiki_selenium/support/pages/reset_preferences_page.rb +++ b/lib/mediawiki_selenium/support/pages/reset_preferences_page.rb @@ -8,6 +8,8 @@ mediawiki_selenium top-level directory and at https://git.wikimedia.org/blob/mediawiki%2Fselenium/HEAD/CREDITS. =end +require "page-object" +require "mediawiki_selenium/support/modules/url_module" class ResetPreferencesPage include PageObject diff --git a/mediawiki_selenium.gemspec b/mediawiki_selenium.gemspec index 2028545..0979d1a 100644 --- a/mediawiki_selenium.gemspec +++ b/mediawiki_selenium.gemspec @@ -25,4 +25,8 @@ spec.add_runtime_dependency "rest-client", "~> 1.6", ">= 1.6.7" spec.add_runtime_dependency "rspec-expectations", "~> 2.14", ">= 2.14.4" spec.add_runtime_dependency "syntax", "~> 1.2", ">= 1.2.0" + + spec.add_development_dependency "bundler", "~> 1.6", ">= 1.6.3" + spec.add_development_dependency "rspec-core", "~> 2.14", ">= 2.14.4" + spec.add_development_dependency "rspec-mocks", "~> 2.14", ">= 2.14.4" end diff --git a/spec/environment_spec.rb b/spec/environment_spec.rb new file mode 100644 index 0000000..2094c33 --- /dev/null +++ b/spec/environment_spec.rb @@ -0,0 +1,209 @@ +require "spec_helper" + +module MediawikiSelenium + describe Environment do + subject { env } + + let(:env) { Environment.new(config) } + let(:config) { minimum_config } + + let(:minimum_config) do + { + browser: browser, + mediawiki_api_url: mediawiki_api_url, + mediawiki_url: mediawiki_url, + mediawiki_user: mediawiki_user, + mediawiki_password: mediawiki_password, + } + end + + let(:full_config) do + minimum_config.merge({ + browser_language: browser_language, + browser_timeout: browser_timeout, + browser_user_agent: browser_user_agent, + }) + end + + let(:browser) { "firefox" } + let(:mediawiki_api_url) { "http://an.example/wiki/api.php" } + let(:mediawiki_url) { "http://an.example/wiki/" } + let(:mediawiki_user) { "mw user" } + let(:mediawiki_password) { "mw password" } + + let(:browser_language) { "en-US" } + let(:browser_timeout) { 60 } + let(:browser_user_agent) { "Lynx/1.0" } + + describe "#==" do + subject { env == other } + + context "given an environment with the same configuration" do + let(:other) { Environment.new(env.config) } + + it "considers them equal" do + expect(subject).to be(true) + end + end + + context "given an environment with different configuration" do + let(:other) { Environment.new(env.config.merge(some: "extra")) } + + it "considers them not equal" do + expect(subject).to be(false) + end + end + end + + describe "#as_user" do + let(:config) do + { + mediawiki_user: "user", + mediawiki_password: "pass", + mediawiki_user_b: "user b", + mediawiki_password_b: "pass b", + } + end + + it "yields a new environment for the alternative user and its password" do + expected_env = Environment.new(config.merge( + mediawiki_user: "user b", + mediawiki_password: "pass b", + )) + expect { |block| env.as_user(:b, &block) }.to yield_with_args(expected_env) + end + end + + describe "#browser" do + subject { env.browser } + + it "should create a browser through the right factory" do + factory = double(BrowserFactory::Firefox) + + expect(BrowserFactory).to receive(:new). + with(env.browser_name, env.browser_options). + and_return(factory) + expect(factory).to receive(:browser) + + subject + end + + context "where the options are the same as before" do + it "should return a cached browser instance" do + factory = double(BrowserFactory::Firefox) + browser = double(Watir::Browser) + + expect(BrowserFactory).to receive(:new).once. + with(env.browser_name, env.browser_options). + and_return(factory) + expect(factory).to receive(:browser).and_return(browser) + + browsers = 2.times.collect { env.browser } + expect(browsers.first).to be(browsers.last) + end + end + end + + describe "#browser_name" do + subject { env.browser_name } + + let(:browser) { "Firefox" } + + it "is always a lowercase symbol" do + expect(subject).to be(:firefox) + end + + context "missing browser configuration" do + let(:browser) { nil } + + it "raises a ConfigurationError" do + expect { subject }.to raise_error(Environment::ConfigurationError) + end + end + end + + describe "#browser_options" do + subject { env.browser_options } + + context "minimum configuration" do + let(:config) { minimum_config } + + it "contains just :browser, :mediawiki_url, and :mediawiki_user" do + expect(subject.keys).to match_array([:browser, :mediawiki_url, :mediawiki_user]) + end + end + + context "full configuration" do + let(:config) { full_config } + + it "also contains :browser_language, :browser_timeout, and :browser_user_agent" do + expect(subject.keys).to match_array([ + :browser, :mediawiki_url, :mediawiki_user, + :browser_language, :browser_timeout, :browser_user_agent + ]) + end + end + + context "missing some required configuration (e.g. :browser)" do + let(:browser) { nil } + + it "raises a ConfigurationError" do + expect { subject }.to raise_error(Environment::ConfigurationError) + end + end + end + + describe "#on_wiki" do + let(:config) do + { + mediawiki_url: "http://an.example/wiki", + mediawiki_url_b: "http://alt.example/wiki", + mediawiki_api_url: "http://an.example/api", + mediawiki_api_url_b: "http://alt.example/api", + } + end + + it "yields a new environment for the alternative wiki and API urls" do + expected_env = Environment.new(config.merge( + mediawiki_url: "http://alt.example/wiki", + mediawiki_api_url: "http://alt.example/api", + )) + expect { |block| env.on_wiki(:b, &block) }.to yield_with_args(expected_env) + end + end + + describe "#with_alternative" do + let(:config) do + { + mediawiki_url: "http://an.example/wiki", + mediawiki_url_b: "http://alt.example/wiki", + mediawiki_api_url: "http://an.example/api", + mediawiki_api_url_b: "http://alt.example/api", + } + end + + context "given one option name and an ID" do + let(:names) { :mediawiki_url } + + it "yields an environment that substitutes it using the alternative" do + expected_env = Environment.new(config.merge( + mediawiki_url: "http://alt.example/wiki", + )) + expect { |block| env.with_alternative(names, :b, &block) }.to yield_with_args(expected_env) + end + end + + context "given multiple option names and an ID" do + let(:names) { [:mediawiki_url, :mediawiki_api_url] } + + it "yields an environment that substitutes both using the alternatives" do + expected_env = Environment.new(config.merge( + mediawiki_url: "http://alt.example/wiki", + mediawiki_api_url: "http://alt.example/api", + )) + expect { |block| env.with_alternative(names, :b, &block) }.to yield_with_args(expected_env) + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..f92911d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,4 @@ +require "bundler/setup" +require "mediawiki_selenium" + +Bundler.require(:development) -- To view, visit https://gerrit.wikimedia.org/r/159644 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I1a3aa64b8fdca73aff2778ec12143a8789e4843f Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/selenium Gerrit-Branch: master Gerrit-Owner: Dduvall <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
