-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathpage-object.doc
903 lines (594 loc) · 33.8 KB
/
page-object.doc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
If you are working with WebDriver web tests, you will be familiar with the concept of Page Objects. Page Objects are a way of isolating the implementation details of a web page inside a class, exposing only business-focused methods related to that page. They are an excellent way of making your web tests more maintainable.
In Thucydides, page objects can be just ordinary WebDriver page objects, on the condition that they have a constructor that accepts a WebDriver parameter. However, the Thucydides +PageObject+ class provides a number of utility methods that make page objects more convenient to work with, so a Thucydides Page Object generally extends this class.
Here is a simple example:
---------------------
@DefaultUrl("http://localhost:9000/somepage")
public class FindAJobPage extends PageObject {
WebElement keywords;
WebElement searchButton;
public FindAJobPage(WebDriver driver) {
super(driver);
}
public void look_for_jobs_with_keywords(String values) {
typeInto(keywords, values);
searchButton.click();
}
public List<String> getJobTabs() {
List<WebElement> tabs = getDriver().findElements(By.xpath("//div[@id='tabs']//a"));
return extract(tabs, on(WebElement.class).getText());
}
}
---------------------
The +typeInto+ method is a shorthand that simply clears a field and enters the specified text. If you prefer a more fluent-API style, you can also do something like this:
---------------------
@DefaultUrl("http://localhost:9000/somepage")
public class FindAJobPage extends PageObject {
WebElement keywordsField;
WebElement searchButton;
public FindAJobPage(WebDriver driver) {
super(driver);
}
public void look_for_jobs_with_keywords(String values) {
**enter(values).into(keywordsField);**
searchButton.click();
}
public List<String> getJobTabs() {
List<WebElement> tabs = getDriver().findElements(By.xpath("//div[@id='tabs']//a"));
return extract(tabs, on(WebElement.class).getText());
}
}
---------------------
You can use an even more fluent style of expressing the implementation steps by using methods like +find+, +findBy+ and +then+.
For example, you can use webdriver "By" finders with element name, id, css selector or xpath selector as follows:
---------------------
page.find(By.name("demo")).then(By.name("specialField")).getValue();
page.find(By.cssSelector(".foo")).getValue();
page.find(By.xpath("//th")).getValue();
---------------------
You can also use +findBy+ method and pass the css or xpath selector directly. For example,
---------------------
page.findBy("#demo").then("#specialField").getValue(); //css selectors
page.findBy("//div[@id='dataTable']").getValue(); //xpath selector
---------------------
=== Using pages in a step library
When you need to use a page object in one of your steps, you just ask for one from the Page factory, providing the class of the page object you need, e.g.
---------------------
FindAJobPage page = getPages().get(FindAJobPage.class);
---------------------
If you want to make sure you are on the right page, you can use the +currentPageAt()+ method. This will check the page class for any +@At+ annotations present in the Page Object class and, if present, check that the current URL corresponds to the URL pattern specified in the annotation. For example, when you invoke it using +currentPageAt()+, the following Page Object will check that the current URL is precisely http://www.apache.org.
---------------------
@At("http://www.apache.org")
public class ApacheHomePage extends PageObject {
...
}
---------------------
The +@At+ annotation also supports wildcards and regular expressions. The following page object will match any Apache sub-domain:
---------------------
@At("http://.*.apache.org")
public class AnyApachePage extends PageObject {
...
}
---------------------
More generally, however, you are more interested in what comes after the host name. You can use the special +#HOST+ token to match any server name. So the following Page Object will match both http://localhost:8080/app/action/login.form an http://staging.acme.com/app/action/login.form. It will also ignore parameters, so http://staging.acme.com/app/action/login.form?username=toto&password=oz will work fine too.
---------------------
@At(urls={"#HOST/app/action/login.form"})
public class LoginPage extends PageObject {
...
}
---------------------
=== Opening the page
A page object is usually designed to work with a particular web page. When the +open()+ method is invoked, the browser will be opened to the default URL for the page.
The +@DefaultUrl+ annotation indicates the URL that this test should use when run in isolation (e.g. from within your IDE). Generally, however, the host part of the default URL will be overridden by the +webdriver.base.url+ property, as this allows you to set the base URL across the board for all of your tests, and so makes it easier to run your tests on different environments by simply changing this property value. For example, in the test class above, setting the +webdriver.base.url+ to 'https://staging.mycompany.com' would result in the page being opened at the URL of 'https://staging.mycompany.com/somepage'.
You can also define named URLs that can be used to open the web page, optionally with parameters. For example, in the following code, we define a URL called 'open.issue', that accepts a single parameter:
---------------------
@DefaultUrl("http://jira.mycompany.org")
@NamedUrls(
{
@NamedUrl(name = "open.issue", url = "http://jira.mycompany.org/issues/{1}")
}
)
public class JiraIssuePage extends PageObject {
...
}
---------------------
You could then open this page to the http://jira.mycompany.org/issues/ISSUE-1 URL as shown here:
---------------------
page.open("open.issue", withParameters("ISSUE-1"));
---------------------
You could also dispense entirely with the base URL in the named URL definition, and rely on the default values:
---------------------
@DefaultUrl("http://jira.mycompany.org")
@NamedUrls(
{
@NamedUrl(name = "open.issue", url = "/issues/{1}")
}
)
public class JiraIssuePage extends PageObject {
...
}
---------------------
And naturally you can define more than one definition:
---------------------
@NamedUrls(
{
@NamedUrl(name = "open.issue", url = "/issues/{1}"),
@NamedUrl(name = "close.issue", url = "/issues/close/{1}")
}
)
---------------------
You should never try to implement the +open()+ method yourself. In fact, it is final. If you need your page to do something upon loading, such as waiting for a dynamic element to appear, you can use the @WhenPageOpens annotation. Methods in the PageObject with this annotation will be invoked (in an unspecified order) after the URL has been opened. In this example, the +open()+ method will not return until the dataSection web element is visible:
---------------------
@DefaultUrl("http://localhost:8080/client/list")
public class ClientList extends PageObject {
@FindBy(id="data-section");
WebElement dataSection;
...
@WhenPageOpens
public void waitUntilTitleAppears() {
element(dataSection).waitUntilVisible();
}
}
---------------------
=== Working with web elements
.Important
************************************************
The element() method described below is no longer
needed. See <<webelementfacade, Web Element Facade section>> for details.
************************************************
==== Checking whether elements are visible
The element method of the PageObject class provides a convenient fluent API for dealing with web elements, providing some commonly-used extra features that are not provided out-of-the-box by the WebDriver API. For example, you can check that an element is visible as shown here:
---------------------
public class FindAJobPage extends PageObject {
WebElement searchButton;
public boolean searchButtonIsVisible() {
return element(searchButton).isVisible();
}
...
}
---------------------
If the button is not present on the screen, the test will wait for a short period in case it appears due to some Ajax magic. If you don't want the test to do this, you can use the faster version:
---------------------
public boolean searchButtonIsVisibleNow() {
return element(searchButton).isCurrentlyVisible();
}
---------------------
You can turn this into an assert by using the +shouldBeVisible()+ method instead:
---------------------
public void checkThatSearchButtonIsVisible() {
element(searchButton).shouldBeVisible();
}
---------------------
This method will through an assertion error if the search button is not visible to the end user.
If you are happy to expose the fact that your page has a search button to your step methods, you can make things even simpler by adding an accessor method that returns a WebElementFacade, as shown here:
---------------------
public WebElementFacade searchButton() {
return element(searchButton);
}
---------------------
Then your steps will contain code like the following:
---------------------
searchPage.searchButton().shouldBeVisible();
---------------------
==== Checking whether elements are enabled
You can also check whether an element is enabled or not:
---------------------
element(searchButton).isEnabled() element(searchButton).shouldBeEnabled()
---------------------
There are also equivalent negative methods:
---------------------
element(searchButton).shouldNotBeVisible();
element(searchButton).shouldNotBeCurrentlyVisible();
element(searchButton).shouldNotBeEnabled()
---------------------
You can also check for elements that are present on the page but not visible, e.g:
---------------------
element(searchButton).isPresent();
element(searchButton).isNotPresent();
element(searchButton).shouldBePresent();
element(searchButton).shouldNotBePresent();
---------------------
==== Manipulating select lists
There are also helper methods available for drop-down lists. Suppose you have the following dropdown on your page:
---------------------
<select id="color">
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
---------------------
You could write a page object to manipulate this dropdown as shown here:
---------------------
public class FindAJobPage extends PageObject {
@FindBy(id="color")
WebElement colorDropdown;
public selectDropdownValues() {
element(colorDropdown).selectByVisibleText("Blue");
assertThat(element(colorDropdown).getSelectedVisibleTextValue(), is("Blue"));
element(colorDropdown).selectByValue("blue");
assertThat(element(colorDropdown).getSelectedValue(), is("blue"));
page.element(colorDropdown).selectByIndex(2);
assertThat(element(colorDropdown).getSelectedValue(), is("green"));
}
...
}
---------------------
==== Determining focus
You can determine whether a given field has the focus as follows:
---------------------
element(firstName).hasFocus()
---------------------
You can also wait for elements to appear, disappear, or become enabled or disabled:
---------------------
element(button).waitUntilEnabled()
element(button).waitUntilDisabled()
---------------------
or
---------------------
element(field).waitUntilVisible()
element(button).waitUntilNotVisible()
---------------------
[[webelementfacade]]
==== Using WebElementFacade variables directly
Instead of declaring WebElement variables in Page Objects and then calling element() or $() to wrap them in WebElementFacades, you can now declare WebElementFacade variables directly inside the Page Objects. This will make the Page Object code simpler more readable.
So, instead of writing,
---------------------
public class FindAJobPage extends PageObject {
WebElement searchButton;
public boolean searchButtonIsVisible() {
return element(searchButton).isVisible();
}
...
}
---------------------
you can write,
---------------------
public class FindAJobPage extends PageObject {
WebElementFacade searchButton;
public boolean searchButtonIsVisible() {
return searchButton.isVisible();
}
...
}
---------------------
==== Using direct XPath and CSS selectors
Another way to access a web element is to use an XPath or CSS expression. You can use the +element+ method with an XPath expression to do this more simply. For example, imagine your web application needs to click on a list item containing a given post code. One way would be as shown here:
---------------------
WebElement selectedSuburb = getDriver().findElement(By.xpath("//li/a[contains(.,'" + postcode + "')]"));
selectedSuburb.click();
---------------------
However, a simpler option would be to do this:
----------------------
element(By.xpath("//li/a[contains(.,'" + postcode + "')]")).click();
----------------------
=== Working with Asynchronous Pages
Asynchronous pages are those whose fields or data is not all displayed when the page is loaded. Sometimes, you need to wait for certain elements to appear, or to disappear, before being able to proceed with your tests. Thucydides provides some handy methods in the PageObject base class to help with these scenarios. They are primarily designed to be used as part of your business methods in your page objects, though in the examples we will show them used as external calls on a PageObject instance for clarity.
==== Checking whether an element is visible
In WebDriver terms, there is a distinction between when an element is present on the screen (i.e. in the HTML source code), and when it is rendered (i.e. visible to the user). You may also need to check whether an element is visible on the screen. You can do this in two ways. Your first option is to use the isElementVisible method, which returns a boolean value based on whether the element is rendered (visible to the user) or not:
----------------------
assertThat(indexPage.isElementVisible(By.xpath("//h2[.='A visible title']")), is(true));
-----------------------
or
----------------------
assertThat(indexPage.isElementVisible(By.xpath("//h2[.='An invisible title']")), is(false));
----------------------
Your second option is to actively assert that the element should be visible:
----------------------
indexPage.shouldBeVisible(By.xpath("//h2[.='An invisible title']");
----------------------
If the element does not appear immediately, you can wait for it to appear:
----------------------
indexPage.waitForRenderedElements(By.xpath("//h2[.='A title that is not immediately visible']"));
----------------------
An alternative to the above syntax is to use the more fluid +waitFor+ method which takes a css or xpath selector as argument:
---------------------
indexPage.waitFor("#popup"); //css selector
indexPage.waitFor("//h2[.='A title that is not immediately visible']"); //xpath selector
---------------------
If you just want to check if the element is present though not necessarily visible, you can use +waitForRenderedElementsToBePresent+ :
----------------------
indexPage.waitForRenderedElementsToBePresent(By.xpath("//h2[.='A title that is not immediately visible']"));
----------------------
or its more expressive flavour, +waitForPresenceOf+ which takes a css or xpath selector as argument.
---------------------
indexPage.waitForPresenceOf("#popup"); //css
indexPage.waitForPresenceOf("//h2[.='A title that is not immediately visible']"); //xpath
---------------------
You can also wait for an element to disappear by using +waitForRenderedElementsToDisappear+ or +waitForAbsenceOf+ :
----------------------
indexPage.waitForRenderedElementsToDisappear(By.xpath("//h2[.='A title that will soon disappear']"));
indexPage.waitForAbsenceOf("#popup");
indexPage.waitForAbsenceOf("//h2[.='A title that will soon disappear']");
----------------------
For simplicity, you can also use the +waitForTextToAppear+ and +waitForTextToDisappear+ methods:
----------------------
indexPage.waitForTextToDisappear("A visible bit of text");
----------------------
If several possible texts may appear, you can use +waitForAnyTextToAppear+ or +waitForAllTextToAppear+:
----------------------
indexPage.waitForAnyTextToAppear("this might appear","or this", "or even this");
---------------------
If you need to wait for one of several possible elements to appear, you can also use the +waitForAnyRenderedElementOf+ method:
----------------------
indexPage.waitForAnyRenderedElementOf(By.id("color"), By.id("taste"), By.id("sound"));
----------------------
=== Executing Javascript
There are times when you may find it useful to execute a little Javascript directly within the browser to get the job done. You can use the +evaluateJavascript()+ method of the +PageObject+ class to do this. For example, you might need to evaluate an expression and use the result in your tests. The following command will evaluate the document title and return it to the calling Java code:
---------------------
String result = (String) evaluateJavascript("return document.title");
---------------------
Alternatively, you may just want to execute a Javascript command locally in the browser. In the following code, for example, we set the focus to the 'firstname' input field:
---------------------
evaluateJavascript("document.getElementById('firstname').focus()");
---------------------
And, if you are familiar with JQuery, you can also invoke JQuery expressions:
---------------------
evaluateJavascript("$('#firstname').focus()");
---------------------
This is often a useful strategy if you need to trigger events such as mouse-overs that are not currently supported by the WebDriver API.
=== Uploading files
Uploading files is easy. Files to be uploaded can be either placed in a hard-coded location (bad) or stored on the classpath (better). Here is a simple example:
---------------------
public class NewCompanyPage extends PageObject {
...
@FindBy(id="object_logo")
WebElement logoField;
public NewCompanyPage(WebDriver driver) {
super(driver);
}
public void loadLogoFrom(String filename) {
upload(filename).to(logoField);
}
}
---------------------
=== Using Fluent Matcher expressions
When writing acceptance tests, you often find yourself expressing expectations about individual domain objects or collections of domain objects. For example, if you are testing a multi-criteria search feature, you will want to know that the application finds the records you expected. You might be able to do this in a very precise manner (for example, knowing exactly what field values you expect), or you might want to make your tests more flexible by expressing the ranges of values that would be acceptable. Thucydides provides a few features that make it easier to write acceptance tests for this sort of case.
In the rest of this section, we will study some examples based on tests for the Maven Central search site (see <<maven-search-report>>). This site lets you search the Maven repository for Maven artifacts, and view the details of a particular artifact.
[[maven-search-report]]
.The results page for the Maven Central search page
image::figs/maven-search-report.png[scaledwidth="80%", width=475]
We will use some imaginary regression tests for this site to illustrate how the Thucydides matchers can be used to write more expressive tests. The first scenario we will consider is simply searching for an artifact by name, and making sure that only artifacts matching this name appear in the results list. We might express this acceptance criteria informally in the following way:
* Give that the developer is on the search page,
* And the developer searches for artifacts called 'Thucydides'
* Then the developer should see at least 16 Thucydides artifacts, each with a unique artifact Id
In JUnit, a Thucydides test for this scenario might look like the one:
--------------
...
import static net.thucydides.core.matchers.BeanMatchers.the_count;
import static net.thucydides.core.matchers.BeanMatchers.each;
import static net.thucydides.core.matchers.BeanMatchers.the;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
@RunWith(ThucydidesRunner.class)
public class WhenSearchingForArtifacts {
@Managed
WebDriver driver;
@ManagedPages(defaultUrl = "http://search.maven.org")
public Pages pages;
@Steps
public DeveloperSteps developer;
@Test
public void should_find_the_right_number_of_artifacts() {
developer.opens_the_search_page();
developer.searches_for("Thucydides");
developer.should_see_artifacts_where(the("GroupId", startsWith("net.thucydides")),
each("ArtifactId").isDifferent(),
the_count(is(greaterThanOrEqualTo(16))));
}
}
--------------
Let's see how the test in this class is implemented. The +should_find_the_right_number_of_artifacts()+ test could be expressed as follows:
. When we open the search page
. And we search for artifacts containing the word 'Thucydides'
. Then we should see a list of artifacts where each Group ID starts with "net.thucydides", each Artifact ID is unique, and that there are at least 16 such entries displayed.
The implementation of these steps is illustrated here:
-----------
...
import static net.thucydides.core.matchers.BeanMatcherAsserts.shouldMatch;
public class DeveloperSteps extends ScenarioSteps {
public DeveloperSteps(Pages pages) {
super(pages);
}
@Step
public void opens_the_search_page() {
onSearchPage().open();
}
@Step
public void searches_for(String search_terms) {
onSearchPage().enter_search_terms(search_terms);
onSearchPage().starts_search();
}
@Step
public void should_see_artifacts_where(BeanMatcher... matchers) {
shouldMatch(onSearchResultsPage().getSearchResults(), matchers);
}
private SearchPage onSearchPage() {
return getPages().get(SearchPage.class);
}
private SearchResultsPage onSearchResultsPage() {
return getPages().get(SearchResultsPage.class);
}
}
-----------
The first two steps are implemented by relatively simple methods. However the third step is more interesting. Let's look at it more closely:
-----------
@Step
public void should_see_artifacts_where(BeanMatcher... matchers) {
shouldMatch(onSearchResultsPage().getSearchResults(), matchers);
}
-----------
Here, we are passing an arbitrary number of expressions into the method. These expressions actually 'matchers', instances of the BeanMatcher class. Not that you usually have to worry about that level of detail - you create these matcher expressions using a set of static methods provided in the BeanMatchers class. So you typically would pass fairly readable expressions like +the("GroupId", startsWith("net.thucydides"))+ or +each("ArtifactId").isDifferent()+.
The +shouldMatch()+ method from the BeanMatcherAsserts class takes either a single Java object, or a collection of Java objects, and checks that at least some of the objects match the constraints specified by the matchers. In the context of web testing, these objects are typically POJOs provided by the Page Object to represent the domain object or objects displayed on a screen.
There are a number of different matcher expressions to choose from. The most commonly used matcher just checks the value of a field in an object. For example, suppose you are using the domain object shown here:
-----------
public class Person {
private final String firstName;
private final String lastName;
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {...}
public String getLastName() {...}
}
-----------
You could write a test to ensure that a list of Persons contained at least one person named "Bill" by using the "the" static method, as shown here:
-----------
List<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));
shouldMatch(persons, the("firstName", is("Bill"))
-----------
The second parameter in the the() method is a Hamcrest matcher, which gives you a great deal of flexibility with your expressions. For example, you could also write the following:
-----------
List<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));
shouldMatch(persons, the("firstName", is(not("Tim"))));
shouldMatch(persons, the("firstName", startsWith("B")));
-----------
You can also pass in multiple conditions:
-----------
List<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));
shouldMatch(persons, the("firstName", is("Bill"), the("lastName", is("Oddie"));
-----------
Thucydides also provides the DateMatchers class, which lets you apply Hamcrest matches to standard java Dates and +JodaTime+ DateTimes. The following code samples illustrate how these might be used:
-----------
DateTime january1st2010 = new DateTime(2010,01,01,12,0).toDate();
DateTime may31st2010 = new DateTime(2010,05,31,12,0).toDate();
the("purchaseDate", isBefore(january1st2010))
the("purchaseDate", isAfter(january1st2010))
the("purchaseDate", isSameAs(january1st2010))
the("purchaseDate", isBetween(january1st2010, may31st2010))
-----------
You sometimes also need to check constraints that apply to all of the elements under consideration. The simplest of these is to check that all of the field values for a particular field are unique. You can do this using the +each()+ method:
-----------
shouldMatch(persons, each("lastName").isDifferent())
-----------
You can also check that the number of matching elements corresponds to what you are expecting. For example, to check that there is only one person who's first name is Bill, you could do this:
-----------
shouldMatch(persons, the("firstName", is("Bill"), the_count(is(1)));
-----------
You can also check the minimum and maximum values using the max() and min() methods. For example, if the Person class had a +getAge()+ method, we could ensure that every person is over 21 and under 65 by doing the following:
-----------
shouldMatch(persons, min("age", greaterThanOrEqualTo(21)),
max("age", lessThanOrEqualTo(65)));
-----------
These methods work with normal Java objects, but also with Maps. So the following code will also work:
-----------
Map<String, String> person = new HashMap<String, String>();
person.put("firstName", "Bill");
person.put("lastName", "Oddie");
List<Map<String,String>> persons = Arrays.asList(person);
shouldMatch(persons, the("firstName", is("Bill"))
-----------
The other nice thing about this approach is that the matchers play nicely with the Thucydides reports. So when you use the BeanMatcher class as a parameter in your test steps, the conditions expressed in the step will be displayed in the test report, as shown in <<fig-maven-search-report>>.
[[fig-maven-search-report]]
.Conditional expressions are displayed in the test reports
image::figs/maven-search-report.png[scaledwidth="80%", width=475]
There are two common usage patterns when building Page Objects and steps that use this sort of matcher. The first is to write a Page Object method that returns the list of domain objects (for example, Persons) displayed on the table. For example, the getSearchResults() method used in the should_see_artifacts_where() step could be implemented as follows:
-------------
public List<Artifact> getSearchResults() {
List<WebElement> rows = resultTable.findElements(By.xpath(".//tr[td]"));
List<Artifact> artifacts = new ArrayList<Artifact>();
for (WebElement row : rows) {
List<WebElement> cells = row.findElements(By.tagName("td"));
artifacts.add(new Artifact(cells.get(0).getText(),
cells.get(1).getText(),
cells.get(2).getText()));
}
return artifacts;
}
-------------
The second is to access the HTML table contents directly, without explicitly modelling the data contained in the table. This approach is faster and more effective if you don't expect to reuse the domain object in other pages. We will see how to do this next.
==== Working with HTML Tables
Since HTML tables are still widely used to represent sets of data on web applications, Thucydides comes the HtmlTable class, which provides a number of useful methods that make it easier to write Page Objects that contain tables. For example, the rowsFrom method returns the contents of an HTML table as a list of Maps, where each map contains the cell values for a row indexed by the corresponding heading, as shown here:
-------------
...
import static net.thucydides.core.pages.components.HtmlTable.rowsFrom;
public class SearchResultsPage extends PageObject {
WebElement resultTable;
public SearchResultsPage(WebDriver driver) {
super(driver);
}
public List<Map<String, String>> getSearchResults() {
return rowsFrom(resultTable);
}
}
-------------
This saves a lot of typing - our +getSearchResults()+ method now looks like this:
---------
public List<Map<String, String>> getSearchResults() {
return rowsFrom(resultTable);
}
---------
And since the Thucydides matchers work with both Java objects and Maps, the matcher expressions will be very similar. The only difference is that the Maps returned are indexed by the text values contained in the table headings, rather than by java-friendly property names.
You can also read tables without headers (i.e., <th> elements) by specifying your own headings using the +withColumns+ method. For example:
---------
List<Map<Object, String>> tableRows =
HtmlTable.withColumns("First Name","Last Name", "Favorite Colour")
.readRowsFrom(page.table_with_no_headings);
---------
You can also use the HtmlTable class to select particular rows within a table to work with. For example, another test scenario for the Maven Search page involves clicking on an artifact and displaying the details for that artifact. The test for this might look something like this:
-------------
@Test
public void clicking_on_artifact_should_display_details_page() {
developer.opens_the_search_page();
developer.searches_for("Thucydides");
developer.open_artifact_where(the("ArtifactId", is("thucydides")),
the("GroupId", is("net.thucydides")));
developer.should_see_artifact_details_where(the("artifactId", is("thucydides")),
the("groupId", is("net.thucydides")));
}
-------------
Now the open_artifact_where() method needs to click on a particular row in the table. This step looks like this:
-------------
@Step
public void open_artifact_where(BeanMatcher... matchers) {
onSearchResultsPage().clickOnFirstRowMatching(matchers);
}
-------------
So we are effectively delegating to the Page Object, who does the real work. The corresponding Page Object method looks like this:
-------------
import static net.thucydides.core.pages.components.HtmlTable.filterRows;
...
public void clickOnFirstRowMatching(BeanMatcher... matchers) {
List<WebElement> matchingRows = filterRows(resultTable, matchers);
WebElement targetRow = matchingRows.get(0);
WebElement detailsLink = targetRow.findElement(By.xpath(".//a[contains(@href,'artifactdetails')]"));
detailsLink.click();
}
-------------
The interesting part here is the first line of the method, where we use the filterRows() method. This method will return a list of WebElements that match the matchers you have passed in. This method makes it fairly easy to select the rows you are interested in for special treatment.
=== Running several steps using the same page object
Sometimes, querying the browser can be expensive. For example, if you are testing tables with large numbers of web elements (e.g. a web element for each cell), performance can be slow, and memory usage high. Normally, Thucydides will requery the page (and create a new Page Object) each time you call +Pages.get()+ or +Pages.currentPageAt()+. If you are certain that the page will not change (i.e., that you are only performing read-only operations on the page), you can use the onSamePage() method of the ScenarioSteps class to ensure that subsequent calls to +Pages.get()+ or +Pages.currentPageAt()+ will return the same page object:
------------------
@RunWith(ThucydidesRunner.class)
public class WhenDisplayingTableContents {
@Managed
public WebDriver webdriver;
@ManagedPages(defaultUrl = "http://my.web.site/index.html")
public Pages pages;
@Steps
public DemoSiteSteps steps;
@Test
public void the_user_opens_another_page() {
steps.navigate_to_page_with_a_large_table();
steps.onSamePage(DemoSiteSteps.class).check_row(1);
steps.onSamePage(DemoSiteSteps.class).check_row(2);
steps.onSamePage(DemoSiteSteps.class).check_row(3);
}
}
---------------------
=== Switching to another page
A method, switchToPage() is provided in PageObject class to make it convenient to return a new PageObject after navigation from within a method of a PageObject class. For example,
------------------
@DefaultUrl("http://mail.acme.com/login.html")
public class EmailLoginPage extends PageObject {
...
public void forgotPassword() {
...
forgotPassword.click();
ForgotPasswordPage forgotPasswordPage = this.switchToPage(ForgotPasswordPage.class);
forgotPasswordPage.open();
...
}
...
}
------------------