bbq-core 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +4 -0
- data/CHANGELOG +47 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +318 -0
- data/Rakefile +10 -0
- data/bbq-core.gemspec +28 -0
- data/lib/bbq/core.rb +7 -0
- data/lib/bbq/core/roles.rb +14 -0
- data/lib/bbq/core/session.rb +64 -0
- data/lib/bbq/core/test_client.rb +97 -0
- data/lib/bbq/core/test_user.rb +33 -0
- data/lib/bbq/core/test_user/capybara_dsl.rb +17 -0
- data/lib/bbq/core/test_user/eyes.rb +15 -0
- data/lib/bbq/core/test_user/within.rb +35 -0
- data/lib/bbq/core/util.rb +21 -0
- data/lib/bbq/core/version.rb +5 -0
- data/lib/tasks/bbq.rake +15 -0
- data/test/session_pool_test.rb +44 -0
- data/test/test_client_test.rb +30 -0
- data/test/test_helper.rb +8 -0
- data/test/test_user_test.rb +112 -0
- data/test/util_test.rb +36 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4a48e0d7e2b08e20b007adafe0ead84e77bb54cc68bd4c8cdf7e5d68a1ae1fde
|
4
|
+
data.tar.gz: d8a944e766fb21bc119d771d28fcdee6e27a393e78ae4fded7bd403b420e613f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 69a88b57acf34e2ec33f347a86d82f8c45f275f2ed25852e4a313fa4196ba9f6fc2fab33758f9059b67fa7b318a18ba2efd87ef4cc88bbb7114df4a84a47f6a6
|
7
|
+
data.tar.gz: f90be21ff429c510175e37d08fa438734053b97260b9979bb04b53e2caf3bc8edcd17d4b2458cd4cb7e50be74ff7efcccba963e28b1553dc526c1d9afa29261c
|
data/.gitignore
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
*.gem
|
2
|
+
.bundle
|
3
|
+
Gemfile.lock
|
4
|
+
pkg/*
|
5
|
+
bin/*
|
6
|
+
tmp/*
|
7
|
+
test/dummy/db/*.sqlite3
|
8
|
+
test/dummy/log/*.log
|
9
|
+
test/dummy/tmp/
|
10
|
+
test/dummy/test/acceptance/*
|
11
|
+
test/dummy/spec/acceptance/*
|
12
|
+
test/dope/test/acceptance/*
|
13
|
+
test/dope/spec/acceptance/*
|
14
|
+
nbproject/
|
data/.travis.yml
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
0.2.1 / 2013-08-22
|
2
|
+
==================
|
3
|
+
|
4
|
+
* bugfix: Bbq::TestClient propagates NoMethodErrors from the controller of
|
5
|
+
tested app instead of rising unsupported method errors.
|
6
|
+
|
7
|
+
0.2.0 / 2013-01-06
|
8
|
+
==================
|
9
|
+
|
10
|
+
* Dropped support for Ruby 1.8.7
|
11
|
+
* Capybara 2.0 support
|
12
|
+
|
13
|
+
0.1.0 / 2013-01-06
|
14
|
+
==================
|
15
|
+
|
16
|
+
* Extracted Rails' URL helpers inclusion to separate module
|
17
|
+
* Include only Capybara::Session::DSL_METHODS in TestUser, not the whole Capybara::DSL
|
18
|
+
* Added Capybara sessions pool
|
19
|
+
* Moved Test::User default options to separate method
|
20
|
+
* Added Test::Client for testing REST APIs
|
21
|
+
* Renamed bbq/test.rb to bbq/test_unit.rb. You may want to fix your test_helper.
|
22
|
+
* Moved require bbq/test_user to generated test_user.rb. You may want to update your existing test_user.rb.
|
23
|
+
* Fixed Capybara 'within' scope for RSpec flavour
|
24
|
+
|
25
|
+
0.0.4 / 2011-10-19
|
26
|
+
==================
|
27
|
+
|
28
|
+
* Make Bbq work with Capybara 1.0 and 1.1
|
29
|
+
* Rails is development dependency
|
30
|
+
|
31
|
+
0.0.3 / 2011-07-14
|
32
|
+
==================
|
33
|
+
|
34
|
+
* Added support and tests for Sinatra
|
35
|
+
* Added Bbq.app and Bbq.app=
|
36
|
+
|
37
|
+
0.0.2 / 2011-07-01
|
38
|
+
==================
|
39
|
+
|
40
|
+
* Extracted Bbq::TestUser::Eyes module
|
41
|
+
* Added :within option to TestUser methods
|
42
|
+
* Fix tests for ree and ruby187
|
43
|
+
|
44
|
+
0.0.1 / 2011-04-20
|
45
|
+
==================
|
46
|
+
|
47
|
+
* Bbq introduced to the wild world!
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2011 DRUG, Dolnośląska Grupa Użytkowników Ruby
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,318 @@
|
|
1
|
+
# BBQ
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/drugpl/bbq-core.png)](http://travis-ci.org/drugpl/bbq-core) [![Dependency Status](https://gemnasium.com/drugpl/bbq-core.png)](https://gemnasium.com/drugpl/bbq-core) [![Code Climate](https://codeclimate.com/github/drugpl/bbq-core.png)](https://codeclimate.com/github/drugpl/bbq-core) [![Gem Version](https://badge.fury.io/rb/bbq-core.png)](http://badge.fury.io/rb/bbq-core)
|
4
|
+
|
5
|
+
Object oriented acceptance testing using personas.
|
6
|
+
|
7
|
+
* Ruby (no Gherkin)
|
8
|
+
* Objects and methods instead of steps
|
9
|
+
* Test framework independent (RSpec and Test::Unit support)
|
10
|
+
* Thins based on Capybara.
|
11
|
+
* DCI (Data Context Interaction) for roles/personas
|
12
|
+
* Opinionated
|
13
|
+
|
14
|
+
## Setup
|
15
|
+
|
16
|
+
First, add BBQ to your apps `Gemfile`:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem "bbq", "0.2.1"
|
20
|
+
```
|
21
|
+
|
22
|
+
Run install generator:
|
23
|
+
|
24
|
+
```
|
25
|
+
bundle exec rails generate bbq:install
|
26
|
+
```
|
27
|
+
|
28
|
+
Require BBQ in test/test_helper.rb (in case of Test::Unit):
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require "bbq/test_unit"
|
32
|
+
```
|
33
|
+
|
34
|
+
Require BBQ in spec/spec_helper.rb (in case of RSpec):
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require "bbq/rspec"
|
38
|
+
```
|
39
|
+
|
40
|
+
## Feature generator
|
41
|
+
|
42
|
+
```
|
43
|
+
bundle exec rails g bbq:test MyFeatureName
|
44
|
+
```
|
45
|
+
|
46
|
+
## Running features
|
47
|
+
|
48
|
+
For Test::Unit flavour:
|
49
|
+
|
50
|
+
```
|
51
|
+
bundle exec rake test:acceptance
|
52
|
+
```
|
53
|
+
|
54
|
+
For RSpec flavour:
|
55
|
+
|
56
|
+
```
|
57
|
+
bundle exec rake spec:acceptance
|
58
|
+
```
|
59
|
+
|
60
|
+
## Examples
|
61
|
+
|
62
|
+
### Roles and Devise integration
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class TestUser < Bbq::TestUser
|
66
|
+
include Bbq::Devise
|
67
|
+
|
68
|
+
def update_ticket(summary, comment)
|
69
|
+
show_ticket(summary)
|
70
|
+
fill_in "Comment", :with => comment
|
71
|
+
click_on "Add update"
|
72
|
+
end
|
73
|
+
|
74
|
+
def open_application
|
75
|
+
visit '/'
|
76
|
+
end
|
77
|
+
|
78
|
+
module TicketReporter
|
79
|
+
def open_tickets_listing
|
80
|
+
open_application
|
81
|
+
click_link 'Tickets'
|
82
|
+
end
|
83
|
+
|
84
|
+
def open_ticket(summary, description)
|
85
|
+
open_tickets_listing
|
86
|
+
click_on "Open a new ticket"
|
87
|
+
fill_in "Summary", :with => summary
|
88
|
+
fill_in "Description", :with => description
|
89
|
+
click_on "Open ticket"
|
90
|
+
end
|
91
|
+
|
92
|
+
def show_ticket(summary)
|
93
|
+
open_tickets_listing
|
94
|
+
click_on summary
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
module TicketManager
|
99
|
+
def open_administration
|
100
|
+
visit '/admin'
|
101
|
+
end
|
102
|
+
|
103
|
+
def open_tickets_listing
|
104
|
+
open_administration
|
105
|
+
click_link 'Tickets'
|
106
|
+
end
|
107
|
+
|
108
|
+
def close_ticket(summary, comment = nil)
|
109
|
+
open_tickets_listing
|
110
|
+
click_on summary
|
111
|
+
fill_in "Comment", :with => comment if comment
|
112
|
+
click_on "Close ticket"
|
113
|
+
end
|
114
|
+
|
115
|
+
def show_ticket(summary)
|
116
|
+
open_tickets_listing
|
117
|
+
click_on summary
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class AdminTicketsTest < Bbq::TestCase
|
125
|
+
background do
|
126
|
+
admin = Factory(:admin)
|
127
|
+
@email, @password = admin.email, admin.password
|
128
|
+
end
|
129
|
+
|
130
|
+
scenario "admin can browse all user tickets" do
|
131
|
+
summaries = ["Forgot my password", "Page is not displayed correctly"]
|
132
|
+
descriptions = ["I lost my yellow note with password under the table!",
|
133
|
+
"My IE renders crap instead of crispy fonts!"]
|
134
|
+
|
135
|
+
alice = TestUser.new
|
136
|
+
alice.roles(:ticket_reporter)
|
137
|
+
alice.register_and_login
|
138
|
+
alice.open_ticket(summaries.first, descriptions.first)
|
139
|
+
|
140
|
+
bob = TestUser.new
|
141
|
+
bob.roles(:ticket_reporter)
|
142
|
+
bob.register_and_login
|
143
|
+
bob.open_ticket(summaries.second, descriptions.second)
|
144
|
+
|
145
|
+
charlie = TestUser.new(:email => @email, :password => @password)
|
146
|
+
charlie.login # charlie was already "registered" in factory as admin
|
147
|
+
charlie.roles(:ticket_manager)
|
148
|
+
charlie.open_tickets_listing
|
149
|
+
charlie.see!(*summaries)
|
150
|
+
|
151
|
+
charlie.click_on(summaries.second)
|
152
|
+
charlie.see!(summaries.second, descriptions.second)
|
153
|
+
charlie.not_see!(summaries.first, descriptions.first)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
### RSpec integration
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class TestUser < Bbq::TestUser
|
162
|
+
def email
|
163
|
+
@options[:email] || "buyer@example.com"
|
164
|
+
end
|
165
|
+
|
166
|
+
module Buyer
|
167
|
+
def ask_question(question)
|
168
|
+
fill_in "question", :with => question
|
169
|
+
fill_in "email", :with => email
|
170
|
+
click_on("Ask")
|
171
|
+
end
|
172
|
+
|
173
|
+
def go_to_page_and_open_widget(page_url, &block)
|
174
|
+
go_to_page(page_url)
|
175
|
+
open_widget &block
|
176
|
+
end
|
177
|
+
|
178
|
+
def go_to_page(page_url)
|
179
|
+
visit page_url
|
180
|
+
wait_until { page.find("iframe") }
|
181
|
+
end
|
182
|
+
|
183
|
+
def open_widget
|
184
|
+
within_widget do
|
185
|
+
page.find("#widget h3").click
|
186
|
+
yield if block_given?
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
ef within_widget(&block)
|
191
|
+
within_frame(widget_frame, &block)
|
192
|
+
end
|
193
|
+
|
194
|
+
def widget_frame
|
195
|
+
page.evaluate_script("document.getElementsByTagName('iframe')[0].id")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
feature "ask question widget" do
|
203
|
+
let(:user) {
|
204
|
+
user = TestUser.new(:driver => :webkit)
|
205
|
+
user.roles('buyer')
|
206
|
+
user
|
207
|
+
}
|
208
|
+
|
209
|
+
scenario "as a guest user, I should be able to ask a question" do
|
210
|
+
user.go_to_page_and_open_widget("/widget") do
|
211
|
+
user.ask_question "my question"
|
212
|
+
user.see!("Thanks!")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
## Testing REST APIs
|
219
|
+
|
220
|
+
Bbq provides `Bbq::TestClient`, similar to `Bbq::TestUser`, but intended for testing APIs.
|
221
|
+
It's a thin wrapper around `Rack::Test` which allows you to send requests and run assertions
|
222
|
+
against responses.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class ApiTest < Bbq::TestCase
|
226
|
+
background do
|
227
|
+
headers = {'HTTP_ACCEPT' => 'application/json'}
|
228
|
+
@client = TestClient.new(:headers => headers)
|
229
|
+
end
|
230
|
+
|
231
|
+
scenario "admin can browse all user tickets" do
|
232
|
+
@client.get "/unicorn" do |response|
|
233
|
+
assert_equal 200, response.status
|
234
|
+
assert_equal "pink", response.body["unicorn"]["color"]
|
235
|
+
end
|
236
|
+
@client.post "/ponies", { :name => "Miracle" } do |response|
|
237
|
+
assert_equal 200, response.status
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
## Rails URL Helpers
|
244
|
+
|
245
|
+
Using url helpers from Rails in integration tests is not recommended.
|
246
|
+
Testing routes is part of integration test, so you should actually use only
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
visit '/'
|
250
|
+
```
|
251
|
+
|
252
|
+
in your integration test. Use links and buttons in order to get to other pages in your app.
|
253
|
+
|
254
|
+
If you really need url helpers in your test user, just include them in your TestUser class:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
require 'bbq/rails/routes'
|
258
|
+
|
259
|
+
class TestUser < Bbq::TestUser
|
260
|
+
include Bbq::Rails::Routes
|
261
|
+
end
|
262
|
+
```
|
263
|
+
or just
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class TestUser < Bbq::TestUser
|
267
|
+
include ::ActionDispatch::Routing::UrlFor
|
268
|
+
include ::Rails.application.routes.url_helpers
|
269
|
+
include ::ActionDispatch::Routing::RouteSet::MountedHelpers unless ::Rails.version < "3.1"
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
## Devise support
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
require "bbq/test_user"
|
277
|
+
require "bbq/devise"
|
278
|
+
|
279
|
+
class TestUser < Bbq::TestUser
|
280
|
+
include Bbq::Devise
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
After that TestUser have *login*, *logout*, *register*, *register_and_login* methods.
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
test "user register with devise" do
|
288
|
+
user = TestUser.new # or TestUser.new(:email => "email@example.com", :password => "secret")
|
289
|
+
user.register_and_login
|
290
|
+
user.see!("Stuff after auth")
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## Caveats
|
295
|
+
|
296
|
+
### Timeout::Error
|
297
|
+
|
298
|
+
If you simulate multiple users in your tests and spawn multiple browsers with selenium it might
|
299
|
+
be a good idea to use Thin instead of Webrick to create application server.
|
300
|
+
We have experienced some problems with Webrick that lead to `Timeout::Error` exception
|
301
|
+
when user/browser that was inactive for some time (due to other users/browsers
|
302
|
+
activities) was requested to execute an action.
|
303
|
+
|
304
|
+
Capybara will use Thin instead of Webrick when it's available, so you only need to add Thin to you Gemfile:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
# In test group if you want it to
|
308
|
+
# be used only in tests and not in your development mode
|
309
|
+
# ex. when running 'rails s'
|
310
|
+
|
311
|
+
gem 'thin', :require => false
|
312
|
+
```
|
313
|
+
|
314
|
+
## Additional information
|
315
|
+
|
316
|
+
* [2 problems with Cucumber](http://andrzejonsoftware.blogspot.com/2011/03/2-problems-with-cucumber.html)
|
317
|
+
* [Object oriented acceptance testing](http://andrzejonsoftware.blogspot.com/2011/04/object-oriented-acceptance-testing.html)
|
318
|
+
|
data/Rakefile
ADDED
data/bbq-core.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "bbq/core/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "bbq-core"
|
7
|
+
s.version = Bbq::Core::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["DRUG - Dolnośląska Grupa Użytkowników Ruby"]
|
10
|
+
s.email = ["bbq@drug.org.pl"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.description = %q{Objected oriented acceptance testing for Rails, using personas.}
|
13
|
+
s.summary = %q{Objected oriented acceptance testing for Rails, using personas.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "bbq-core"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency "capybara", ">= 2.0"
|
23
|
+
s.add_dependency "activesupport", ">= 2.0"
|
24
|
+
|
25
|
+
s.add_development_dependency "rake"
|
26
|
+
s.add_development_dependency "rdoc", "~> 3.7"
|
27
|
+
s.add_development_dependency "minitest", "~> 5.0"
|
28
|
+
end
|
data/lib/bbq/core.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'bbq/core'
|
2
|
+
|
3
|
+
module Bbq
|
4
|
+
module Core
|
5
|
+
module Session
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def next(options = {})
|
9
|
+
driver = options.delete(:driver)
|
10
|
+
pool = options.delete(:pool)
|
11
|
+
|
12
|
+
if pool
|
13
|
+
pool.next(driver)
|
14
|
+
else
|
15
|
+
create(driver)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(driver)
|
20
|
+
Capybara::Session.new(driver, Bbq::Core.app)
|
21
|
+
end
|
22
|
+
|
23
|
+
def pool
|
24
|
+
@pool ||= Pool.new
|
25
|
+
end
|
26
|
+
|
27
|
+
class Pool
|
28
|
+
attr_accessor :idle, :taken
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@idle = []
|
32
|
+
@taken = []
|
33
|
+
end
|
34
|
+
|
35
|
+
def next(driver)
|
36
|
+
take_idle(driver) || create(driver)
|
37
|
+
end
|
38
|
+
|
39
|
+
def release
|
40
|
+
taken.each(&:reset!)
|
41
|
+
idle.concat(taken)
|
42
|
+
taken.clear
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def take_idle(driver)
|
48
|
+
idle.find { |s| s.mode == driver }.tap do |session|
|
49
|
+
if session
|
50
|
+
idle.delete(session)
|
51
|
+
taken.push(session)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create(driver)
|
57
|
+
Bbq::Core::Session.create(driver).tap do |session|
|
58
|
+
taken.push(session)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'bbq/core/roles'
|
2
|
+
|
3
|
+
module Bbq
|
4
|
+
module Core
|
5
|
+
class TestClient
|
6
|
+
class UnsupportedMethodError < StandardError; end
|
7
|
+
|
8
|
+
include Bbq::Core::Roles
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
HTTP_METHODS = %w(get post put delete head options patch)
|
15
|
+
|
16
|
+
HTTP_METHODS.each do |method|
|
17
|
+
class_eval <<-RUBY
|
18
|
+
def #{method}(path, params = {}, headers = {})
|
19
|
+
unless driver.respond_to? :#{method}
|
20
|
+
raise UnsupportedMethodError, "Your driver does not support #{method.upcase} method"
|
21
|
+
end
|
22
|
+
|
23
|
+
response = driver.#{method}(path, params, default_headers.merge(headers))
|
24
|
+
parsed_response = parse_response(response)
|
25
|
+
yield parsed_response if block_given?
|
26
|
+
parsed_response
|
27
|
+
end
|
28
|
+
RUBY
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
def app
|
33
|
+
@options[:app] || Bbq::Core.app
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_headers
|
37
|
+
@options[:headers] || {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def driver
|
41
|
+
@driver ||= RackTest.new(app)
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_response(response)
|
45
|
+
case response.headers["Content-Type"]
|
46
|
+
when /^application\/(.*\+)?json/
|
47
|
+
response.extend(JsonBody)
|
48
|
+
when /^application\/(.*\+)?x-yaml/
|
49
|
+
response.extend(YamlBody)
|
50
|
+
else
|
51
|
+
response
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class RackTest
|
56
|
+
attr_accessor :app
|
57
|
+
|
58
|
+
def initialize(app)
|
59
|
+
self.app = app
|
60
|
+
end
|
61
|
+
|
62
|
+
module ConvertHeaders
|
63
|
+
HTTP_METHODS.each do |method|
|
64
|
+
class_eval <<-RUBY
|
65
|
+
def #{method}(path, params = {}, headers = {})
|
66
|
+
super(path, params, to_env_headers(headers))
|
67
|
+
end
|
68
|
+
RUBY
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_env_headers(http_headers)
|
72
|
+
http_headers.map do |k, v|
|
73
|
+
k = k.upcase.gsub("-", "_")
|
74
|
+
k = "HTTP_#{k}" unless ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(k)
|
75
|
+
{ k => v }
|
76
|
+
end.inject({}, :merge)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
include Rack::Test::Methods
|
81
|
+
include ConvertHeaders
|
82
|
+
end
|
83
|
+
|
84
|
+
module JsonBody
|
85
|
+
def body
|
86
|
+
@parsed_body ||= super.empty?? super : JSON.parse(super)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module YamlBody
|
91
|
+
def body
|
92
|
+
@parsed_body ||= YAML.load(super)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bbq/core/session'
|
2
|
+
require 'bbq/core/roles'
|
3
|
+
require 'bbq/core/test_user/capybara_dsl'
|
4
|
+
require 'bbq/core/test_user/eyes'
|
5
|
+
require 'bbq/core/test_user/within'
|
6
|
+
|
7
|
+
module Bbq
|
8
|
+
module Core
|
9
|
+
class TestUser
|
10
|
+
include Bbq::Core::TestUser::CapybaraDsl
|
11
|
+
include Bbq::Core::TestUser::Eyes
|
12
|
+
include Bbq::Core::TestUser::Within
|
13
|
+
include Bbq::Core::Roles
|
14
|
+
|
15
|
+
attr_reader :options
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
@options = default_options.merge(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_options
|
22
|
+
{
|
23
|
+
:pool => Bbq::Core::Session.pool,
|
24
|
+
:driver => ::Capybara.default_driver
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def page
|
29
|
+
@page ||= options[:session] || Bbq::Core::Session.next(:driver => options[:driver], :pool => options[:pool])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'capybara/dsl'
|
2
|
+
|
3
|
+
module Bbq
|
4
|
+
module Core
|
5
|
+
class TestUser
|
6
|
+
module CapybaraDsl
|
7
|
+
::Capybara::Session::DSL_METHODS.each do |method|
|
8
|
+
class_eval <<-RUBY, __FILE__, __LINE__+1
|
9
|
+
def #{method}(*args, &block)
|
10
|
+
page.#{method}(*args, &block)
|
11
|
+
end
|
12
|
+
RUBY
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_support/core_ext/array/extract_options'
|
2
|
+
|
3
|
+
module Bbq
|
4
|
+
module Core
|
5
|
+
class TestUser
|
6
|
+
module Within
|
7
|
+
METHODS_USING_WITHIN = [
|
8
|
+
:see?, :not_see?,
|
9
|
+
:attach_file, :check, :choose, :click_link_or_button, :click_button,
|
10
|
+
:click_link, :click_on, :fill_in, :select, :uncheck, :unselect
|
11
|
+
]
|
12
|
+
|
13
|
+
METHODS_USING_WITHIN.each do |method_name|
|
14
|
+
class_eval <<-RUBY
|
15
|
+
def #{method_name}(*args)
|
16
|
+
using_within(args) { super }
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
|
21
|
+
def using_within(args)
|
22
|
+
options = args.extract_options!
|
23
|
+
locator = options.delete(:within)
|
24
|
+
args.push(options) unless options.empty?
|
25
|
+
|
26
|
+
if locator
|
27
|
+
within(locator) { yield }
|
28
|
+
else
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
|
3
|
+
module Bbq
|
4
|
+
module Core
|
5
|
+
class Util
|
6
|
+
def self.find_module(name, scope = nil)
|
7
|
+
namespace = case scope
|
8
|
+
when String, Symbol
|
9
|
+
"::#{scope.to_s.camelize}"
|
10
|
+
when Class
|
11
|
+
"::#{scope.name}"
|
12
|
+
when NilClass
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
"::#{scope.class.name}"
|
16
|
+
end
|
17
|
+
"#{namespace}::#{name.to_s.camelize}".constantize
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/tasks/bbq.rake
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new("test:acceptance") do |t|
|
4
|
+
t.libs << 'test'
|
5
|
+
t.pattern = 'test/acceptance/**/*_test.rb'
|
6
|
+
t.verbose = false
|
7
|
+
end if File.exists?('test/acceptance')
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
RSpec::Core::RakeTask.new('spec:acceptance') do |spec|
|
12
|
+
spec.pattern = FileList['spec/acceptance/**/*_spec.rb']
|
13
|
+
end if File.exists?('spec/acceptance')
|
14
|
+
rescue LoadError
|
15
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'bbq/core/test_user'
|
3
|
+
|
4
|
+
class SessionPoolTest < Minitest::Test
|
5
|
+
|
6
|
+
def setup
|
7
|
+
Bbq::Core::Session.instance_variable_set(:@pool, nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_reuses_sessions
|
11
|
+
pool = Bbq::Core::Session::Pool.new
|
12
|
+
user1 = Bbq::Core::TestUser.new(:pool => pool).tap { |u| u.page }
|
13
|
+
pool.release
|
14
|
+
user2 = Bbq::Core::TestUser.new(:pool => pool).tap { |u| u.page }
|
15
|
+
|
16
|
+
assert_same user1.page, user2.page
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_has_default_pool
|
20
|
+
user1 = Bbq::Core::TestUser.new.tap { |u| u.page }
|
21
|
+
Bbq::Core::Session::pool.release
|
22
|
+
user2 = Bbq::Core::TestUser.new.tap { |u| u.page }
|
23
|
+
|
24
|
+
assert_same user1.page, user2.page
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_without_pool
|
28
|
+
user1 = Bbq::Core::TestUser.new(:pool => false).tap { |u| u.page }
|
29
|
+
Bbq::Core::Session::pool.release
|
30
|
+
user2 = Bbq::Core::TestUser.new(:pool => false).tap { |u| u.page }
|
31
|
+
|
32
|
+
refute_same user1.page, user2.page
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_pool_returns_correct_driver
|
36
|
+
pool = Bbq::Core::Session::Pool.new
|
37
|
+
pool.next(:rack_test)
|
38
|
+
pool.next(:rack_test_the_other)
|
39
|
+
pool.release
|
40
|
+
|
41
|
+
assert_equal :rack_test, pool.next(:rack_test).mode
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'bbq/core/test_client'
|
3
|
+
|
4
|
+
class TestClientTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_rack_test_to_env_headers_for_empty_hash
|
7
|
+
test_client = Bbq::Core::TestClient::RackTest.new(:app)
|
8
|
+
assert_equal({}, test_client.to_env_headers({}))
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_rack_test_to_env_headers_for_content_type_or_content_length
|
12
|
+
test_client = Bbq::Core::TestClient::RackTest.new(:app)
|
13
|
+
result = test_client.to_env_headers({
|
14
|
+
"content-type" => "text/plain",
|
15
|
+
"content-length" => "40"
|
16
|
+
})
|
17
|
+
assert_includes(result.keys, "CONTENT_TYPE")
|
18
|
+
assert_includes(result.keys, "CONTENT_LENGTH")
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_rack_test_to_env_headers_for_other_headers
|
22
|
+
test_client = Bbq::Core::TestClient::RackTest.new(:app)
|
23
|
+
result = test_client.to_env_headers({
|
24
|
+
"silly-header" => "silly-value"
|
25
|
+
})
|
26
|
+
assert_includes(result.keys, "HTTP_SILLY_HEADER")
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'bbq/core/test_user'
|
3
|
+
|
4
|
+
|
5
|
+
def text(output)
|
6
|
+
end
|
7
|
+
|
8
|
+
def html(output)
|
9
|
+
->{ ['200', {'Content-Type' => 'text/html'}, [output]] }
|
10
|
+
end
|
11
|
+
|
12
|
+
TestApp = Rack::Builder.new do
|
13
|
+
map '/' do
|
14
|
+
run ->(env) { ['200', {'Content-Type' => 'text/plain'}, ['BBQ']] }
|
15
|
+
end
|
16
|
+
|
17
|
+
map '/miracle' do
|
18
|
+
run ->(env) { ['200', {'Content-Type' => 'text/plain'}, ['MIRACLE']] }
|
19
|
+
end
|
20
|
+
|
21
|
+
map '/ponycorns' do
|
22
|
+
run ->(env) { ['200', {'Content-Type' => 'text/html'}, [<<HTML
|
23
|
+
<ul id="unicorns">
|
24
|
+
<li>Pink</li>
|
25
|
+
</ul>
|
26
|
+
<ul id="ponies">
|
27
|
+
<li>Violet</li>
|
28
|
+
</ul>
|
29
|
+
<div id="new_pony">
|
30
|
+
<input type="text" name="color" value="" />
|
31
|
+
</div>
|
32
|
+
<a href="#">More ponycorns</a>
|
33
|
+
HTML
|
34
|
+
]] }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
class TestUser < Bbq::Core::TestUser
|
40
|
+
module Commenter
|
41
|
+
def comment
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module VideoUploader
|
46
|
+
def upload
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module CommentModerator
|
51
|
+
def moderate
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class TestUserTest < Minitest::Test
|
57
|
+
|
58
|
+
def setup
|
59
|
+
Bbq::Core.app = TestApp
|
60
|
+
end
|
61
|
+
|
62
|
+
def teardown
|
63
|
+
Bbq::Core.app = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_capybara_dsl_methods
|
67
|
+
user = TestUser.new
|
68
|
+
Capybara::Session::DSL_METHODS.each do |m|
|
69
|
+
assert user.respond_to?(m)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_driver_option
|
74
|
+
user = TestUser.new(:driver => :rack_test_the_other)
|
75
|
+
assert_equal :rack_test_the_other, user.page.mode
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_roles
|
79
|
+
user = TestUser.new
|
80
|
+
%w(comment upload moderate).each { |m| assert !user.respond_to?(m) }
|
81
|
+
|
82
|
+
user.roles(:commenter, "comment_moderator")
|
83
|
+
%w(comment moderate).each { |m| assert user.respond_to?(m) }
|
84
|
+
assert !user.respond_to?(:upload)
|
85
|
+
|
86
|
+
user.roles(:video_uploader)
|
87
|
+
%w(comment upload moderate).each { |m| assert user.respond_to?(m) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_explicit_user_eyes
|
91
|
+
@user = TestUser.new
|
92
|
+
@user.visit "/miracle"
|
93
|
+
assert @user.not_see?("BBQ")
|
94
|
+
assert @user.see?("MIRACLE")
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_user_eyes_within_scope
|
98
|
+
@user = TestUser.new
|
99
|
+
@user.visit "/ponycorns"
|
100
|
+
assert @user.see?("Pink", :within => "#unicorns")
|
101
|
+
assert ! @user.see?("Violet", :within => "#unicorns")
|
102
|
+
assert @user.not_see?("Violet", :within => "#unicorns")
|
103
|
+
assert ! @user.not_see?("Pink", :within => "#unicorns")
|
104
|
+
|
105
|
+
@user.fill_in "color", :with => "red", :within => "#new_pony"
|
106
|
+
assert_raises Capybara::ElementNotFound do
|
107
|
+
@user.fill_in "color", :with => "red", :within => "#new_unicorn"
|
108
|
+
end
|
109
|
+
@user.click_link "More ponycorns"
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
data/test/util_test.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'bbq/core/util'
|
3
|
+
|
4
|
+
class User
|
5
|
+
module Commenter
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Commenter
|
10
|
+
end
|
11
|
+
|
12
|
+
class UtilTest < Minitest::Test
|
13
|
+
|
14
|
+
def test_find_module_in_object_namespace
|
15
|
+
assert_commenter(User.new, User::Commenter)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_find_module_in_class_namespace
|
19
|
+
assert_commenter(User, User::Commenter)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_find_module_in_string_namespace
|
23
|
+
assert_commenter("User", User::Commenter)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_find_global_module
|
27
|
+
assert_commenter(nil, ::Commenter)
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_commenter(namespace, result)
|
31
|
+
[:commenter, "commenter"].each do |name|
|
32
|
+
assert_equal Bbq::Core::Util.find_module(name, namespace), result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bbq-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- DRUG - Dolnośląska Grupa Użytkowników Ruby
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: capybara
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rdoc
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
description: Objected oriented acceptance testing for Rails, using personas.
|
84
|
+
email:
|
85
|
+
- bbq@drug.org.pl
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".travis.yml"
|
92
|
+
- CHANGELOG
|
93
|
+
- Gemfile
|
94
|
+
- MIT-LICENSE
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bbq-core.gemspec
|
98
|
+
- lib/bbq/core.rb
|
99
|
+
- lib/bbq/core/roles.rb
|
100
|
+
- lib/bbq/core/session.rb
|
101
|
+
- lib/bbq/core/test_client.rb
|
102
|
+
- lib/bbq/core/test_user.rb
|
103
|
+
- lib/bbq/core/test_user/capybara_dsl.rb
|
104
|
+
- lib/bbq/core/test_user/eyes.rb
|
105
|
+
- lib/bbq/core/test_user/within.rb
|
106
|
+
- lib/bbq/core/util.rb
|
107
|
+
- lib/bbq/core/version.rb
|
108
|
+
- lib/tasks/bbq.rake
|
109
|
+
- test/session_pool_test.rb
|
110
|
+
- test/test_client_test.rb
|
111
|
+
- test/test_helper.rb
|
112
|
+
- test/test_user_test.rb
|
113
|
+
- test/util_test.rb
|
114
|
+
homepage: ''
|
115
|
+
licenses: []
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubygems_version: 3.0.3
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: Objected oriented acceptance testing for Rails, using personas.
|
136
|
+
test_files:
|
137
|
+
- test/session_pool_test.rb
|
138
|
+
- test/test_client_test.rb
|
139
|
+
- test/test_helper.rb
|
140
|
+
- test/test_user_test.rb
|
141
|
+
- test/util_test.rb
|