watir_pump 0.4.6 → 0.4.7

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 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
+ ```