rsel 0.0.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.
- data/.gitignore +4 -0
- data/.yardopts +7 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +22 -0
- data/README.md +35 -0
- data/Rakefile +75 -0
- data/docs/custom.md +87 -0
- data/docs/development.md +48 -0
- data/docs/index.md +13 -0
- data/docs/install.md +33 -0
- data/docs/todo.md +15 -0
- data/docs/usage.md +50 -0
- data/lib/rsel.rb +4 -0
- data/lib/rsel/exceptions.rb +4 -0
- data/lib/rsel/selenium_test.rb +524 -0
- data/rsel.gemspec +29 -0
- data/spec/selenium_test_spec.rb +296 -0
- data/spec/spec_helper.rb +41 -0
- data/test/app.rb +34 -0
- data/test/config.ru +4 -0
- data/test/views/about.erb +9 -0
- data/test/views/form.erb +102 -0
- data/test/views/index.erb +15 -0
- data/vendor/rubyslim-0.1.1/.gitignore +7 -0
- data/vendor/rubyslim-0.1.1/README.md +159 -0
- data/vendor/rubyslim-0.1.1/Rakefile +21 -0
- data/vendor/rubyslim-0.1.1/bin/rubyslim +10 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/list_deserializer.rb +69 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/list_executor.rb +12 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/list_serializer.rb +45 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/ruby_slim.rb +61 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/slim_error.rb +3 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/slim_helper_library.rb +23 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/socket_service.rb +53 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/statement.rb +79 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/statement_executor.rb +174 -0
- data/vendor/rubyslim-0.1.1/lib/rubyslim/table_to_hash_converter.rb +32 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/library_new.rb +13 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/library_old.rb +12 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/should_not_find_test_slim_in_here/test_slim.rb +7 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/simple_script.rb +9 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/test_chain.rb +5 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/test_slim.rb +86 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/test_slim_with_arguments.rb +23 -0
- data/vendor/rubyslim-0.1.1/lib/test_module/test_slim_with_no_sut.rb +5 -0
- data/vendor/rubyslim-0.1.1/rubyslim.gemspec +22 -0
- data/vendor/rubyslim-0.1.1/spec/instance_creation_spec.rb +40 -0
- data/vendor/rubyslim-0.1.1/spec/it8f_spec.rb +8 -0
- data/vendor/rubyslim-0.1.1/spec/list_deserializer_spec.rb +66 -0
- data/vendor/rubyslim-0.1.1/spec/list_executor_spec.rb +187 -0
- data/vendor/rubyslim-0.1.1/spec/list_serialzer_spec.rb +38 -0
- data/vendor/rubyslim-0.1.1/spec/method_invocation_spec.rb +127 -0
- data/vendor/rubyslim-0.1.1/spec/slim_helper_library_spec.rb +80 -0
- data/vendor/rubyslim-0.1.1/spec/socket_service_spec.rb +98 -0
- data/vendor/rubyslim-0.1.1/spec/spec_helper.rb +6 -0
- data/vendor/rubyslim-0.1.1/spec/statement_executor_spec.rb +50 -0
- data/vendor/rubyslim-0.1.1/spec/statement_spec.rb +17 -0
- data/vendor/rubyslim-0.1.1/spec/table_to_hash_converter_spec.rb +31 -0
- metadata +241 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 Automation Excellence
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
Rsel
|
2
|
+
====
|
3
|
+
|
4
|
+
Rsel connects [FitNesse](http://fitnesse.org) to [Selenium](http://seleniumhq.org)
|
5
|
+
via [Ruby](http://ruby-lang.org).
|
6
|
+
|
7
|
+
[Full documentation is on rdoc.info](http://rdoc.info/github/a-e/rsel/master/frames).
|
8
|
+
|
9
|
+
|
10
|
+
Copyright
|
11
|
+
---------
|
12
|
+
|
13
|
+
The MIT License
|
14
|
+
|
15
|
+
Copyright (c) 2011 Automation Excellence
|
16
|
+
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
18
|
+
a copy of this software and associated documentation files (the
|
19
|
+
"Software"), to deal in the Software without restriction, including
|
20
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
21
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
22
|
+
permit persons to whom the Software is furnished to do so, subject to
|
23
|
+
the following conditions:
|
24
|
+
|
25
|
+
The above copyright notice and this permission notice shall be
|
26
|
+
included in all copies or substantial portions of the Software.
|
27
|
+
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
29
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
30
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
31
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
32
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
33
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
34
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
35
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'net/http'
|
3
|
+
require 'selenium/rake/tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
ROOT_DIR = File.expand_path(File.dirname(__FILE__))
|
7
|
+
TEST_APP = File.join(ROOT_DIR, 'test', 'app.rb')
|
8
|
+
SELENIUM_RC_JAR = File.join(ROOT_DIR, 'test', 'server', 'selenium-server-1.0.3-SNAPSHOT-standalone.jar')
|
9
|
+
SELENIUM_RC_LOG = File.join(ROOT_DIR, 'selenium-rc.log')
|
10
|
+
|
11
|
+
namespace 'testapp' do
|
12
|
+
desc "Start the test webapp in the background"
|
13
|
+
task :start do
|
14
|
+
puts "Starting the test webapp: #{TEST_APP}"
|
15
|
+
system("ruby #{TEST_APP} &")
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Stop the test webapp"
|
19
|
+
task :stop do
|
20
|
+
puts "Stopping the test webapp"
|
21
|
+
begin
|
22
|
+
Net::HTTP.get('localhost', '/shutdown', 8070)
|
23
|
+
rescue EOFError
|
24
|
+
# This is expected
|
25
|
+
puts "Test webapp stopped."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Selenium::Rake::RemoteControlStartTask.new do |rc|
|
31
|
+
rc.jar_file = SELENIUM_RC_JAR
|
32
|
+
rc.port = 4444
|
33
|
+
rc.background = true
|
34
|
+
rc.timeout_in_seconds = 60
|
35
|
+
rc.wait_until_up_and_running = true
|
36
|
+
rc.log_to = SELENIUM_RC_LOG
|
37
|
+
end
|
38
|
+
|
39
|
+
Selenium::Rake::RemoteControlStopTask.new do |rc|
|
40
|
+
rc.host = 'localhost'
|
41
|
+
rc.port = 4444
|
42
|
+
rc.timeout_in_seconds = 60
|
43
|
+
rc.wait_until_stopped = true
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Run spec tests"
|
47
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
48
|
+
t.pattern = 'spec/**/*_spec.rb'
|
49
|
+
t.rspec_opts = ['--color', '--format doc']
|
50
|
+
end
|
51
|
+
|
52
|
+
namespace 'servers' do
|
53
|
+
desc "Start the Selenium and testapp servers"
|
54
|
+
task :start do
|
55
|
+
Rake::Task['testapp:start'].invoke
|
56
|
+
Rake::Task['selenium:rc:start'].invoke
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Stop the Selenium and testapp servers"
|
60
|
+
task :stop do
|
61
|
+
Rake::Task['selenium:rc:stop'].invoke
|
62
|
+
Rake::Task['testapp:stop'].invoke
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Start Selenium and testapp servers, run tests, then stop servers"
|
67
|
+
task :test do
|
68
|
+
begin
|
69
|
+
Rake::Task['servers:start'].invoke
|
70
|
+
Rake::Task['spec'].invoke
|
71
|
+
ensure
|
72
|
+
Rake::Task['servers:stop'].invoke
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
data/docs/custom.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
Customization
|
2
|
+
-------------
|
3
|
+
|
4
|
+
Rsel provides only the most basic imperative navigational steps and
|
5
|
+
verification. For real-world testing, you will most likely want to define your
|
6
|
+
own steps, built up from the low-level ones provided by Rsel.
|
7
|
+
|
8
|
+
It's pretty easy to do this by subclassing `SeleniumTest` and adding your own
|
9
|
+
methods to it. Create a sibling directory to your `FitNesseRoot`, named
|
10
|
+
something like `custom_rsel`, then create a Ruby file in there. The Ruby file
|
11
|
+
can be called whatever you want--it's most logical to name it after your
|
12
|
+
application:
|
13
|
+
|
14
|
+
- `FitNesseRoot`
|
15
|
+
- `custom_rsel`
|
16
|
+
- `my_app_test.rb`
|
17
|
+
|
18
|
+
Inside `my_app_test.rb`, you must define a module named after the folder, and a
|
19
|
+
class named after the file. You'll also need to include some `require` statements
|
20
|
+
to make sure `rsel/selenium` gets loaded. In this case, it should be:
|
21
|
+
|
22
|
+
require `rubygems` # If you installed Rsel from a gem
|
23
|
+
require `rsel/selenium_test` # Makes the SeleniumTest class available
|
24
|
+
|
25
|
+
module CustomRsel
|
26
|
+
class MyAppTest < Rsel::SeleniumTest
|
27
|
+
# Custom methods go here
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Inside your custom class, you can define any methods you want, and you can call
|
32
|
+
any of the methods defined in the `Rsel::SeleniumTest` class. For example,
|
33
|
+
let's say the login process for your application consists of three steps:
|
34
|
+
|
35
|
+
| Fill in | Username | with | admin |
|
36
|
+
| Fill in | Password | with | letmein |
|
37
|
+
| Press | Login |
|
38
|
+
|
39
|
+
If you're logging in a lot, you may want a single-step login method. Here's how
|
40
|
+
you might define one:
|
41
|
+
|
42
|
+
module CustomRsel
|
43
|
+
class MyAppTest < Rsel::SeleniumTest
|
44
|
+
|
45
|
+
# Login with the given username and password
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
# | Login as | admin | with password | letmein |
|
49
|
+
#
|
50
|
+
def login_as_with_password(username, password)
|
51
|
+
fill_in_with("Username", username)
|
52
|
+
fill_in_with("Password", password)
|
53
|
+
press("Login")
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
In your FitNesse `SetUp` page, rather than (or in addition to) importing the `Rsel` module,
|
60
|
+
import your custom module:
|
61
|
+
|
62
|
+
!| import |
|
63
|
+
| CustomRsel |
|
64
|
+
|
65
|
+
Note that this name must match the `module` line in your Ruby file, and the
|
66
|
+
folder where your Ruby file resides must be the lowercase_and_underscore
|
67
|
+
version of that same module name. Finally, in your actual test table, instead of:
|
68
|
+
|
69
|
+
!| script | selenium test | ... |
|
70
|
+
|
71
|
+
You'll use:
|
72
|
+
|
73
|
+
!| script | my app test | ... |
|
74
|
+
|
75
|
+
This will ensure that the `MyAppTest` class will be used for evaluating the
|
76
|
+
steps contained in that table.
|
77
|
+
|
78
|
+
It's worth noting that the `SeleniumTest` class (and hence any derived classes)
|
79
|
+
have a `@browser` attribute referring to the underlying `Selenium::Client::Driver`
|
80
|
+
instance, which includes
|
81
|
+
[a wealth of methods](http://rdoc.info/github/ph7/selenium-client/master/Selenium/Client/Driver)
|
82
|
+
for interacting with webpages and performing verifications. If `SeleniumTest`
|
83
|
+
itself does not provide the methods you need, you can use the `@browser`
|
84
|
+
attribute directly.
|
85
|
+
|
86
|
+
Next: [Development](development.md)
|
87
|
+
|
data/docs/development.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
Development
|
2
|
+
-----------
|
3
|
+
|
4
|
+
If you would like to contribute to development of Rsel, create a fork of the
|
5
|
+
[Github repository](https://github.com/a-e/rsel), clone your fork, and submit
|
6
|
+
pull requests for any improvements you would like to share.
|
7
|
+
|
8
|
+
While developing, you should run the RSpec tests frequently to ensure nothing
|
9
|
+
gets broken. You can do this with a single `rake` command:
|
10
|
+
|
11
|
+
$ rake test
|
12
|
+
|
13
|
+
This will do the following:
|
14
|
+
|
15
|
+
- Start the Sinatra test application (code in `test/app.rb` and `test/views/*.erb`)
|
16
|
+
- Start the Selenium server (the `.jar` file in `test/server`)
|
17
|
+
- Run RSpec
|
18
|
+
- Stop the Selenium server
|
19
|
+
- Stop the Sinatra test application
|
20
|
+
|
21
|
+
Since there's a large startup cost associated with all of this (in particular
|
22
|
+
the Selenium server), when running frequent tests you may want to start those
|
23
|
+
up and keep them running. There are rake tasks for that also:
|
24
|
+
|
25
|
+
$ rake servers:start
|
26
|
+
|
27
|
+
Now do:
|
28
|
+
|
29
|
+
$ rake spec
|
30
|
+
|
31
|
+
as many times as you want. When you're done, manually stop the servers:
|
32
|
+
|
33
|
+
$ rake servers:stop
|
34
|
+
|
35
|
+
You can also start the Selenium and Sinatra testapp servers individually:
|
36
|
+
|
37
|
+
$ rake selenium:rc:start
|
38
|
+
$ rake testapp:start
|
39
|
+
|
40
|
+
and stop them:
|
41
|
+
|
42
|
+
$ rake selenium:rc:stop
|
43
|
+
$ rake testapp:stop
|
44
|
+
|
45
|
+
If you are developing new features, please add spec tests in the `spec`
|
46
|
+
directory!
|
47
|
+
|
48
|
+
Next: [To-do list](todo.md)
|
data/docs/index.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Rsel
|
2
|
+
====
|
3
|
+
|
4
|
+
Rsel connects [FitNesse](http://fitnesse.org) to
|
5
|
+
[Selenium](http://seleniumhq.org) via [Ruby](http://ruby-lang.org). It allows
|
6
|
+
you to use a natural English syntax to write web application tests.
|
7
|
+
|
8
|
+
- [Installation & Configuration](install.md)
|
9
|
+
- [Usage](usage.md)
|
10
|
+
- [Customization](custom.md)
|
11
|
+
- [Development](development.md)
|
12
|
+
- [To-do list](todo.md)
|
13
|
+
|
data/docs/install.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Installation & Configuration
|
2
|
+
----------------------------
|
3
|
+
|
4
|
+
To install Rsel from a gem:
|
5
|
+
|
6
|
+
$ gem install rsel
|
7
|
+
|
8
|
+
If you have the following test hierarchy:
|
9
|
+
|
10
|
+
- `FitNesseRoot`
|
11
|
+
- `SeleniumTests`
|
12
|
+
- `SetUp`
|
13
|
+
- `LoginTest`
|
14
|
+
|
15
|
+
You should define the requisite `rubyslim` options in the `SeleniumTests` page content:
|
16
|
+
|
17
|
+
!define TEST_SYSTEM {slim}
|
18
|
+
!define TEST_RUNNER {rubyslim}
|
19
|
+
!define COMMAND_PATTERN {rubyslim}
|
20
|
+
|
21
|
+
If you're using Bundler, you may need to use:
|
22
|
+
|
23
|
+
!define TEST_SYSTEM {slim}
|
24
|
+
!define TEST_RUNNER {bundle exec rubyslim}
|
25
|
+
!define COMMAND_PATTERN {bundle exec rubyslim}
|
26
|
+
|
27
|
+
Finally, put this in your `SeleniumTests.SetUp` page:
|
28
|
+
|
29
|
+
!| import |
|
30
|
+
| Rsel |
|
31
|
+
|
32
|
+
Next: [Usage](usage.md)
|
33
|
+
|
data/docs/todo.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
To-do list
|
2
|
+
----------
|
3
|
+
|
4
|
+
- Pass better error messages back to FitNesse. It seems that Slim script tables
|
5
|
+
only support true/false results, with no clear way to report on what went
|
6
|
+
wrong (aside from raising an exception, which even then curiously does not
|
7
|
+
mark the step as failed)
|
8
|
+
- Find a way to abort a test if something goes catastrophically wrong (such as
|
9
|
+
being unable to connect to the Selenium server)
|
10
|
+
|
11
|
+
Some additional step methods needed:
|
12
|
+
|
13
|
+
- dropdown_includes
|
14
|
+
- dropdown_equals
|
15
|
+
|
data/docs/usage.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
Usage
|
2
|
+
-----
|
3
|
+
|
4
|
+
Once you have created your `SetUp` page, you can create sibling pages with
|
5
|
+
tests in them. For instance, continuing with the example from
|
6
|
+
[Installation & Configuration](install.md), your `SeleniumTests.LoginTest`
|
7
|
+
might look like this:
|
8
|
+
|
9
|
+
!| script | selenium test | http://www.mysite.com |
|
10
|
+
| Open browser |
|
11
|
+
| Fill in | Username | with | castle |
|
12
|
+
| Fill in | Password | with | beckett |
|
13
|
+
| Click button | Log in |
|
14
|
+
| Page loads in | 5 | seconds or less |
|
15
|
+
| See | Logged in as castle |
|
16
|
+
| Close browser |
|
17
|
+
|
18
|
+
Before running a test, you must make sure you have Selenium Server installed and running.
|
19
|
+
Download [selenium-server-standalone-x.x.x.jar](http://seleniumhq.org/download/), and start
|
20
|
+
it up like this:
|
21
|
+
|
22
|
+
$ java -jar selenium-server-standalone-x.x.x.jar
|
23
|
+
|
24
|
+
By default, the server runs on port 4444, and this is the port that Rsel uses
|
25
|
+
unless you tell it otherwise. Rsel also assumes that you're running
|
26
|
+
selenium-server on your localhost (that is, the same host where FitNesse is
|
27
|
+
running); if you need to use a different host or port number, pass those as
|
28
|
+
arguments to the first line of the table. For example, if you are running
|
29
|
+
selenium-server on `my.selenium.host`, port `4455`, do this:
|
30
|
+
|
31
|
+
!| script | selenium test | http://www.mysite.com | my.selenium.host | 4455 |
|
32
|
+
|
33
|
+
The first argument after `selenium test` is the URL of the site you will be testing.
|
34
|
+
This URL is loaded when you call `Open browser`, and all steps that follow are
|
35
|
+
assumed to stay within the same domain. You can navigate around the site by
|
36
|
+
clicking links and filling in forms just as a human user would; you can also go
|
37
|
+
directly to a specific path within the domain with the `Visit` method:
|
38
|
+
|
39
|
+
| Visit | /some/path |
|
40
|
+
| Visit | /some/other/path |
|
41
|
+
|
42
|
+
These paths are evaluated relative to the domain your test is running in. (It's
|
43
|
+
theoretically possible to navigate to a different domain, but the Selenium
|
44
|
+
driver frowns upon it.)
|
45
|
+
|
46
|
+
See the `SeleniumTest` class documentation for a full list of available methods
|
47
|
+
and how to use them.
|
48
|
+
|
49
|
+
Next: [Customization](custom.md)
|
50
|
+
|
data/lib/rsel.rb
ADDED
@@ -0,0 +1,524 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'exceptions')
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'xpath'
|
5
|
+
require 'selenium/client'
|
6
|
+
|
7
|
+
module Rsel
|
8
|
+
|
9
|
+
# Main Selenium-test class.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# !| script | selenium test | http://www.example.com/ |
|
13
|
+
#
|
14
|
+
# NOTE: Function names beginning with these words are forbidden:
|
15
|
+
#
|
16
|
+
# - check
|
17
|
+
# - ensure
|
18
|
+
# - reject
|
19
|
+
# - note
|
20
|
+
# - show
|
21
|
+
# - start
|
22
|
+
#
|
23
|
+
# This is because the above words are keywords in Slim script tables; if
|
24
|
+
# the first cell of a script table begins with any of these words, Slim tries
|
25
|
+
# to apply special handling to them, which usually doesn't do what you want.
|
26
|
+
#
|
27
|
+
class SeleniumTest
|
28
|
+
|
29
|
+
# Initialize a test, connecting to the given Selenium server.
|
30
|
+
#
|
31
|
+
# @param [String] url
|
32
|
+
# Full URL, including http://, of the system under test
|
33
|
+
# @param [String] host
|
34
|
+
# IP address or hostname where selenium-server is running
|
35
|
+
# @param [String] port
|
36
|
+
# Port number of selenium-server
|
37
|
+
# @param [String] browser
|
38
|
+
# Which browser to test with
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# | script | selenium test | http://site.to.test/ |
|
42
|
+
# | script | selenium test | http://site.to.test/ | 192.168.0.3 | 4445 |
|
43
|
+
#
|
44
|
+
def initialize(url, host='localhost', port='4444', browser='*firefox')
|
45
|
+
@url = url
|
46
|
+
@browser = Selenium::Client::Driver.new(
|
47
|
+
:host => host,
|
48
|
+
:port => port,
|
49
|
+
:browser => browser,
|
50
|
+
:url => url)
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :url, :browser
|
54
|
+
|
55
|
+
|
56
|
+
# Start the session and open a browser to the URL defined at the start of
|
57
|
+
# the test.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# | Open browser |
|
61
|
+
#
|
62
|
+
def open_browser
|
63
|
+
begin
|
64
|
+
@browser.start_new_browser_session
|
65
|
+
rescue
|
66
|
+
# TODO: Find a way to make the test abort here
|
67
|
+
raise SeleniumNotRunning, "Could not start Selenium."
|
68
|
+
else
|
69
|
+
visit @url
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Stop the session and close the browser window.
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# | Close browser |
|
78
|
+
#
|
79
|
+
def close_browser
|
80
|
+
@browser.close_current_browser_session
|
81
|
+
return true
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Load an absolute URL or a relative path in the browser.
|
86
|
+
#
|
87
|
+
# @param [String] path_or_url
|
88
|
+
# Relative path or absolute URL to load. Absolute URLs must be in the
|
89
|
+
# same domain as the URL that was passed to {#initialize}.
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# | Visit | http://www.automation-excellence.com |
|
93
|
+
# | Visit | /software |
|
94
|
+
#
|
95
|
+
def visit(path_or_url)
|
96
|
+
return_error_status do
|
97
|
+
@browser.open(path_or_url)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Click the Back button to navigate to the previous page.
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# | Click back |
|
106
|
+
#
|
107
|
+
def click_back
|
108
|
+
return_error_status do
|
109
|
+
@browser.go_back
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Reload the current page.
|
115
|
+
#
|
116
|
+
# @example
|
117
|
+
# | refresh page |
|
118
|
+
#
|
119
|
+
def refresh_page
|
120
|
+
return_error_status do
|
121
|
+
@browser.refresh
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# Maximize the browser window. May not work in some browsers.
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# | maximize browser |
|
130
|
+
#
|
131
|
+
def maximize_browser
|
132
|
+
@browser.window_maximize
|
133
|
+
return true
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Ensure that the given text appears on the current page.
|
138
|
+
#
|
139
|
+
# @param [String] text
|
140
|
+
# Plain text that should be visible on the current page
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# | See | Welcome, Marcus |
|
144
|
+
#
|
145
|
+
def see(text)
|
146
|
+
return @browser.text?(text)
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# Ensure that the given text does not appear on the current page.
|
151
|
+
#
|
152
|
+
# @param [String] text
|
153
|
+
# Plain text that should not be visible on the current page
|
154
|
+
#
|
155
|
+
# @example
|
156
|
+
# | Do not see | Take a hike |
|
157
|
+
#
|
158
|
+
def do_not_see(text)
|
159
|
+
return !@browser.text?(text)
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# Ensure that the current page has the given title text.
|
164
|
+
#
|
165
|
+
# @param [String] title
|
166
|
+
# Text of the page title that you expect to see
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
# | See title | Our Homepage |
|
170
|
+
#
|
171
|
+
def see_title(title)
|
172
|
+
return (@browser.get_title == title)
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
# Ensure that the current page does not have the given title text.
|
177
|
+
#
|
178
|
+
# @param [String] title
|
179
|
+
# Text of the page title that you should not see
|
180
|
+
#
|
181
|
+
# @example
|
182
|
+
# | Do not see title | Someone else's homepage |
|
183
|
+
#
|
184
|
+
def do_not_see_title(title)
|
185
|
+
return !(@browser.get_title == title)
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Type a value into the given field.
|
190
|
+
#
|
191
|
+
# @param [String] text
|
192
|
+
# What to type into the field
|
193
|
+
# @param [String] locator
|
194
|
+
# Label, name, or id of the field you want to type into
|
195
|
+
#
|
196
|
+
# @example
|
197
|
+
# | Type | Dale | into field | First name |
|
198
|
+
# | Type | Dale | into | First name | field |
|
199
|
+
#
|
200
|
+
def type_into_field(text, locator)
|
201
|
+
return_error_status do
|
202
|
+
@browser.type(xpath('field', locator), text)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# Fill in a field with the given text.
|
208
|
+
#
|
209
|
+
# @param [String] locator
|
210
|
+
# Label, name, or id of the field you want to type into
|
211
|
+
# @param [String] text
|
212
|
+
# What to type into the field
|
213
|
+
#
|
214
|
+
# @example
|
215
|
+
# | Fill in | First name | with | Eric |
|
216
|
+
#
|
217
|
+
def fill_in_with(locator, text)
|
218
|
+
type_into_field(text, locator)
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
# Verify that a text field contains the given text.
|
223
|
+
# The field may include additional text, as long as the
|
224
|
+
# expected value is in there somewhere.
|
225
|
+
#
|
226
|
+
# @param [String] locator
|
227
|
+
# Label, name, or id of the field you want to inspect
|
228
|
+
# @param [String] text
|
229
|
+
# Text to expect in the field
|
230
|
+
#
|
231
|
+
# @example
|
232
|
+
# | Field | First name | contains | Eric |
|
233
|
+
#
|
234
|
+
def field_contains(locator, text)
|
235
|
+
@browser.field(xpath('field', locator)).include?(text)
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
# Verify that a text field's value equals the given text.
|
240
|
+
# The value must match exactly.
|
241
|
+
#
|
242
|
+
# @param [String] locator
|
243
|
+
# Label, name, or id of the field you want to inspect
|
244
|
+
# @param [String] text
|
245
|
+
# Text to expect in the field
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# | Field | First name | equals | Eric |
|
249
|
+
#
|
250
|
+
def field_equals(locator, text)
|
251
|
+
@browser.field(xpath('field', locator)) == text
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
# Click on a link or button, and wait for a page to load.
|
256
|
+
#
|
257
|
+
# @param [String] locator
|
258
|
+
# Text, value or id of the link or button to click
|
259
|
+
#
|
260
|
+
# @example
|
261
|
+
# | Click | Next |
|
262
|
+
# | Click | Logout |
|
263
|
+
#
|
264
|
+
def click(locator)
|
265
|
+
return_error_status do
|
266
|
+
@browser.click(xpath('link_or_button', locator))
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
# Click on a link.
|
272
|
+
#
|
273
|
+
# @param [String] locator
|
274
|
+
# Link text or id of the anchor element
|
275
|
+
#
|
276
|
+
# @example
|
277
|
+
# | Click | Logout | link |
|
278
|
+
# | Click link | Logout |
|
279
|
+
#
|
280
|
+
def click_link(locator)
|
281
|
+
return_error_status do
|
282
|
+
@browser.click(xpath('link', locator))
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
# Alias for {#click_link}.
|
288
|
+
#
|
289
|
+
# @example
|
290
|
+
# | Follow | Logout |
|
291
|
+
#
|
292
|
+
def follow(locator)
|
293
|
+
click_link(locator)
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
# Press a button.
|
298
|
+
#
|
299
|
+
# @param [String] locator
|
300
|
+
# Button text, value, or id of the button
|
301
|
+
#
|
302
|
+
# @example
|
303
|
+
# | Click | Login | button |
|
304
|
+
# | Click button | Login |
|
305
|
+
#
|
306
|
+
def click_button(locator)
|
307
|
+
return_error_status do
|
308
|
+
@browser.click(xpath('button', locator))
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
# Alias for {#click_button}
|
314
|
+
#
|
315
|
+
# @example
|
316
|
+
# | Press | Search |
|
317
|
+
#
|
318
|
+
def press(locator)
|
319
|
+
click_button(locator)
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
# Enable (check) a checkbox.
|
324
|
+
#
|
325
|
+
# @param [String] locator
|
326
|
+
# Label, value, or id of the checkbox to check
|
327
|
+
#
|
328
|
+
# @example
|
329
|
+
# | Enable | Send me spam | checkbox |
|
330
|
+
# | Enable checkbox | Send me spam |
|
331
|
+
#
|
332
|
+
def enable_checkbox(locator)
|
333
|
+
return_error_status do
|
334
|
+
@browser.check(xpath('checkbox', locator))
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
# Disable (uncheck) a checkbox.
|
340
|
+
#
|
341
|
+
# @param [String] locator
|
342
|
+
# Label, value, or id of the checkbox to uncheck
|
343
|
+
#
|
344
|
+
# @example
|
345
|
+
# | Disable | Send me spam | checkbox |
|
346
|
+
# | Disable checkbox | Send me spam |
|
347
|
+
#
|
348
|
+
def disable_checkbox(locator)
|
349
|
+
return_error_status do
|
350
|
+
@browser.uncheck(xpath('checkbox', locator))
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
|
355
|
+
# Verify that a given checkbox or radiobutton is enabled (checked)
|
356
|
+
#
|
357
|
+
# @param [String] locator
|
358
|
+
# Label, value, or id of the checkbox to inspect
|
359
|
+
#
|
360
|
+
# @example
|
361
|
+
# | Checkbox is enabled | send me spam |
|
362
|
+
# | Checkbox | send me spam | is enabled |
|
363
|
+
#
|
364
|
+
def checkbox_is_enabled(locator)
|
365
|
+
begin
|
366
|
+
enabled = @browser.checked?(xpath('checkbox', locator))
|
367
|
+
rescue
|
368
|
+
return false
|
369
|
+
else
|
370
|
+
return enabled
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
|
375
|
+
# Verify that a given checkbox or radiobutton is disabled (unchecked)
|
376
|
+
#
|
377
|
+
# @param [String] locator
|
378
|
+
# Label, value, or id of the checkbox to inspect
|
379
|
+
#
|
380
|
+
# @example
|
381
|
+
# | Checkbox is disabled | send me spam |
|
382
|
+
#
|
383
|
+
def checkbox_is_disabled(locator)
|
384
|
+
begin
|
385
|
+
enabled = @browser.checked?(xpath('checkbox', locator))
|
386
|
+
rescue
|
387
|
+
return false
|
388
|
+
else
|
389
|
+
return !enabled
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
|
394
|
+
# Select a radio button.
|
395
|
+
#
|
396
|
+
# @param [String] locator
|
397
|
+
# Label, id, or name of the radio button to select
|
398
|
+
#
|
399
|
+
# @example
|
400
|
+
# | Select | female | radio |
|
401
|
+
# | Select radio | female |
|
402
|
+
#
|
403
|
+
def select_radio(locator)
|
404
|
+
return_error_status do
|
405
|
+
@browser.click(xpath('radio_button', locator))
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
|
410
|
+
# Alias for {#checkbox_is_disabled}
|
411
|
+
#
|
412
|
+
# @example
|
413
|
+
# | Radio is disabled | medium |
|
414
|
+
# | Radio | medium | is disabled |
|
415
|
+
#
|
416
|
+
def radio_is_disabled(locator)
|
417
|
+
checkbox_is_disabled(locator)
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
# Alias for {#checkbox_is_enabled}
|
422
|
+
#
|
423
|
+
# @example
|
424
|
+
# | Radio is enabled | medium |
|
425
|
+
# | Radio | medium | is enabled |
|
426
|
+
#
|
427
|
+
def radio_is_enabled(locator)
|
428
|
+
checkbox_is_enabled(locator)
|
429
|
+
end
|
430
|
+
|
431
|
+
|
432
|
+
# Select a value from a dropdown/combo box.
|
433
|
+
#
|
434
|
+
# @param [String] value
|
435
|
+
# The value to choose from the dropdown
|
436
|
+
# @param [String] locator
|
437
|
+
# Label, name, or id of the dropdown
|
438
|
+
#
|
439
|
+
# @example
|
440
|
+
# | select | Tall | from | Height | dropdown |
|
441
|
+
# | select | Tall | from dropdown | Height |
|
442
|
+
#
|
443
|
+
def select_from_dropdown(value, locator)
|
444
|
+
return_error_status do
|
445
|
+
@browser.select(xpath('select', locator), value)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
|
450
|
+
# Pause for a certain number of seconds.
|
451
|
+
#
|
452
|
+
# @param [String, Int] seconds
|
453
|
+
# How many seconds to pause
|
454
|
+
#
|
455
|
+
# @example
|
456
|
+
# | Pause | 5 | seconds |
|
457
|
+
#
|
458
|
+
def pause_seconds(seconds)
|
459
|
+
sleep seconds.to_i
|
460
|
+
return true
|
461
|
+
end
|
462
|
+
|
463
|
+
|
464
|
+
# Wait some number of seconds for the current page request to finish.
|
465
|
+
# Fails if the page does not finish loading within the specified time.
|
466
|
+
#
|
467
|
+
# @param [String, Int] seconds
|
468
|
+
# How long to wait for before timing out
|
469
|
+
#
|
470
|
+
# @example
|
471
|
+
# | Page loads in | 10 | seconds or less |
|
472
|
+
#
|
473
|
+
def page_loads_in_seconds_or_less(seconds)
|
474
|
+
return_error_status do
|
475
|
+
@browser.wait_for_page_to_load("#{seconds}000")
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
|
480
|
+
# Execute the given block, and return false if it raises an exception.
|
481
|
+
# Otherwise, return true.
|
482
|
+
#
|
483
|
+
# @example
|
484
|
+
# return_error_status do
|
485
|
+
# # some code that might raise an exception
|
486
|
+
# end
|
487
|
+
#
|
488
|
+
def return_error_status
|
489
|
+
begin
|
490
|
+
yield
|
491
|
+
rescue => e
|
492
|
+
#puts e.message
|
493
|
+
#puts e.backtrace
|
494
|
+
return false
|
495
|
+
else
|
496
|
+
return true
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
# Return a Selenium-style xpath for the given locator.
|
501
|
+
#
|
502
|
+
# @param [String] kind
|
503
|
+
# What kind of locator you're using (link, button, checkbox, field etc.).
|
504
|
+
# This must correspond to a method name in `XPath::HTML`.
|
505
|
+
# @param [String] locator
|
506
|
+
# Name, id, value, label or whatever locator the `XPath::HTML.<kind>`
|
507
|
+
# method accepts.
|
508
|
+
#
|
509
|
+
# @example
|
510
|
+
# xpath('link', 'Log in')
|
511
|
+
# xpath('button', 'Submit')
|
512
|
+
# xpath('field', 'First name')
|
513
|
+
#
|
514
|
+
def xpath(kind, locator)
|
515
|
+
# Doing explicit string-join here, so it'll work with older versions
|
516
|
+
# of the XPath module that don't have `Union#to_s` defined.
|
517
|
+
"xpath=" + XPath::HTML.send(kind, locator).to_xpaths.join(' | ')
|
518
|
+
end
|
519
|
+
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
|
524
|
+
|