watir_pump 0.4.6 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8b35528786a7594498534888276d4b16b94696c5
4
- data.tar.gz: 6976ae9ccda975a1d85aae48a866ffa2fbed4b3a
2
+ SHA256:
3
+ metadata.gz: c9ac45f1057344a09c7d15e228a5342a9706ce72bdc2ccfc29803fef63ab050d
4
+ data.tar.gz: 04a6ec0412739ea76326266a65d2fae07a3490beb2303ef904bac47b74df7c4b
5
5
  SHA512:
6
- metadata.gz: 5aa5e3cf1392ceced31521c95301ed99e9fdfdd3cf283f9a33b9959102f7caaabf2b19206a2467312edbcc949beff179635e695ee9d8af664d3f91817906d87c
7
- data.tar.gz: dee32b914c4c36fef6f2a6340abf66d9ba61e69e76bf6390eb726bfc47d3f5f02b9905113097f729f2d902ad44272788cc7bd54b970ea72663b2f128eb3764b2
6
+ metadata.gz: b0a53a55fe324275357fdf8ea9db127404b5c73f78fe57da026a76b9b7dd7c01a7f9e34a012da7d18339f0e3aff606df9a1966e8cbee6d01578b1c2432b82677
7
+ data.tar.gz: 5fef97f83fa2ca0a4219b8dff4839bcdf633821b63d00f51937a1097fd0d7de4c776694bb55c141c2707409c5c3ededc0d67b7b7f68d705c507e3d28ef103459
@@ -0,0 +1,995 @@
1
+ # WatirPump
2
+
3
+ `WatirPump` is a `Page Object` pattern implementation for `Watir`. Hacker friendly and enterprise ready.
4
+ Heavily inspired by `SitePrism` and `Watirsome`.
5
+
6
+ ### To learn WatirPump by example please refer to [THIS TUTORIAL](https://github.com/bwilczek/watir_pump_tutorial)
7
+
8
+ **Table of contents**
9
+ * [Key features](#key-features)
10
+ * [Examples](#examples)
11
+ * [Step 1: Just Watir elements](#step-1-just-watir-elements)
12
+ * [Step 2: Make it a component](#step-2-make-it-a-component)
13
+ * [Step 3: Make it more elegant and ready for Ajax](#step-3-make-it-more-elegant-and-ready-for-ajax)
14
+ * [Documentation](#documentation)
15
+ * [Installation](#installation)
16
+ * [Configuration](#configuration)
17
+ * [Page](#page)
18
+ * [uri & loaded?](#uri--loaded)
19
+ * [Interacting with pages](#interacting-with-pages)
20
+ * [1. DSL like style](#1-dsl-like-style)
21
+ * [2. A regular yield](#2-a-regular-yield)
22
+ * [3. No magic, the regular Page Object pattern way](#3-no-magic-the-regular-page-object-pattern-way)
23
+ * [Component](#component)
24
+ * [Instance methods](#instance-methods)
25
+ * [Declaring elements and subcomponents with class macros](#declaring-elements-and-subcomponents-with-class-macros)
26
+ * [Elements](#elements)
27
+ * [Subcomponents](#subcomponents)
28
+ * [Locating elements and subcomponents](#locating-elements-and-subcomponents)
29
+ * [Query class macro](#query-class-macro)
30
+ * [Element action macros](#element-action-macros-1)
31
+ * [Form helpers](#form-helpers)
32
+ * [Region aka anonymous component](#region-aka-anonymous-component)
33
+ * [ComponentCollection](#componentcollection)
34
+ * [Decoration](#decoration)
35
+
36
+ ### To learn WatirPump by example please refer to [THIS TUTORIAL](https://github.com/bwilczek/watir_pump_tutorial)
37
+
38
+ ## Key features
39
+
40
+ #### DSL to describe pages
41
+
42
+ ```ruby
43
+ class SeachPage < WatirPump::Page
44
+ text_field :query_input, id: 'query'
45
+ button :search_button, id: 'btnG'
46
+ end
47
+ ```
48
+
49
+ Class macro methods (here: `text_field`, `button`) act as a proxy to `watir` element locator methods with same names.
50
+
51
+ #### DSL to interact with pages
52
+
53
+ ```ruby
54
+ SearchPage.open do
55
+ query_input.set 'Watir'
56
+ search_button.click
57
+ end
58
+ ```
59
+
60
+ #### Nestable components
61
+
62
+ ```ruby
63
+ class SubComponent < WatirPump::Component
64
+ # some elements
65
+ end
66
+
67
+ class LoginBox < WatirPump::Component
68
+ component :sub, SubComponent, -> { root.div(class: 'resetPassword') }
69
+ text_field :username, id: 'user'
70
+ text_field :password, id: 'pass'
71
+ button :login, id: 'login'
72
+ end
73
+
74
+ class HomePage < WatirPump::Page
75
+ component login_box, LoginBox, -> { root.div(id: 'login_box') }
76
+
77
+ def do_login(user, pass)
78
+ login_box.username.set user
79
+ login_box.password.set pass
80
+ login_box.login.click
81
+ end
82
+ end
83
+ ```
84
+
85
+ #### Element action macros
86
+
87
+ ```ruby
88
+ class LoginPage < WatirPump::Page
89
+ text_field_writer :username, id: 'user'
90
+ text_field_writer :password, id: 'pass'
91
+ button_clicker :login, id: 'login'
92
+ end
93
+
94
+ LoginPage.open do
95
+ username = 'bob' # same as element.set 'bob'
96
+ password = '$3crEt' # same as element.set '$3crEt'
97
+ login # same as element.click
98
+ end
99
+ ```
100
+
101
+ #### Helpers for forms ####
102
+
103
+ ```ruby
104
+ class NewProductPage < WatirPump::Page
105
+ text_field_writer :name, id: 'name'
106
+ text_field_writer :quantity, id: 'qty'
107
+ button_clicker :submit, id: 'add'
108
+ end
109
+
110
+ class ShowProductPage < WatirPump::Page
111
+ span_reader :name, id: 'name'
112
+ span_reader :quantity, id: 'qty'
113
+ end
114
+
115
+ RSpec.describe 'product creation' do
116
+ let(:data) { { name: 'Hammer XT-431', quantity: 500 } }
117
+
118
+ it 'saves product' do
119
+ NewProductPage.open do
120
+ fill_form(data)
121
+ submit
122
+ end
123
+ ShowProductPage.use do
124
+ expect(form_data).to eq data
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ #### Support for parametrized URLs
131
+
132
+ ```ruby
133
+ class SearchResults < WatirPump::Page
134
+ url '/search/{phrase}'
135
+ divs :results, class: 'result-item'
136
+ end
137
+
138
+ SearchResults.open(phrase: 'watir') do
139
+ expect(results.count).to be > 0
140
+ end
141
+ ```
142
+
143
+ # Examples
144
+
145
+ Imagine a page that contains three ToDo lists. Or maybe instead of imagining just clone this repo and
146
+ open `sinatra_app/public/todos.html` in your browser. This page will serve
147
+ as an example of how one can model and test pages using `WatirPump`.
148
+
149
+ The HTML code representing a single `ToDo` list can look like this:
150
+
151
+ ```html
152
+ <div id="todos_home" role="todo_list">
153
+ <div role="title">Home</div>
154
+ <input role="new_item" /><button role="add">Add</button>
155
+ <ul>
156
+ <li><span role="name">Dishes</span><a role="rm">[rm]</a></li>
157
+ <li><span role="name">Laundry</span><a role="rm">[rm]</a></li>
158
+ <li><span role="name">Vacuum</span><a role="rm">[rm]</a></li>
159
+ </ul>
160
+ </div>
161
+ ```
162
+
163
+ ## Step 1: Just Watir elements
164
+
165
+ For the sake of simplicity let's focus on just one ToDo list for the start.
166
+
167
+ ```ruby
168
+ class ToDosPage < WatirPump::Page
169
+ uri '/todos.html'
170
+ # Watir equivalent: browser.div(role: 'title')
171
+ div :title, role: 'title'
172
+ # similarly:
173
+ text_field :new_item, role: 'new_item'
174
+ button :add, role: 'add'
175
+ lis :items, role: 'name'
176
+ end
177
+
178
+ RSpec.describe ToDosPage do
179
+ let(:browser) { Watir::Browser.new }
180
+ let(:page) { ToDosPage.new(browser).open }
181
+ before(:all) { WatirPump.config.base_url = 'http://localhost:4567' }
182
+
183
+ it 'adds an item to the "Home" ToDo list' do
184
+ page.new_item.set 'Ironing'
185
+ page.add.click
186
+ new_items = items.map { |li| li.span(role: 'name').text }
187
+ expect(new_items).to include('Ironing')
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## Step 2: Make it a component
193
+
194
+ The previous example works fine for a page containing just one ToDo list.
195
+ Let's encapsulate the elements into a [Component](#component), so that it could be reused
196
+ on multiple pages, or even on one page.
197
+
198
+ Components can be nested, and grouped into `ComponentCollections`.
199
+
200
+ Additionally in this iteration [element action macros](#element-action-macros-1) are introduced.
201
+ Instead of generating methods that return `Watir` elements they perform certain actions at once.
202
+
203
+ ```ruby
204
+ class ToDoList < WatirPump::Component
205
+ div_reader :title, role: 'title'
206
+ text_field_writer :new_item, role: 'new_item'
207
+ button_clicker :btn_add, role: 'add'
208
+ components :items, ToDoListItem, :lis
209
+ end
210
+
211
+ class ToDoListItem < WatirPump::Component
212
+ link_clicker :rm, role: 'rm'
213
+ span_reader :name, role: 'name'
214
+ end
215
+
216
+ class ToDosPage < WatirPump::Page
217
+ uri '/todos.html'
218
+ # page contains several ToDo lists (an Array)
219
+ components :todo_lists, ToDoList, :divs, role: 'todo_list'
220
+ end
221
+
222
+ RSpec.describe ToDosPage do
223
+ before(:each) { |example| WatirPump.config.current_example = example }
224
+ before :all do
225
+ WatirPump.configure do |c|
226
+ c.base_url = 'http://localhost:4567'
227
+ c.browser = Watir::Browser.new
228
+ end
229
+ end
230
+
231
+ it 'adds an item to the "Home" ToDo list' do
232
+ # another way of opening and accessing page
233
+ ToDosPage.open do
234
+ home_todo_list = todo_lists.find { |l| l.title == 'Home' }
235
+ home_todo_list.new_item = 'Ironing'
236
+ home_todo_list.btn_add
237
+ new_items = home_todo_list.items.map(&:name)
238
+ expect(new_items).to include('Ironing')
239
+ end
240
+ end
241
+ end
242
+ ```
243
+
244
+ ## Step 3: Make it more elegant and ready for Ajax
245
+
246
+ The new concept introduced here is the [query](#query) class macro.
247
+
248
+ And now the improved example:
249
+
250
+ ```ruby
251
+ # ToDoListItem stays same as before
252
+
253
+ class ToDoList < WatirPump::Component
254
+ div_reader :title, role: 'title'
255
+ text_field_writer :new_item, role: 'new_item'
256
+ button_clicker :btn_add, role: 'add'
257
+ # use array of Watir elements internally
258
+ components :item_elements, ToDoListItem, :lis
259
+ # expose shorter method name to return just array of strings
260
+ query :items, -> { item_elements.map(&:name) }
261
+
262
+ def items_alternative
263
+ # another way to return items, class macro query is just nicer
264
+ item_elements.map(&:name)
265
+ end
266
+
267
+ def add(item)
268
+ cnt_before = item_elements.count
269
+ # mind the self. without it a local variable will be crated
270
+ self.new_item = text
271
+ btn_add
272
+ # assume that the addition is performed over an Ajax call
273
+ Watir::Wait.until { item_elements.count == cnt_before + 1 }
274
+ end
275
+ end
276
+
277
+ class ToDoListCollection < WatirPump::ComponentCollection
278
+ def [](title)
279
+ find { |l| l.title == title }
280
+ end
281
+ end
282
+
283
+ class ToDosPage < WatirPump::Page
284
+ uri '/todos.html'
285
+ # Page will declare itself loaded once todo_lists are present
286
+ query :loaded?, -> { todo_lists.present? }
287
+ components :todo_lists, ToDoList, :divs, role: 'todo_list'
288
+ decorate :todo_lists, ToDoListCollection
289
+ end
290
+
291
+ RSpec.describe ToDosPage do
292
+ # setup omitted for brevity
293
+
294
+ it 'adds an item to the "Home" ToDo list' do
295
+ ToDosPage.open do
296
+ # possible thanks to decoration of todo_lists in ToDosPage
297
+ home_todo_list = todo_lists['Home']
298
+ home_todo_list.add('Ironing')
299
+ expect(home_todo_list.items).to include('Ironing')
300
+ end
301
+ end
302
+ end
303
+ ```
304
+
305
+ # Documentation
306
+
307
+ ## Installation
308
+
309
+ Just like with any other `gem`:
310
+
311
+ Directly:
312
+ ```
313
+ gem install watir_pump
314
+ ```
315
+
316
+ or via `Gemfile` + `bundle install`
317
+ ```
318
+ gem 'watir_pump', '~>0.2'
319
+ ```
320
+
321
+ ## Configuration
322
+
323
+ `WatirPump` includes `ActiveSupport::Configurable` - a popular concept known from `Rails`.
324
+
325
+ The following settings are required to start:
326
+
327
+ ```ruby
328
+ WatirPump.configure do |c|
329
+ # Self explanatory: Watir::Browser instance
330
+ c.browser = Watir::Browser.new
331
+
332
+ # Self explanatory: root URL for the application under test
333
+ c.base_url = 'http://localhost:4567'
334
+
335
+ # Flag defining execution context of blocks passed to Page.use and Page.open
336
+ # See 'Interacting with pages'
337
+ # true - block is evaluated with yield and accepts |page, browser| arguments
338
+ # false - block is evaluated with instance_exec on Page (default)
339
+ c.call_page_blocks_with_yield = false
340
+ end
341
+ ```
342
+
343
+ To make `rspec` work with page DSL the following key has to be set:
344
+
345
+ ```ruby
346
+ before(:each) { |example| WatirPump.config.current_example = example }
347
+ ```
348
+
349
+ ## Page
350
+
351
+ `Page` class definition consists of a list of class macros invocations.
352
+ Most of them are inherited from [Component](#component) class. Few exceptions are:
353
+
354
+ * `uri` - the URL part that is relative to `WatirPump.config.base_url`
355
+ * `loaded?` - predicate returning `true` if page is ready to be interacted with. Default implementation checks if current browser URL matches the `uri`
356
+
357
+ For information about how to declare elements and component for the `Page` please go to [Component](#component) section.
358
+ Internally `Page` itself is a `Component`, that holds other components and Watir elements (components are nestable).
359
+
360
+ ### URI & loaded?
361
+
362
+ Let's consider the following configuration for the examples below:
363
+
364
+ ```ruby
365
+ WatirPump.config.base_url = 'https://myapp.local:8080'
366
+ ```
367
+ #### URI without parameters
368
+
369
+ ```ruby
370
+ class ContactPage
371
+ uri "/contact"
372
+ end
373
+
374
+ ContactPage.open
375
+ # => https://myapp.local:8080/contact
376
+ ```
377
+
378
+ #### URI with a single parameter
379
+
380
+ ```ruby
381
+ class UserPage
382
+ uri "/users{/username}"
383
+ end
384
+
385
+ UserPage.open(username: 'boromir')
386
+ # => https://myapp.local:8080/users/boromir
387
+ ```
388
+
389
+ #### URI with a query string
390
+ ```ruby
391
+ class UserPage
392
+ uri "/search{?query*}"
393
+ end
394
+
395
+ SearchPage.open(query: { phrase: 'watir', offset: 50, limit: 100 })
396
+ # => https://myapp.local:8080/search?phrase=watir&offset=50&limit=100
397
+ ```
398
+
399
+ #### Customized `loaded?` condition
400
+ ```ruby
401
+ class HeavyReactPage
402
+ uri "/spa"
403
+ query :loaded?, -> { root.div(class: 'ajax-fetched-content').visible? }
404
+ end
405
+
406
+ HeavyReactPage.open do
407
+ # 'This will execute once JS renders the element referenced in loaded? method'
408
+ end
409
+ # => https://myapp.local:8080/spa
410
+ ```
411
+
412
+ See [addressable gem](https://github.com/sporkmonger/addressable)
413
+ for more information about the URL template format.
414
+
415
+ ### Interacting with pages
416
+
417
+ Let's consider the following pages (simplified declaration):
418
+
419
+ ```ruby
420
+ class SearchFormPage < WatirPump::Page
421
+ uri '/search'
422
+ text_field :phrase, id: 'q'
423
+ button :search, id: 'btnG'
424
+
425
+ def do_search(query)
426
+ phrase.set query
427
+ search.click
428
+ SearchResultsPage.new(browser).wait_for_loaded
429
+ end
430
+ end
431
+
432
+ class SearchResultsPage < WatirPump::Page
433
+ uri '/results'
434
+ divs :results, class: 'result-item'
435
+ end
436
+ ```
437
+ There are three ways that page objects can be interacted with.
438
+
439
+ #### 1. DSL like style
440
+
441
+ Block is evaluated in scope of the `Page` object.
442
+ Looks nice (no need to type 'page.') but methods visible in the spec
443
+ are not visible in the block. The only exception are the `RSpec` methods.
444
+
445
+ ```ruby
446
+ WatirPump.config.call_page_blocks_with_yield = false # this is default
447
+
448
+ # this is required to make rspec expectations work inside the block
449
+ before(:each) { |example| WatirPump.config.current_example = example }
450
+
451
+ ToDosPage.open do
452
+ phrase.set 'watir'
453
+ search.click
454
+ end
455
+ SearchResultsPage.use do
456
+ expect(results.cnt).to be > 0
457
+ end
458
+ ```
459
+
460
+ **IMPORTANT NOTICE:** This won't work:
461
+ ```ruby
462
+ def search_term
463
+ 'watir'
464
+ end
465
+
466
+ ToDosPage.open do
467
+ phrase.set search_term
468
+ # Error: Method search_term is undefined in this scope.
469
+ search.click
470
+ end
471
+ ```
472
+
473
+ Use rspec's `let` instead:
474
+ ```ruby
475
+ let(:search_term) { 'watir' }
476
+
477
+ ToDosPage.open do
478
+ phrase.set search_term
479
+ # now it works
480
+ search.click
481
+ end
482
+ ```
483
+
484
+ #### 2. A regular yield
485
+
486
+ A regular block. `page` and `browser` references are passed as parameters to the block
487
+
488
+ ```ruby
489
+ WatirPump.config.call_page_blocks_with_yield = true
490
+
491
+ ToDosPage.open do |page, _browser|
492
+ page.phrase.set 'watir'
493
+ page.search.click
494
+ end
495
+ SearchResultsPage.use do |page, browser|
496
+ expect(page.results.cnt).to be > 0
497
+ expect(browser.title) to include 'Results'
498
+ end
499
+ ```
500
+
501
+ #### So how it works internally?
502
+
503
+ Internally `Page.open`/`Page.use` methods uses one of:
504
+ ```ruby
505
+ Page.open_yield Page.use_yield
506
+ Page.open_dsl Page.use_dsl
507
+ ```
508
+ depending on the value of config field `call_page_blocks_with_yield`.
509
+ These methods can be called directly if there is a need to mix the approaches.
510
+
511
+ #### use vs open
512
+
513
+ ```ruby
514
+ MyPage.open { block }
515
+ # browser navigates to page's uri before executing the block
516
+
517
+ MyPage.use { block }
518
+ # block is executed once page is loaded. No browser.goto called internally
519
+ # use has an alias method called act
520
+ ```
521
+
522
+ #### 3. No magic, the regular Page Object pattern way
523
+
524
+ ```ruby
525
+ page = ToDosPage.new(browser)
526
+ page.phrase.set 'watir'
527
+ page.search.click
528
+ page = SearchResultsPage.new(browser)
529
+ expect(page.results.cnt).to be > 0
530
+
531
+ # or more elegantly:
532
+ search_page = ToDosPage.new(browser)
533
+ results_page = search_page.do_search('watir')
534
+ expect(results_page.results.cnt).to be > 0
535
+ ```
536
+
537
+ ## Component
538
+
539
+ Component is the core concept of `WatirPump` page object model definition.
540
+ It provides a set of class macros and regular instance methods that make creation of
541
+ such model easy.
542
+
543
+ ### Instance methods
544
+
545
+ * `browser` - reference to `Watir::Browser` instance
546
+ * `root` (alias: `node`) - reference to `Watir::Element`: component's 'mounting point' inside the DOM tree. (WARNING: for `Pages` it refers to `browser`)
547
+ * `parent` - reference to parent component (`nil` for `Pages`)
548
+
549
+ ### Declaring elements and subcomponents with class macros
550
+
551
+ #### Elements
552
+
553
+ Declaration of simple HTML/Watir elements is easy. Every instance method of [Watir::Container](http://www.rubydoc.info/gems/watir-webdriver/Watir/Container) module
554
+ is exposed to `WatirPump::Component` as a class macro method.
555
+
556
+ Examples:
557
+
558
+ ```ruby
559
+ class MyPage < WatirPump::Page
560
+ link :index, href: /index/
561
+ # equivalent of:
562
+ def index
563
+ browser.link href: /index/
564
+ # more WatirPump like notation would be to use root instead of browser:
565
+ # root.link href: /index/
566
+ end
567
+ # usage: page.index.click
568
+
569
+ button :ok, value: 'OK'
570
+ # equivalent of:
571
+ def ok
572
+ root.button value: 'OK'
573
+ end
574
+ # usage: page.ok.click
575
+
576
+ button :action, ->(val) { root.button(value: val) }
577
+ # equivalent of:
578
+ def action(val)
579
+ root.button(value: val)
580
+ end
581
+ # usage: page.action('Confirm').click
582
+ end
583
+ ```
584
+
585
+ Fore more examples see [Watir guides](http://watir.com/guides/elements/).
586
+
587
+ #### Subcomponents
588
+
589
+ There are two class macros: `component` and `components` that are used to declare a single subcomponent, or a collection.
590
+
591
+ Synopsis:
592
+
593
+ ```
594
+ component :name, ComponentClass, <locator_for_single_node>
595
+ components :name, ComponentClass, <locator_for_multiple_nodes>
596
+ ```
597
+
598
+ Examples:
599
+
600
+ ```ruby
601
+ class LoginBox < WatirPump::Components
602
+ button :login, id: 'btn_login'
603
+ end
604
+
605
+ class MyPage < WatirPump::Page
606
+ component :login_box, LoginBox, :div, id: 'login_box'
607
+ # usage: page.login_box.login.click
608
+
609
+ components :results, SearchResultItem, :divs, class: 'login_box'
610
+ # usage: page.results.count
611
+ end
612
+ ```
613
+
614
+ For other ways of locating elements (using lambdas and parametrized lambdas) see below.
615
+
616
+ #### Others
617
+
618
+ Other macros, like `query`, `region` and `component actions` are documented in the following paragraphs.
619
+
620
+ #### Locating elements and subcomponents
621
+
622
+ There are two ways of defining location of subcomponents within the current component (or page). Both are relative to current component's `root`.
623
+ Location used in declaration of a subcomponent (invocation of `componenet` class macro) will be the `root` of that subcomponent.
624
+
625
+ The parent component reference is accessible through `parent` method.
626
+
627
+ ##### The Watir way
628
+
629
+ For complete list of elements supported this way please see [Watir::Container](http://www.rubydoc.info/gems/watir-webdriver/Watir/Container).
630
+
631
+ Synopsis:
632
+
633
+ ```
634
+ component <name>, <component_class>, <watir_method_name>, <watir_method_params_optionally>
635
+ ```
636
+
637
+ Examples:
638
+
639
+ ```ruby
640
+ # component class LoginBox, instance name login_box, located under root.div(id: 'login_box')
641
+ component :login_box, LoginBox, :div, id: 'login_box'
642
+ # example usage: page.login_box.wait_until_present
643
+
644
+ # component class ArticleParagraph, instance name paragraph, located under root.p
645
+ component :paragraph, ArticleParagraph, :p
646
+ # example usage: page.paragraph.visible?
647
+ ```
648
+
649
+ ##### Lambdas
650
+
651
+ Examples:
652
+
653
+ ```ruby
654
+ # component class LoginBox, instance name login_box, located under root.div(id: 'login_box')
655
+ component :login_box, LoginBox, -> { root.div(id: 'login_box') }
656
+
657
+ # component class ArticleParagraph, instance name paragraph, located under root.p(id: <passed as an argument>)
658
+ component :paragraph, ArticleParagraph, ->(cls) { root.p(id: cls) }
659
+ # example usage: page.paragraph('abstract').text
660
+ ```
661
+
662
+ ##### root vs browser
663
+
664
+ For top level components (pages) both `root.div(class: 'asd')` and `browser.div(class: 'asd')` would work the same.
665
+ This is because `root` of every `Page` is `browser`. For subcomponents however `root` points to node
666
+ which is the mounting point of the component in the DOM tree.
667
+
668
+ Using `root` as a base for locating elements is recommended as a more robust convention.
669
+
670
+ Use `browser` to interact with the browser itself (cookies, navigation, javascript, title, etc.). NOT to navigate DOM.
671
+
672
+ ##### Example
673
+
674
+ Let's consider the following Page structure:
675
+
676
+ ```ruby
677
+ class MyPage < WatirPump::Page
678
+ component :login_box, LoginBox, :div, id: 'login_box'
679
+ end
680
+
681
+ class LoginBox < WatirPump::Component
682
+ component :reset_password, ResetPassword, -> { root.div(class: 'reset-password') }
683
+ end
684
+
685
+ class ResetPassword < WatirPump::Component
686
+ button :send_link, class: 'send-link'
687
+ end
688
+ ```
689
+
690
+ This is how certain elements/components are located:
691
+
692
+ ```ruby
693
+ page = MyPage.new(browser)
694
+ page.root
695
+ # => browser.body
696
+
697
+ page.login_box.root
698
+ # => browser.div(id: 'login_box')
699
+
700
+ page.login_box.reset_password.root
701
+ # => browser.div(id: 'login_box').div(class: 'reset-password')
702
+
703
+ page.login_box.reset_password.parent
704
+ # => page.login_box
705
+
706
+ page.login_box.reset_password.send_link
707
+ # => browser.div(id: 'login_box').div(class: 'reset-password').button(class: 'send-link')
708
+ ```
709
+
710
+ ### `query` class macro
711
+
712
+ It is a shorthand to generate simple methods, usually to query DOM tree with Watir. Examples:
713
+
714
+ ```ruby
715
+ class SamplePage < WatirPump::Page
716
+ spans :items, class: 'search-result'
717
+
718
+ # regular methods
719
+ def items_text
720
+ items.map(&:text)
721
+ end
722
+
723
+ def items_cnt
724
+ items.count
725
+ end
726
+
727
+ def items_with_substring(phrase)
728
+ items_text.select { |item| item.include? phrase }
729
+ end
730
+
731
+ # query class macro equivalent
732
+ query :items_text, -> { items.map(&:text) }
733
+ query :items_cnt, -> { items.count }
734
+ query :items_with_substring ->(phrase) { items_text.select { |item| item.include? phrase } }
735
+
736
+ # more examples: watir methods can be chained
737
+ query :nested_watir_element -> { root.form(id: 'new_item').button(class: 'reset_count') }
738
+ end
739
+ ```
740
+
741
+ As one can see `query` macro is not specific to Watir, it's just a general purpose shorthand to define methods.
742
+
743
+ `query` has two decorated variants:
744
+ * `element` - raises error if value returned from `query` is not a `Watir::Element`
745
+ * `elements` - raises error if value returned from `query` is not a `Watir::ElementCollection`
746
+ One can use them to declare page objects in `watir-drops` style.
747
+
748
+ ### Element action macros
749
+
750
+ There are cases where certain page element is used only to perform one action: either click, write into, or read value.
751
+ In such case it would be more convenient to have a page object method that would perform that action at once, instead of returning the Watir element.
752
+
753
+ Element actions macros are design to do just that.
754
+
755
+ | Declaration in page class | Element action example |
756
+ |------------------------------------------|-------------------------------------|
757
+ | `span :name, id: 'abc'` | `n = page.name.text` |
758
+ | `span_reader :name, id: 'abc'` | `n = page.name` |
759
+ | `link :goto_contacts, id: 'abc'` | `page.goto_contacts.click` |
760
+ | `link_clicker :goto_contacts, id: 'abc'` | `page.goto_contacts` |
761
+ | `text_field :email, id: 'abc'` | `page.email.set 'john@example.com'` |
762
+ | `text_field_writer :email, id: 'abc'` | `page.email = 'john@example.com'` |
763
+
764
+ How it internally works?
765
+
766
+ Macro `span_reader :article_title, id: 'title'` creates two public methods:
767
+
768
+ * `article_title_reader_element` which returns Watir element `:span, id: 'title'`
769
+ * `article_title` which returns `article_title_reader_element.text`
770
+
771
+ **WARNING:** radios, checkboxes and select lists (dropdowns) are handled slightly differently. See below.
772
+
773
+ Macros `*_clicker` and `*_writer` follow the same convention: additional `_(clicker|writer)_element` method is created next to the action method.
774
+
775
+ Full list of tags supported by certain action macros can be found in [WatirPump::Constants](lib/watir_pump/constants.rb).
776
+
777
+ Keep in mind that `writers` cannot rely on element location using parametrized lambda. `field('Employee')="John"` just won't work.
778
+
779
+ In order to create both `reader` and `writer` for the same element one can use `_accessor` macro.
780
+
781
+ #### radio_group, checkbox_group, flag, dropdown_list
782
+
783
+ Radios, checkboxes and selects require special handling because they don't represent a single HTML element, but several of them. For example:
784
+
785
+ ```html
786
+ <fieldset>
787
+ <div>Predicate</div>
788
+ <label>Yes<input type="radio" name="predicate" value="yes" /></label>
789
+ <label>No<input type="radio" name="predicate" value="no" /></label>
790
+ </fieldset>
791
+ <!-- There are two radio buttons that describe values for one form field `predicate`. -->
792
+ ```
793
+
794
+ There's a handful of macros to describe such fields in our page objects:
795
+
796
+ ```ruby
797
+ class UserFormPage < WatirPump::Page
798
+ # input(name: 'gender') matches a collection of radio elements
799
+ radio_reader :gender, name: 'gender'
800
+ radio_writer :gender, name: 'gender'
801
+ radio_accessor :gender, name: 'gender' # alias: radio_group, combined radio_reader and radio_writer
802
+ # page.gender = 'Female' will click the radio button with a corresponding label (NOT value)
803
+ # page.gender will return 'Female'
804
+
805
+ # input(name: 'hobbies[]') matches a collection of checkbox elements
806
+ checkbox_reader :hobbies, name: 'hobbies[]'
807
+ checkbox_writer :hobbies, name: 'hobbies[]'
808
+ checkbox_accessor :hobbies, name: 'hobbies[]' # alias: checkbox_group, combined checkbox_reader and checkbox_writer
809
+ # page.hobbies = 'Yoga' will tick the checkbox with the corresponding label (NOT value)
810
+ # page.hobbies = ['Yoga', 'Music'] sets multiple values
811
+ # page.hobbies will return an array of ticked values
812
+
813
+ # input(name: 'confirmed') matches a single checkbox element
814
+ flag_writer :confirmed, name: 'confirmed'
815
+ flag_reader :confirmed, name: 'confirmed'
816
+ flag_accessor :confirmed, name: 'confirmed' # alias: flag, combined flag_writer and flag_reader
817
+ # page.confirmed = true will tick the checkbox
818
+ # page.confirmed will return a boolean with the `checked` status of the element
819
+ # page.confirmed? - same as above
820
+
821
+ # select(name: 'ingredients[]') matches a select element
822
+ select_reader :ingredients, name: 'ingredients[]'
823
+ select_writer :ingredients, name: 'ingredients[]'
824
+ select_accessor :ingredients, name: 'ingredients[]' # alias: dropdown_list, combined select_reader and select_writer
825
+ # page.ingredients = 'Salt' will select option with a respective label (NOT value)
826
+ # page.ingredients = ['Salt', 'Oregano'] will select multiple options with respective labels, if select is declared as multiple
827
+ # page.ingredients will return a selected option (single or multiple - depending on 'multiple' attribute of the select element)
828
+ end
829
+ ```
830
+
831
+ #### Custom readers and writers
832
+
833
+ Whenever reading or writing value for given form field is more sophisticated than just simple interaction with one HTML element
834
+ `custom_reader` and `custom_writer` come handy. Let's consider that a value for certain field should be an array, and the HTML code
835
+ that represents it looks like this:
836
+
837
+ ```html
838
+ <ul id="hobbies">
839
+ <li>Gardening</li>
840
+ <li>Dancing</li>
841
+ <li>Golf</li>
842
+ </ul>
843
+ ```
844
+
845
+ There are two ways `custom_reader` for this field could be created:
846
+
847
+ ```ruby
848
+ # 1. for one-liners passing a lambda to the class macro invocation will suffice
849
+ custom_reader :hobbies, -> { root.ul(id: 'hobbies')&.lis&.map(&:text) || [] }
850
+
851
+ # 2. for more sophisticated cases use class macro to declare that certain instance method should be treated as a reader
852
+ custom_reader :hobbies
853
+
854
+ def hobbies
855
+ # lots of other code if necessary
856
+ root.ul(id: 'hobbies')&.lis&.map(&:text) || [] }
857
+ end
858
+
859
+ # page.hobbies == ['Gardening', 'Dancing', 'Golf']
860
+ ```
861
+
862
+ Same principles apply for `custom_writer`. Let's rewrite the default `text_field_writer` using `custom_writer` as an example.
863
+
864
+ ```ruby
865
+ # 1. for one-liner use lambda
866
+ custom_writer :first_name, ->(val) { root.text_field(name: 'first_name').set(val) }
867
+
868
+ # 2. for more complex writer logic use a separate method. NOTE the '=' in method name!
869
+ custom_writer :first_name
870
+
871
+ def first_name=(val)
872
+ # do some fancy logic here if necessary
873
+ root.text_field(name: 'first_name').set(val)
874
+ end
875
+ ```
876
+
877
+ ### Form helpers
878
+
879
+ `fill_form(data)` - invokes `writer` method for every key of the `data` hash (or struct), with associated value as a parameter. Example:
880
+
881
+ ```ruby
882
+ fill_form(name: 'Bob', surname: 'Williams', age: 34)
883
+ # is equivalent of
884
+ self.name = 'Bob'
885
+ self.surname = 'Williams'
886
+ self.age = 34
887
+ ```
888
+
889
+ `fill_form!(data)` - invokes `fill_form(data)` and additionally `submit` method if it exists (otherwise it raises an exception).
890
+
891
+ `form_data` - returns a hash of values of all elements that have a `_reader` declared. Example:
892
+
893
+ ```ruby
894
+ class UserFormPage < WatirPump::Page
895
+ span_reader :name, id: 'name'
896
+ span_reader :surname, id: 'surname'
897
+ span_reader :age, id: 'age'
898
+ end
899
+
900
+ UserFormPage.open do
901
+ expect(form_data).to contain_exactly(name: 'Bob', surname: 'Williams', age: 34)
902
+ end
903
+ ```
904
+
905
+ ### Forwarding to root
906
+
907
+ There's a few methods that components forward directly to its root:
908
+
909
+ * visible?
910
+ * present?
911
+ * stale?
912
+ * wait_until_present
913
+ * wait_while_present
914
+ * wait_until
915
+ * wait_while
916
+ * flash
917
+
918
+ Thanks to this one can write just `comp.present?` instead of `comp.root.present?`.
919
+
920
+ ## Region aka anonymous component
921
+
922
+ If certain HTML section appears only on one page (thus there's no point in creating another `Component` class)
923
+ it can be declared in-place, as a region (anonymous component), which will just act as
924
+ a name space in the `Page` object.
925
+
926
+ ```ruby
927
+ class HomePage < WatirPump::Page
928
+ region :login_box, :div, id: 'login_box' do
929
+ text_field :username, id: 'user'
930
+ text_field :password, id: 'pass'
931
+ button :login, id: 'login'
932
+ end
933
+
934
+ def do_login(user, pass)
935
+ login_box.username.set user
936
+ login_box.password.set pass
937
+ login_box.login.click
938
+ end
939
+ end
940
+ ```
941
+
942
+ `region` class macro accepts the following parameters:
943
+
944
+ * name of region
945
+ * root node [locator](#locating-elements-and-subcomponents)
946
+ * block with group of elements/subcomponents
947
+
948
+ ## ComponentCollection
949
+
950
+ `ComponentCollection` is a wrapper for collection of components. For example: a list of search results. See [Subcomponents](#subcomponents) for an example.
951
+
952
+ Basically it's an array, with few extra methods that return true if any of the collection items return true.
953
+
954
+ The example methods are:
955
+
956
+ ```
957
+ visible?
958
+ present?
959
+ wait_until_present
960
+ wait_while_present
961
+ ```
962
+
963
+ The complete list lives in `WatirPump::Constants::METHODS_FORWARDED_TO_ROOT`.
964
+
965
+ ## Decoration
966
+
967
+ _under construction_
968
+
969
+ How it works:
970
+
971
+ ```ruby
972
+ decorate :method_to_decorate, DecoratorClass, AnotherDecoratorClasses
973
+ ```
974
+
975
+ New `method_to_decorate` is created this way (simplified):
976
+
977
+ ```ruby
978
+ def method_to_decorate
979
+ AnotherDecoratorClasses.new(
980
+ DecoratorClass.new(
981
+ old_method_to_decorate
982
+ )
983
+ )
984
+ end
985
+ ```
986
+
987
+ See [this example](#step-3-make-it-more-elegant-and-ready-for-ajax): class `ToDoListCollection` and invocation of `decorate` macro.
988
+
989
+ ```ruby
990
+ # decorator class for component/element collections should extend WatirPump::ComponentCollection
991
+ decorate :todo_lists, ToDoListCollection, DummyDecoratedCollection
992
+
993
+ # decorator class for elements should extend WatirPump::DecoratedElement
994
+ decorate :btn_add, DummyDecoratedElement
995
+ ```