Class: Protractor

Inherits:
Object
  • Object
show all
Defined in:
lib/angular_webdriver/protractor/protractor.rb

Constant Summary collapse

NEW_FINDERS_KEYS =
%i(
  binding
  exactBinding
  partialButtonText
  buttonText
  model
  options
  cssContainingText
  repeater
).freeze
NEW_FINDERS_HASH =
:binding
NEW_FINDERS_KEYS.map { |e| [e, e.to_s] }.to_h.freeze
ABOUT_BLANK =

Reset URL used on IE & Safari since they don't work well with data URLs

'about:blank'.freeze
DEFAULT_RESET_URL =

Reset URL used by non-IE/Safari browsers

'data:text/html,<html></html>'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Protractor

Creates a new protractor instance and dynamically patches the provided driver.

Parameters:

  • opts (Hash) (defaults to: {})

    the options to initialize with

Options Hash (opts):

  • :watir (Watir::Browser)

    the watir instance used for automation

  • :root_element (String)

    the root element on which to find Angular

  • :ignore_sync (Boolean)

    if true, Protractor won't auto sync the page


243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/angular_webdriver/protractor/protractor.rb', line 243

def initialize opts={}
  @watir = opts[:watir]

  valid_watir = defined?(Watir::Browser) && @watir.is_a?(Watir::Browser)
  fail ArgumentError, "Driver must be a Watir::Browser not #{@driver.class}" unless valid_watir
  @driver = @watir.driver

  unless Selenium::WebDriver::SearchContext::FINDERS.keys.include?(NEW_FINDERS_KEYS)
    Selenium::WebDriver::SearchContext::FINDERS.merge!(NEW_FINDERS_HASH)
  end

  unless Watir::ElementLocator::WD_FINDERS.include? NEW_FINDERS_KEYS
    old = Watir::ElementLocator::WD_FINDERS
    # avoid const redefinition warning
    Watir::ElementLocator.send :remove_const, :WD_FINDERS
    Watir::ElementLocator.send :const_set, :WD_FINDERS, old + NEW_FINDERS_KEYS
  end

  @driver.protractor = self

  # The css selector for an element on which to find Angular. This is usually
  # 'body' but if your ng-app is on a subsection of the page it may be
  # a subelement.
  #
  # @return [String]
  #
  @root_element      = opts.fetch :root_element, 'body'

  # If true, Protractor will not attempt to synchronize with the page before
  # performing actions. This can be harmful because Protractor will not wait
  # until $timeouts and $http calls have been processed, which can cause
  # tests to become flaky. This should be used only when necessary, such as
  # when a page continuously polls an API using $timeout.
  #
  # @return [Boolean]
  #
  @ignore_sync       = !!opts.fetch(:ignore_sync, false)

  @client_side_scripts = ClientSideScripts

  browser_name = driver.capabilities[:browser_name].to_s.strip
  @reset_url   = reset_url_for_browser browser_name

  @base_url          = opts.fetch(:base_url, nil)

  # must be local var for use with define element below.
  protractor_element = AngularWebdriver::ProtractorElement.new @watir
  driver             = @driver

  # Top level element method to enable protractor syntax.
  # redefine element to point to the new protractor element instance.
  #
  # toplevel self enables by/element from within pry. rspec helpers enables
  # by/element within rspec tests when used with install_rspec_helpers.
  toplevel_main      = TOPLEVEL_BINDING.eval('self')
  [toplevel_main, ::AngularWebdriver::RSpecHelpers].each do |obj|
    method_type = :define_singleton_method

    obj.send method_type, :element do |*args|
      protractor_element.element *args
    end

    obj.send method_type, :by do
      AngularWebdriver::By
    end

    obj.send method_type, :no_wait do |&block|
      max_wait      = driver.max_wait_seconds
      max_page_wait = driver.max_page_wait_seconds

      driver.set_max_wait 0
      driver.set_max_page_wait 0

      begin
        raise ArgumentError, 'Tried to use no_wait without a block' unless block
        result = block.call
      ensure
        driver.set_max_wait max_wait
        driver.set_max_page_wait max_page_wait
      end

      result
    end
  end

  self
end

Instance Attribute Details

#base_urlString

File.join(base_url, destination) when using driver.get and protractor.get (if sync is on, base_url is set, and destination is not absolute).

Returns:

  • (String)

    Default nil.


52
53
54
# File 'lib/angular_webdriver/protractor/protractor.rb', line 52

def base_url
  @base_url
end

#client_side_scriptsObject (readonly)

All scripts to be run on the client via executeAsyncScript or

executeScript should be put here.

NOTE: These scripts are transmitted over the wire as JavaScript text
constructed using their toString representation, and# cannot*
reference external variables.

Some implementations seem to have issues with // comments, so use star-style
inside scripts. that caused the switch to avoid the // comments.)

88
89
90
# File 'lib/angular_webdriver/protractor/protractor.rb', line 88

def client_side_scripts
  @client_side_scripts
end

#driverObject (readonly)

The Selenium::WebDriver driver object


91
92
93
# File 'lib/angular_webdriver/protractor/protractor.rb', line 91

def driver
  @driver
end

#ignore_syncBoolean

If true, Protractor will not attempt to synchronize with the page before performing actions. This can be harmful because Protractor will not wait until $timeouts and $http calls have been processed, which can cause tests to become flaky. This should be used only when necessary, such as when a page continuously polls an API using $timeout.

Returns:

  • (Boolean)

44
45
46
# File 'lib/angular_webdriver/protractor/protractor.rb', line 44

def ignore_sync
  @ignore_sync
end

#reset_urlObject (readonly)

URL to a blank page. Differs depending on the browser.


96
97
98
# File 'lib/angular_webdriver/protractor/protractor.rb', line 96

def reset_url
  @reset_url
end

#root_elementString

The css selector for an element on which to find Angular. This is usually 'body' but if your ng-app is on a subsection of the page it may be a subelement.

Returns:

  • (String)

34
35
36
# File 'lib/angular_webdriver/protractor/protractor.rb', line 34

def root_element
  @root_element
end

Instance Method Details

#_js_comment(description) ⇒ Object

Ensure description is exactly one line that ends in a newline must use /* */ not // due to some browsers having problems with // comments when used with execute script


422
423
424
425
# File 'lib/angular_webdriver/protractor/protractor.rb', line 422

def _js_comment description
  description = description ? '/* ' + description.gsub(/\s+/, ' ').strip + ' */' : ''
  description.strip + "\n"
end

#allowAnimations(web_element, value = nil) ⇒ Boolean

Determine if animation is allowed on the current underlying elements. // Turns off ng-animate animations for all elements in the <body> element(by.css('body')).allowAnimations(false);

Parameters:

  • web_element (Element)
    • the web element to act upon

  • value (Boolean) (defaults to: nil)
    • turn on/off ng-animate animations.

Returns:

  • (Boolean)

    whether animation is allowed.


487
488
489
# File 'lib/angular_webdriver/protractor/protractor.rb', line 487

def allowAnimations web_element, value=nil
  executeScript_ client_side_scripts.allow_animations, 'Protractor.allow_animations()', web_element, value
end

#debuggerObject

Injects client side scripts into window.clientSideScripts for debugging.

Example:

“`ruby # inject the scripts protractor.debugger

# now that the scripts are injected, they can be used via execute_script driver.execute_script “window.clientSideScripts.getLocationAbsUrl('body')” “`

This should be used under Pry. The window client side scripts can be invoked using chrome dev tools after calling debugger.


474
475
476
# File 'lib/angular_webdriver/protractor/protractor.rb', line 474

def debugger
  executeScript_ client_side_scripts.install_in_browser, 'Protractor.debugger()'
end

#driver_get(url) ⇒ Object

Invokes the underlying driver.get. Does not wait for angular. Does not use base_url or reset_url logic.


176
177
178
# File 'lib/angular_webdriver/protractor/protractor.rb', line 176

def driver_get url
  driver.bridge.driver_get url
end

#evaluate(element, expression) ⇒ Object

Evaluate an Angular expression as if it were on the scope of the given element.

Parameters:

  • element (Element)

    The element in whose scope to evaluate.

  • expression (String)

    The expression to evaluate.

Returns:

  • (Object)

    The result of the evaluation.


498
499
500
501
# File 'lib/angular_webdriver/protractor/protractor.rb', line 498

def evaluate element, expression
  # angular.element(element).scope().$eval(expression);
  executeScript_ client_side_scripts.evaluate, 'Protractor.evaluate()', element, expression
end

#executeAsyncScript_(script, description, *args) ⇒ Object

The same as webdriver.WebDriver.prototype.executeAsyncScript, but with a customized description for debugging.

@private
@param script [String] The javascript to execute.
@param description [String]  A description of the command for debugging.
@param args [var_args] The arguments to pass to the script.
@return The scripts return value.

436
437
438
439
440
441
# File 'lib/angular_webdriver/protractor/protractor.rb', line 436

def executeAsyncScript_ script, description, *args
  # add description as comment to script so it shows up in server logs
  script = _js_comment(description) + script

  driver.execute_async_script script, *args
end

#executeScript_(script, description, *args) ⇒ Object

The same as webdriver.WebDriver.prototype.executeScript,

but with a customized description for debugging.

@private
@param script [String] The javascript to execute.
@param description [String]  A description of the command for debugging.
@param args [var_args] The arguments to pass to the script.
@return The scripts return value.

452
453
454
455
456
457
# File 'lib/angular_webdriver/protractor/protractor.rb', line 452

def executeScript_ script, description, *args
  # add description as comment to script so it shows up in server logs
  script = _js_comment(description) + script

  driver.execute_script script, *args
end

#finder?(finder_name) ⇒ boolean

Return true if given finder is a protractor finder.

Parameters:

  • finder_name (Symbol|String)

    the name of the finder

Returns:

  • (boolean)

22
23
24
# File 'lib/angular_webdriver/protractor/protractor.rb', line 22

def finder? finder_name
  NEW_FINDERS_KEYS.include? finder_name.intern
end

#get(destination, opt_timeout = driver.max_page_wait_seconds) ⇒ Object

@see webdriver.WebDriver.get

Navigate to the given destination. Assumes that the page being loaded uses Angular.
If you need to access a page which does not have Angular on load,
use driver_get.

@example
browser.get('https://angularjs.org/');
expect(browser.getCurrentUrl()).toBe('https://angularjs.org/');

Parameters:

  • destination (String)

    The destination URL to load, can be relative if base_url is set

  • opt_timeout (Integer) (defaults to: driver.max_page_wait_seconds)

    Number of seconds to wait for Angular to start. Default 30


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/angular_webdriver/protractor/protractor.rb', line 112

def get destination, opt_timeout=driver.max_page_wait_seconds
  # do not use driver.get because that redirects to this method
  # instead driver_get is provided.

  timeout = opt_timeout

  fail ArgumentError, "Invalid timeout #{timeout}" unless timeout.is_a?(Numeric)

  unless destination.is_a?(String) || destination.is_a?(URI)
    fail ArgumentError, "Invalid destination #{destination}"
  end

  # http://about:blank doesn't work. it must be exactly about:blank
  about_url = destination.start_with?('about:')

  # data urls must be preserved and not have http:// prepended.
  # data:<blah>
  data_url  = destination.start_with?('data:')

  unless about_url || data_url
    # URI.join doesn't allow for http://localhost:8081/#/ as a base_url
    # so this departs from the Protractor behavior and favors File.join instead.
    #
    # In protractor: url.resolve('http://localhost:8081/#/', 'async')
    #                => http://localhost:8081/async
    # In Ruby:       File.join('http://localhost:8081/#/', 'async')
    #                => http://localhost:8081/#/async
    #
    base_url_exists = base_url && !base_url.empty?
    tmp_uri = URI.parse(destination) rescue URI.parse('')
    relative_url = !tmp_uri.scheme || !tmp_uri.host

    if base_url_exists && relative_url
      destination = File.join(base_url, destination.to_s)
    elsif relative_url # prepend 'http://' to urls such as localhost
      destination = "http://#{destination}"
    end
  end

  msg = lambda { |str| 'Protractor.get(' + destination + ') - ' + str }

  return driver_get(destination) if ignore_sync

  driver_get(reset_url)
  executeScript_(
    'window.location.replace("' + destination + '");',
    msg.call('reset url'))

  wait(timeout) do
    url                  = executeScript_('return window.location.href;', msg.call('get url'))
    not_on_reset_url     = url != reset_url
    destination_is_reset = destination == reset_url
    fail 'still on reset url' unless not_on_reset_url || destination_is_reset
  end

  # now that the url has changed, make sure Angular has loaded
  # note that the mock module logic is omitted.
  #
  waitForAngular description: 'Protractor.get', timeout: timeout
end

#getLocationAbsUrlObject

Returns the current absolute url from AngularJS.

Waits for Angular.

@example
browser.get('http://angular.github.io/protractor/#/api');
expect(browser.getLocationAbsUrl())
    .toBe('/api');

229
230
231
232
233
# File 'lib/angular_webdriver/protractor/protractor.rb', line 229

def getLocationAbsUrl
  waitForAngular
  executeScript_(client_side_scripts.get_location_abs_url,
                 'Protractor.getLocationAbsUrl()', root_element)
end

#refresh(opt_timeout = 10) ⇒ Object

@see webdriver.WebDriver.refresh

Makes a full reload of the current page. Assumes that the page being loaded uses Angular.
If you need to access a page which does not have Angular on load, use
driver_get.

@param opt_timeout [Integer] Number of seconds to wait for Angular to start.

188
189
190
191
192
193
194
195
196
197
# File 'lib/angular_webdriver/protractor/protractor.rb', line 188

def refresh opt_timeout=10
  timeout = opt_timeout

  return driver.navigate.refresh if ignore_sync

  href = executeScript_('return window.location.href;',
                        'Protractor.refresh() - getUrl')

  get(href, timeout)
end

#reset_url_for_browser(browser_name) ⇒ Object

IE and Safari require about:blank because they don't work well with data urls (flaky). For other browsers, data urls are stable.

browser_name [String] the browser name from driver caps. Must be 'safari'

or 'internet explorer'

344
345
346
347
348
349
350
# File 'lib/angular_webdriver/protractor/protractor.rb', line 344

def reset_url_for_browser browser_name
  if ['internet explorer', 'safari'].include?(browser_name)
    ABOUT_BLANK
  else
    DEFAULT_RESET_URL
  end
end

#setLocation(url) ⇒ Object

Browse to another page using in-page navigation.

Assumes that the page being loaded uses Angular.

@example
browser.get('http://angular.github.io/protractor/#/tutorial');
browser.setLocation('api');
expect(browser.getCurrentUrl())
    .toBe('http://angular.github.io/protractor/#/api');

@param url [String] In page URL using the same syntax as $location.url()

210
211
212
213
214
215
216
217
218
219
# File 'lib/angular_webdriver/protractor/protractor.rb', line 210

def setLocation url
  waitForAngular

  begin
    executeScript_(client_side_scripts.set_location,
                   'Protractor.setLocation()', root_element, url)
  rescue Exception => e
    raise e.class, "Error while navigating to '#{url}' : #{e}"
  end
end

#sync(webdriver_command) ⇒ Object

Syncs the webdriver command if it's white listed

Parameters:

  • webdriver_command (Symbol)

    the webdriver command to check for syncing


358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/angular_webdriver/protractor/protractor.rb', line 358

def sync webdriver_command
  return unless webdriver_command
  webdriver_command = webdriver_command.intern
  # Note get must not sync here because the get command is redirected to
  # protractor.get which already has the sync logic built in.
  #
  # also don't sync set location (protractor custom command already waits
  # for angular). the selenium set location is for latitude/longitude/altitude
  # and that doesn't require syncing
  #
  sync_whitelist    = %i(
    getCurrentUrl
    refresh
    getPageSource
    getTitle
    findElement
    findElements
    findChildElement
    findChildElements
  )
  must_sync         = sync_whitelist.include? webdriver_command

  waitForAngular if must_sync
end

#waitForAngular(opts = {}) ⇒ WebDriver::Element, ...

Instruct webdriver to wait until Angular has finished rendering and has no outstanding $http or $timeout calls before continuing. Note that Protractor automatically applies this command before every WebDriver action.

Will wait up to driver.max_wait_seconds (set with driver.set_max_wait)

Parameters:

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :description (String)

    An optional description to be added to webdriver logs.

  • :timeout (Integer)

    Amount of time in seconds to wait for angular to load. Default driver.max_wait_seconds

Returns:

  • (WebDriver::Element, Integer, Float, Boolean, NilClass, String, Array)

397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/angular_webdriver/protractor/protractor.rb', line 397

def waitForAngular opts={} # Protractor.prototype.waitForAngular
  return if ignore_sync

  description = opts.fetch(:description, '')
  timeout     = opts.fetch(:timeout, driver.max_wait_seconds)

  wait(timeout: timeout, bubble: true) do
    begin
      # the client side script will return a string on error
      # the string won't be raised as an error unless we explicitly do so here
      error = executeAsyncScript_(client_side_scripts.wait_for_angular,
                                  "Protractor.waitForAngular() #{description}",
                                  root_element)
      raise Selenium::WebDriver::Error::JavascriptError, error if error
    rescue Exception => e
      # https://github.com/angular/protractor/blob/master/docs/faq.md
      raise e.class, "Error while waiting for Protractor to sync with the page: #{e}"
    end
  end
end