citronella 0.0.3 → 1.0.0
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/README.md +216 -1
- data/citronella.gemspec +3 -3
- data/lib/citronella.rb +23 -0
- data/lib/logger.rb +41 -0
- data/lib/page_decorator.rb +58 -0
- data/lib/ui.rb +31 -2
- data/lib/web_page.rb +104 -16
- data/lib/web_ui.rb +109 -0
- metadata +7 -7
- data/lib/page_store.rb +0 -17
- data/lib/page_wrapper.rb +0 -30
- data/lib/ui_object.rb +0 -43
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7ef1f78d05ad8a27657474af4a584e56c30921d217f863cd85fb06e163f358a
|
|
4
|
+
data.tar.gz: 3e8992f2bdec5de5479ed4b62ca2b42feabacfce598c736fb117a9f41ef79967
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9db842a70045cd3cd25592c9a18e27690aa3d22759b99abe7bc9823e16f49fe0cc1a5d45a3ab0c33381a41cb8728c3d844352ff8e9e520cdec64ea7b9f297889
|
|
7
|
+
data.tar.gz: c06316aef0e0343f3d3ee04598b481e047d5b0ef2c808750425e7cf3f76f766747247b00f2acbd11814f01c680228d48fa87b6551a549e197bd23b584d970b0e
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,221 @@
|
|
|
1
1
|
# Citronella
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](http://badge.fury.io/rb/citronella)
|
|
4
|
+
|
|
5
|
+
webdriver extension with a page object wrapper.
|
|
6
|
+
|
|
7
|
+
## Example Tests
|
|
8
|
+
```ruby
|
|
9
|
+
require 'test/unit'
|
|
10
|
+
require "selenium-webdriver"
|
|
11
|
+
require 'citronella'
|
|
12
|
+
require_relative '../pages/contents_page'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NavigationTest < Test::Unit::TestCase
|
|
16
|
+
def setup
|
|
17
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
|
18
|
+
driver = Selenium::WebDriver.for :chrome, options: options
|
|
19
|
+
@web = Citronella::Web::WebPage.new(driver)
|
|
20
|
+
@web.page = ContentsPage
|
|
21
|
+
@web.driver.navigate.to('https://rubygems.org')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def teardown
|
|
25
|
+
@web.driver.quit
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_navigation
|
|
29
|
+
@web.page.home_page.releases_button.click
|
|
30
|
+
assert_includes(@web.driver.title, 'Releases')
|
|
31
|
+
|
|
32
|
+
@web.page.release_page.gems_button.click
|
|
33
|
+
assert_includes(@web.driver.title, 'Gem')
|
|
34
|
+
|
|
35
|
+
@web.page.gems_page.sign_in_button.click
|
|
36
|
+
assert_includes(@web.driver.title, 'Sign in')
|
|
37
|
+
|
|
38
|
+
@web.page.sign_in_page.sign_up_button.click
|
|
39
|
+
assert_includes(@web.driver.title, 'Sign up')
|
|
40
|
+
|
|
41
|
+
@web.page.sign_up_page.guides_button.click
|
|
42
|
+
assert_includes(@web.driver.title, 'Guides')
|
|
43
|
+
|
|
44
|
+
@web.page.guides_page.blog_button.click
|
|
45
|
+
assert_includes(@web.driver.title, 'Blog')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
Even though this module is mainly designed for the page object model, it can also be used without it for quick prototyping or mockups, etc.
|
|
50
|
+
```ruby
|
|
51
|
+
require 'test/unit'
|
|
52
|
+
require "selenium-webdriver"
|
|
53
|
+
require 'citronella'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PackageSearchTest < Test::Unit::TestCase
|
|
57
|
+
def setup
|
|
58
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
|
59
|
+
driver = Selenium::WebDriver.for :chrome, options: options
|
|
60
|
+
@web = Citronella::Web::WebPage.new(driver)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def teardown
|
|
64
|
+
@web.driver.quit
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_search_package
|
|
68
|
+
@web.driver.navigate.to "https://rubygems.org/"
|
|
69
|
+
@web.locate(id: 'home_query').get_element.send_keys('citronella')
|
|
70
|
+
@web.locate(class: 'home__search__icon').get_element.click
|
|
71
|
+
assert(@web.locate(class: 'gems__gem__name').get_element.text, 'citronella')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
___
|
|
76
|
+
## Install Package
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gem install citronella
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
___
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
There are only two modules imported in this package:
|
|
86
|
+
|
|
87
|
+
* The first module is for the tests.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
require 'test/unit'
|
|
91
|
+
require "selenium-webdriver"
|
|
92
|
+
require 'citronella'
|
|
93
|
+
|
|
94
|
+
class NavigationTest < Test::Unit::TestCase
|
|
95
|
+
def setup
|
|
96
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
|
97
|
+
driver = Selenium::WebDriver.for :chrome, options: options
|
|
98
|
+
@web = Citronella::Web::WebPage.new(driver)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def teardown
|
|
102
|
+
@web.driver.quit
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
* The last module is for the page object model.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
require 'citronella'
|
|
111
|
+
require_relative '../components/header_menu'
|
|
112
|
+
|
|
113
|
+
class HomePage
|
|
114
|
+
include HeaderMenu
|
|
115
|
+
|
|
116
|
+
def search_button
|
|
117
|
+
ui(class: 'home__search__icon')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def code_link_button
|
|
121
|
+
ui(css: 'div.nav--v > a:nth-child(3)')
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
___
|
|
127
|
+
## Page Object Design / Strategy
|
|
128
|
+
see full [Page object](https://github.com/heyclore/citronella/tree/main/ruby/examples/page_object/pages) example
|
|
129
|
+
```ruby
|
|
130
|
+
require "selenium-webdriver"
|
|
131
|
+
require 'citronella'
|
|
132
|
+
|
|
133
|
+
module HeaderMenu
|
|
134
|
+
def home_logo
|
|
135
|
+
ui(class: 'header__logo')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def search_input
|
|
139
|
+
ui(id: 'home_query')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def gems_button
|
|
143
|
+
ui(css: 'a[href="/gems"]')
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class HomePage
|
|
148
|
+
include HeaderMenu
|
|
149
|
+
|
|
150
|
+
def search_button
|
|
151
|
+
ui(class: 'home__search__icon')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def code_link_button
|
|
155
|
+
ui(css: 'div.nav--v > a:nth-child(3)')
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class SearchPage
|
|
160
|
+
include HeaderMenu
|
|
161
|
+
|
|
162
|
+
def search_lists
|
|
163
|
+
ui(class: 'gems__gem__name')
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class ContentsPage
|
|
168
|
+
def home_page
|
|
169
|
+
HomePage
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def search_page
|
|
173
|
+
SearchPage
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
|
178
|
+
driver = Selenium::WebDriver.for :chrome, options: options
|
|
179
|
+
web = Citronella::Web::WebPage.new(driver)
|
|
180
|
+
web.page = ContentsPage
|
|
181
|
+
web.driver.navigate.to('https://rubygems.org')
|
|
182
|
+
web.page.home_page.search_input.send_keys('citronella', return_key=true)
|
|
183
|
+
puts web.page.search_page.search_list.get_element.text
|
|
184
|
+
```
|
|
185
|
+
___
|
|
186
|
+
## Usage
|
|
187
|
+
|
|
188
|
+
### citronella.WebPage
|
|
189
|
+
|
|
190
|
+
###### Args:
|
|
191
|
+
- driver / webdriver
|
|
192
|
+
|
|
193
|
+
###### Kwargs (optional):
|
|
194
|
+
- webdriver_wait `number(seconds)`, default value is `10`
|
|
195
|
+
- logger `bool`, default value is `true`
|
|
196
|
+
|
|
197
|
+
###### Method Lists:
|
|
198
|
+
| Method Name | Args* | Kwargs** | Note |
|
|
199
|
+
| ------------------ |:-----------:|:----------------:|:----:|
|
|
200
|
+
| driver | - | - | return selenium `webdriver` object |
|
|
201
|
+
| locate | - | how: what | similar as`driver.get_element` as input & return [citronella.WebUi](https://github.com/heyclore/citronella/tree/main/ruby#citronellaui--citronellawebui)|
|
|
202
|
+
| page | Page Object | - | setter |
|
|
203
|
+
| page | - | - | getter |
|
|
204
|
+
| webdriver_wait | number(sec) | - | |
|
|
205
|
+
| ready_state | number(sec) | - | execute javascript `document.readyState` manually |
|
|
206
|
+
|
|
207
|
+
### citronella.ui / citronella.WebUi
|
|
208
|
+
|
|
209
|
+
###### Kwargs:
|
|
210
|
+
- how: what
|
|
211
|
+
|
|
212
|
+
###### Method Lists:
|
|
213
|
+
| Method Name | Args* | Kwargs** | Note |
|
|
214
|
+
| ------------- |:------:|:------------------:|:----:|
|
|
215
|
+
| send_keys | text | clear `bool`, return_key `bool` | |
|
|
216
|
+
| click | - | - | |
|
|
217
|
+
| get_element | - | - | |
|
|
218
|
+
| get_elements | - | - | |
|
|
4
219
|
|
|
5
220
|
|
|
6
221
|
## Testing powered by
|
data/citronella.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'citronella'
|
|
3
|
-
s.version = '0.0
|
|
3
|
+
s.version = '1.0.0'
|
|
4
4
|
|
|
5
5
|
s.authors = ['heyclore']
|
|
6
6
|
s.email = 'cloore@gmail.com'
|
|
@@ -10,9 +10,9 @@ Gem::Specification.new do |s|
|
|
|
10
10
|
Webdriver extension with a page object wrapper
|
|
11
11
|
DESCRIPTION
|
|
12
12
|
|
|
13
|
-
s.licenses = '
|
|
13
|
+
s.licenses = 'MIT'
|
|
14
14
|
s.required_ruby_version = '>= 2.7.0'
|
|
15
|
-
s.homepage = 'https://github.com/heyclore/citronella#readme'
|
|
15
|
+
s.homepage = 'https://github.com/heyclore/citronella/tree/main/ruby#readme'
|
|
16
16
|
s.metadata = {
|
|
17
17
|
'source_code_uri' => 'https://github.com/heyclore/citronella/tree/main/ruby',
|
|
18
18
|
'github_repo' => 'https://github.com/heyclore/citronella',
|
data/lib/citronella.rb
CHANGED
|
@@ -1,2 +1,25 @@
|
|
|
1
|
+
#MIT License
|
|
2
|
+
#
|
|
3
|
+
#Copyright (c) 2023 Eko Purnomo
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
1
24
|
require_relative 'web_page'
|
|
2
25
|
require_relative 'ui'
|
data/lib/logger.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#MIT License
|
|
2
|
+
#
|
|
3
|
+
#Copyright (c) 2023 Eko Purnomo
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
require 'logger'
|
|
25
|
+
|
|
26
|
+
module Citronella
|
|
27
|
+
module Log
|
|
28
|
+
# A wrapper for UI methods.
|
|
29
|
+
#
|
|
30
|
+
# @param [Boolean] logger Flag indicating whether to log actions.
|
|
31
|
+
# @param [String] class_name The name of the class.
|
|
32
|
+
# @param [String] function_name The name of the function.
|
|
33
|
+
# @param [String] name The name of the action.
|
|
34
|
+
#
|
|
35
|
+
def self.logger(logger, class_name, function_name, name)
|
|
36
|
+
if logger
|
|
37
|
+
Logger.new(STDOUT, level: Logger::INFO).info("#{class_name} => #{function_name} => #{name}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#MIT License
|
|
2
|
+
#
|
|
3
|
+
#Copyright (c) 2023 Eko Purnomo
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
require_relative 'web_ui.rb'
|
|
25
|
+
|
|
26
|
+
module Citronella
|
|
27
|
+
module Wrapper
|
|
28
|
+
class PageDecorator
|
|
29
|
+
# A wrapper for page object class.
|
|
30
|
+
#
|
|
31
|
+
# @param [Webdriver] driver The web driver object.
|
|
32
|
+
# @param [Integer] webdriver_wait The timeout for webdriver wait.
|
|
33
|
+
# @param [PageObject] page The page object class.
|
|
34
|
+
# @param [Boolean] logger A flag indicating whether to log actions.
|
|
35
|
+
#
|
|
36
|
+
def initialize(driver, webdriver_wait, page, logger)
|
|
37
|
+
@driver = driver
|
|
38
|
+
@webdriver_wait = webdriver_wait
|
|
39
|
+
@page = page
|
|
40
|
+
@logger = logger
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Looks up the attribute/method name inside the page object.
|
|
44
|
+
#
|
|
45
|
+
# @return [PageDecorator, WebUi]
|
|
46
|
+
#
|
|
47
|
+
def method_missing(attr)
|
|
48
|
+
original_method = @page.new.method(attr)
|
|
49
|
+
args = original_method.call
|
|
50
|
+
if args.instance_of?(Class)
|
|
51
|
+
return PageDecorator.new(@driver, @webdriver_wait, args, @logger)
|
|
52
|
+
end
|
|
53
|
+
Citronella::Ui::WebUi.new(@driver, @webdriver_wait, @logger,
|
|
54
|
+
args, attr, @page.name)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/ui.rb
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
|
+
#MIT License
|
|
2
|
+
#
|
|
3
|
+
#Copyright (c) 2023 Eko Purnomo
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# A wrapper for driver.find_element or driver.find_elements.
|
|
25
|
+
#
|
|
26
|
+
# @overload ui(args)
|
|
27
|
+
# @param locator [Symbol] The locator type.
|
|
28
|
+
# @param value [String] The locator value.
|
|
29
|
+
# @return [Citronella::Ui::WebUi] The web element object.
|
|
30
|
+
#
|
|
1
31
|
def ui(args)
|
|
2
|
-
|
|
3
|
-
baz.new(args.delete(:page), args.delete(:exception), args)
|
|
32
|
+
args
|
|
4
33
|
end
|
data/lib/web_page.rb
CHANGED
|
@@ -1,24 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
#MIT License
|
|
2
|
+
#
|
|
3
|
+
#Copyright (c) 2023 Eko Purnomo
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
require_relative 'page_decorator'
|
|
25
|
+
require_relative 'web_ui'
|
|
3
26
|
|
|
4
27
|
module Citronella
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
28
|
+
module Web
|
|
29
|
+
# An object class that is used across the tests.
|
|
30
|
+
# `webdriver_wait` is set to '10' seconds by default.
|
|
31
|
+
# `logger` is set to 'true' by default.
|
|
32
|
+
#
|
|
33
|
+
# @param [Webdriver] driver The web driver object.
|
|
34
|
+
# @param [Integer] webdriver_wait The timeout for webdriver wait.
|
|
35
|
+
# @param [Boolean] logger A flag indicating whether to log actions.
|
|
36
|
+
#
|
|
37
|
+
# Usage:
|
|
38
|
+
# driver = Selenium::WebDriver.for :chrome
|
|
39
|
+
# web = WebPage.new(driver)
|
|
40
|
+
#
|
|
41
|
+
class WebPage
|
|
42
|
+
def initialize(driver, webdriver_wait:10, logger:true)
|
|
43
|
+
@driver = driver
|
|
44
|
+
@webdriver_wait = webdriver_wait
|
|
45
|
+
@page = nil
|
|
46
|
+
@logger = logger
|
|
47
|
+
end
|
|
11
48
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
49
|
+
# Returns the original Selenium driver.
|
|
50
|
+
#
|
|
51
|
+
# @return [Webdriver] The web driver object.
|
|
52
|
+
#
|
|
53
|
+
def driver
|
|
54
|
+
@driver
|
|
55
|
+
end
|
|
15
56
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
57
|
+
# Returns the wrapped page object model.
|
|
58
|
+
#
|
|
59
|
+
# @return [Citronella::Wrapper::PageDecorator] The page object model.
|
|
60
|
+
#
|
|
61
|
+
def page
|
|
62
|
+
Citronella::Wrapper::PageDecorator.new(@driver, @webdriver_wait, @page,
|
|
63
|
+
@logger)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Sets the page object model.
|
|
67
|
+
#
|
|
68
|
+
# @param [PageObject] page The page object model.
|
|
69
|
+
#
|
|
70
|
+
def page=(page)
|
|
71
|
+
@page = page
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# An alternative way for testing without using page objects.
|
|
75
|
+
# It returns a WebUi class, but can't use `page` and may cause an error.
|
|
76
|
+
# It's good for quick prototypes or writing tests.
|
|
77
|
+
#
|
|
78
|
+
# @param [Hash] args The locator details.
|
|
79
|
+
# @return [Citronella::Ui::WebUi] The web element.
|
|
80
|
+
#
|
|
81
|
+
# Usage:
|
|
82
|
+
# web.ui(name: 'q').get_element.text
|
|
83
|
+
# web.ui(name: 'q').get_element.click
|
|
84
|
+
#
|
|
85
|
+
def locate(args)
|
|
86
|
+
Citronella::Ui::WebUi.new(@driver, @webdriver_wait, @logger, args,
|
|
87
|
+
__method__.to_s,
|
|
88
|
+
self.class.name.split('::').last.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Executes JavaScript to wait for the page to fully load.
|
|
92
|
+
#
|
|
93
|
+
# @param [Integer] wait The number of times to check the page's ready state.
|
|
94
|
+
#
|
|
95
|
+
def ready_state(wait)
|
|
96
|
+
wait.times do |i|
|
|
97
|
+
return if driver.execute_script(
|
|
98
|
+
"return document.readyState") == "complete"
|
|
99
|
+
sleep(1)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
19
102
|
|
|
20
|
-
|
|
21
|
-
|
|
103
|
+
# Overrides the `webdriver_wait` value.
|
|
104
|
+
#
|
|
105
|
+
# @param [Integer] wait The new value for `webdriver_wait`.
|
|
106
|
+
#
|
|
107
|
+
def webdriver_wait(wait)
|
|
108
|
+
@webdriver_wait = wait
|
|
109
|
+
end
|
|
22
110
|
end
|
|
23
111
|
end
|
|
24
112
|
end
|
data/lib/web_ui.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#MIT License
|
|
2
|
+
#
|
|
3
|
+
#Copyright (c) 2023 Eko Purnomo
|
|
4
|
+
#
|
|
5
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
#in the Software without restriction, including without limitation the rights
|
|
8
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
#furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
#The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
#copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
#SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
require_relative 'logger'
|
|
25
|
+
|
|
26
|
+
module Citronella
|
|
27
|
+
module Ui
|
|
28
|
+
class WebUi
|
|
29
|
+
#
|
|
30
|
+
# @param [Webdriver] driver The web driver object.
|
|
31
|
+
# @param [Integer] webdriver_wait The timeout for webdriver wait.
|
|
32
|
+
# @param [Boolean] logger A flag indicating whether to log actions.
|
|
33
|
+
# @param [Hash] locator The locator for the web element.
|
|
34
|
+
# @param [String] function_name The name of the function.
|
|
35
|
+
# @param [String] class_name The name of the class.
|
|
36
|
+
#
|
|
37
|
+
def initialize(driver, webdriver_wait, logger, locator, function_name,
|
|
38
|
+
class_name)
|
|
39
|
+
@driver = driver
|
|
40
|
+
@wait = webdriver_wait
|
|
41
|
+
@logger = logger
|
|
42
|
+
@locator = locator
|
|
43
|
+
@function_name = function_name
|
|
44
|
+
@class_name = class_name
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Waits for a web element to be available and returns it.
|
|
48
|
+
#
|
|
49
|
+
# @param [Webdriver] ele The web driver object.
|
|
50
|
+
# @param [Boolean] displayed Flag indicating whether to wait for the element to be displayed.
|
|
51
|
+
# @return [Webdriver::Element] The web element.
|
|
52
|
+
#
|
|
53
|
+
private def webdriver_wait(ele, displayed=false)
|
|
54
|
+
el = Selenium::WebDriver::Wait.new(timeout: @wait).until { ele }
|
|
55
|
+
if displayed
|
|
56
|
+
@wait.times do
|
|
57
|
+
break if el.displayed?
|
|
58
|
+
sleep(1)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
el
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Sends keys to the web element.
|
|
65
|
+
#
|
|
66
|
+
# @param [String] text The text to be sent.
|
|
67
|
+
# @param [Boolean] clear Flag indicating whether to clear the field before sending keys.
|
|
68
|
+
# @param [Boolean] return_key Flag indicating whether to send a return key.
|
|
69
|
+
#
|
|
70
|
+
def send_keys(text, clear=false, return_key=false)
|
|
71
|
+
Citronella::Log.logger(@logger, @class_name, @function_name, __method__)
|
|
72
|
+
el = webdriver_wait(@driver.find_element(@locator), displayed=true)
|
|
73
|
+
el.send_keys text
|
|
74
|
+
|
|
75
|
+
if return_key
|
|
76
|
+
el.send_keys :return
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Clicks on the web element.
|
|
81
|
+
#
|
|
82
|
+
def click
|
|
83
|
+
Citronella::Log.logger(@logger, @class_name, @function_name, __method__)
|
|
84
|
+
el = webdriver_wait(@driver.find_element(@locator), displayed=true)
|
|
85
|
+
el.click
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns a web element using find_element method.
|
|
89
|
+
#
|
|
90
|
+
# @return [Webdriver::Element] The web element.
|
|
91
|
+
#
|
|
92
|
+
def get_element
|
|
93
|
+
Citronella::Log.logger(@logger, @class_name, @function_name,
|
|
94
|
+
__method__.to_s)
|
|
95
|
+
webdriver_wait(@driver.find_element(@locator))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns a list of web elements using find_elements method.
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Webdriver::Element>] The list of web elements.
|
|
101
|
+
#
|
|
102
|
+
def get_elements
|
|
103
|
+
Citronella::Log.logger(@logger, @class_name, @function_name,
|
|
104
|
+
__method__.to_s)
|
|
105
|
+
webdriver_wait(@driver.find_elements(@locator))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: citronella
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- heyclore
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-02
|
|
11
|
+
date: 2023-06-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: selenium-webdriver
|
|
@@ -34,14 +34,14 @@ files:
|
|
|
34
34
|
- README.md
|
|
35
35
|
- citronella.gemspec
|
|
36
36
|
- lib/citronella.rb
|
|
37
|
-
- lib/
|
|
38
|
-
- lib/
|
|
37
|
+
- lib/logger.rb
|
|
38
|
+
- lib/page_decorator.rb
|
|
39
39
|
- lib/ui.rb
|
|
40
|
-
- lib/ui_object.rb
|
|
41
40
|
- lib/web_page.rb
|
|
42
|
-
|
|
41
|
+
- lib/web_ui.rb
|
|
42
|
+
homepage: https://github.com/heyclore/citronella/tree/main/ruby#readme
|
|
43
43
|
licenses:
|
|
44
|
-
-
|
|
44
|
+
- MIT
|
|
45
45
|
metadata:
|
|
46
46
|
source_code_uri: https://github.com/heyclore/citronella/tree/main/ruby
|
|
47
47
|
github_repo: https://github.com/heyclore/citronella
|
data/lib/page_store.rb
DELETED
data/lib/page_wrapper.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
require_relative 'ui_object.rb'
|
|
2
|
-
|
|
3
|
-
module Citronella
|
|
4
|
-
module PageWrapper
|
|
5
|
-
def self.method_wrapper(klass, method_name, driver, webdriver_wait, pages)
|
|
6
|
-
original_method = klass.instance_method(method_name)
|
|
7
|
-
klass.define_method(method_name) do
|
|
8
|
-
puts method_name
|
|
9
|
-
args = original_method.bind(self).call
|
|
10
|
-
Citronella::UiObject::Ui.new(driver, webdriver_wait, pages,
|
|
11
|
-
args.locator, args.page, args.exception)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.class_decorator(klass, driver, webdriver_wait, pages)
|
|
16
|
-
return if klass.instance_variable_defined?(:@decorated)
|
|
17
|
-
klass.instance_variable_set(:@decorated, true)
|
|
18
|
-
return if klass.name == "Object"
|
|
19
|
-
lists = klass.instance_methods(false)
|
|
20
|
-
lists.each { |method| method_wrapper(klass, method, driver, webdriver_wait, pages) }
|
|
21
|
-
class_decorator(klass.superclass, driver, webdriver_wait, pages)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.ui_decorator(driver, webdriver_wait, pages)
|
|
25
|
-
klass = pages.get.last
|
|
26
|
-
class_decorator(klass, driver, webdriver_wait, pages)
|
|
27
|
-
klass.new
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
data/lib/ui_object.rb
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
module Citronella
|
|
2
|
-
module UiObject
|
|
3
|
-
class Ui
|
|
4
|
-
def initialize(driver, webdriver_wait, pages, locator, page, exception)
|
|
5
|
-
@driver = driver
|
|
6
|
-
@webdriver_wait = Selenium::WebDriver::Wait.new(timeout: webdriver_wait)
|
|
7
|
-
@pages = pages
|
|
8
|
-
@locator = locator
|
|
9
|
-
@page = page
|
|
10
|
-
@exception = exception
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def send_keys(text, enter: false)
|
|
14
|
-
@webdriver_wait.until { @driver.find_element(@locator).displayed? }
|
|
15
|
-
el = @driver.find_element(@locator)
|
|
16
|
-
el.send_keys text
|
|
17
|
-
|
|
18
|
-
if enter
|
|
19
|
-
el.send_keys :return
|
|
20
|
-
|
|
21
|
-
if @page
|
|
22
|
-
@pages.get << @page
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def click
|
|
28
|
-
@webdriver_wait.until { @driver.find_element(@locator).displayed? }
|
|
29
|
-
@driver.find_element(@locator).click
|
|
30
|
-
|
|
31
|
-
if @page
|
|
32
|
-
@pages.get << @page
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def get_elements
|
|
37
|
-
@webdriver_wait.until { @driver.find_element(@locator).displayed? }
|
|
38
|
-
@driver.find_elements(@locator)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|