Skip to content

Commit 5d42566

Browse files
authored
Add alert modeling (#294)
1 parent 018533d commit 5d42566

File tree

8 files changed

+500
-22
lines changed

8 files changed

+500
-22
lines changed

src/main/java/com/nordstrom/automation/selenium/examples/ExamplePage.java

Lines changed: 178 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.net.URL;
77
import java.util.List;
88
import java.util.Map;
9+
import java.util.Optional;
910
import java.util.stream.Collectors;
1011

1112
import org.openqa.selenium.By;
@@ -17,6 +18,8 @@
1718
import com.nordstrom.automation.selenium.annotations.PageUrl;
1819
import com.nordstrom.automation.selenium.core.ByType;
1920
import com.nordstrom.automation.selenium.core.JsUtility;
21+
import com.nordstrom.automation.selenium.interfaces.DetectsLoadCompletion;
22+
import com.nordstrom.automation.selenium.model.AlertHandler;
2023
import com.nordstrom.automation.selenium.model.Page;
2124
import com.nordstrom.automation.selenium.model.RobustWebElement;
2225
import com.nordstrom.automation.selenium.servlet.ExamplePageLauncher;
@@ -26,12 +29,13 @@
2629
* This class is the model for the 'Example' page used by Selenium Foundation unit tests.
2730
*/
2831
@PageUrl("/grid/admin/ExamplePageServlet")
29-
public class ExamplePage extends Page {
32+
public class ExamplePage extends Page implements DetectsLoadCompletion {
3033

3134
/** page title */
3235
public static final String TITLE = "Example Page";
3336
/** text content of example paragraphs collection */
34-
public static final String[] PARAS = {"This is paragraph one.", "This is paragraph two.", "This is paragraph three."};
37+
public static final String[] PARAS =
38+
{"This is paragraph one.", "This is paragraph two.", "This is paragraph three.", /* hidden paragraph */ ""};
3539
/** text content of example table headers collection */
3640
public static final String[] HEADINGS = {"Firstname", "Lastname", "Age"};
3741
/** text content of example table rows collection */
@@ -58,6 +62,7 @@ public class ExamplePage extends Page {
5862
*/
5963
public ExamplePage(WebDriver driver) {
6064
super(driver);
65+
alertHandler = new ExampleAlertHandler(this);
6166
}
6267

6368
private FrameComponent frameByLocator;
@@ -74,7 +79,9 @@ public ExamplePage(WebDriver driver) {
7479
private ShadowRootComponent shadowRootByElement;
7580
private List<ShadowRootComponent> shadowRootList;
7681
private Map<Object, ShadowRootComponent> shadowRootMap;
82+
private final AlertHandler alertHandler;
7783
private int refreshCount;
84+
private boolean isLoaded;
7885

7986
/** identifier for frame 'A' */
8087
protected static final String FRAME_A_ID = "frame-a";
@@ -116,7 +123,15 @@ protected enum Using implements ByEnum {
116123
/** shadow root element 'B' */
117124
SHADOW_ROOT_B(By.cssSelector("div#shadow-root-b")),
118125
/** division element */
119-
FORM_DIV(By.cssSelector("div#form-div"));
126+
FORM_DIV(By.cssSelector("div#form-div")),
127+
/** alert button */
128+
ALERT(By.cssSelector("button#alert")),
129+
/** confirm button */
130+
CONFIRM(By.cssSelector("button#confirm")),
131+
/** prompt button */
132+
PROMPT(By.cssSelector("button#prompt")),
133+
/** result paragraph */
134+
RESULT(By.cssSelector("p#result"));
120135

121136
private final By locator;
122137

@@ -130,6 +145,39 @@ public By locator() {
130145
}
131146
}
132147

148+
/**
149+
* This enumeration defined alert type constants.
150+
*/
151+
public enum ModalType {
152+
/** 'alert' modal */
153+
ALERT("I am a JS Alert"),
154+
/** 'confirm' modal */
155+
CONFIRM("I am a JS Confirm"),
156+
/** 'prompt' modal */
157+
PROMPT("I am a JS Prompt");
158+
159+
private String text;
160+
161+
ModalType(String text) {
162+
this.text = text;
163+
}
164+
165+
/**
166+
* Convert the specified modal text to the corresponding constant.
167+
*
168+
* @param text modal text
169+
* @return modal type constant
170+
* @throws IllegalArgumentException if specified text is unrecognized
171+
*/
172+
public static ModalType fromString(String text) {
173+
if (text == null) return null;
174+
for (ModalType type : values()) {
175+
if (type.text.equals(text)) return type;
176+
}
177+
throw new IllegalArgumentException("Unrecognized modal text: " + text);
178+
}
179+
}
180+
133181
/**
134182
* Get the automation component that models frame 'A' via the associated element locator.
135183
*
@@ -436,7 +484,74 @@ public boolean hasXpathOptional() {
436484
public boolean hasBogusOptional() {
437485
return findOptional(By.tagName("BOGUS")).hasReference();
438486
}
487+
488+
/**
489+
* Get the type of the browser modal.
490+
*
491+
* @return {@link ModalType} representing the current modal; {@code null} if modal is not shown
492+
*/
493+
public ModalType getModalType() {
494+
return ModalType.fromString(alertHandler.getText());
495+
}
496+
497+
/**
498+
* Open the {@link ModalType#ALERT ALERT} modal.
499+
*/
500+
public void openAlertModal() {
501+
findElement(Using.ALERT).click();
502+
}
439503

504+
/**
505+
* Open the {@link ModalType#CONFIRM CONFIRM} modal.
506+
*/
507+
public void openConfirmModal() {
508+
findElement(Using.CONFIRM).click();
509+
}
510+
511+
/**
512+
* Open the {@link ModalType#PROMPT PROMPT} modal.
513+
*/
514+
public void openPromptModal() {
515+
findElement(Using.PROMPT).click();
516+
}
517+
518+
/**
519+
* Accept the browser modal.
520+
*
521+
* @return landing page object
522+
*/
523+
public Page acceptModal() {
524+
return alertHandler.accept();
525+
}
526+
527+
/**
528+
* Send the specified keys to the browser modal and accept it.
529+
*
530+
* @param keys keys to send
531+
* @return landing page object
532+
*/
533+
public Page sendKeysAndAccept(String keys) {
534+
return alertHandler.sendKeysAndAccept(keys);
535+
}
536+
537+
/**
538+
* Dismiss the browser modal.
539+
*
540+
* @return landing page object
541+
*/
542+
public Page dismissModal() {
543+
return alertHandler.dismiss();
544+
}
545+
546+
/**
547+
* Get the modal result text.
548+
*
549+
* @return modal result text
550+
*/
551+
public String getModalResult() {
552+
return findElement(Using.RESULT).getText();
553+
}
554+
440555
/**
441556
* Set the active Grid hub as the base URI for all relative loads of test pages.
442557
*
@@ -464,5 +579,65 @@ public static URI setHubAsTarget() {
464579
}
465580
return targetUri;
466581
}
582+
583+
/**
584+
* {@inheritDoc}
585+
*/
586+
@Override
587+
public boolean isLoadComplete() {
588+
if (!isLoaded) {
589+
isLoaded = true;
590+
return false;
591+
}
592+
return true;
593+
}
467594

595+
/**
596+
* This class models the browser alerts of the example page.
597+
*/
598+
private static class ExampleAlertHandler extends AlertHandler {
599+
public ExampleAlertHandler(Page parentPage) {
600+
super(parentPage);
601+
}
602+
603+
/**
604+
* {@inheritDoc}
605+
*/
606+
@Override
607+
public ExamplePage accept() {
608+
return Optional.ofNullable(waitForAlert())
609+
.map(alert -> {
610+
alert.accept();
611+
return new ExamplePage(driver);
612+
})
613+
.orElse((ExamplePage) parentPage);
614+
}
615+
616+
/**
617+
* {@inheritDoc}
618+
*/
619+
@Override
620+
public ExamplePage sendKeysAndAccept(final String keys) {
621+
return Optional.ofNullable(waitForAlert())
622+
.map(alert -> {
623+
alert.sendKeys(keys);
624+
alert.accept();
625+
return new ExamplePage(driver);
626+
})
627+
.orElse((ExamplePage) parentPage);
628+
}
629+
630+
/**
631+
* {@inheritDoc}
632+
*/
633+
@Override
634+
public ExamplePage dismiss() {
635+
return Optional.ofNullable(waitForAlert())
636+
.map(alert -> {
637+
alert.dismiss();
638+
return new ExamplePage(driver);
639+
})
640+
.orElse((ExamplePage) parentPage);
641+
}
642+
}
468643
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.nordstrom.automation.selenium.model;
2+
3+
import org.openqa.selenium.Alert;
4+
import org.openqa.selenium.NoAlertPresentException;
5+
import org.openqa.selenium.TimeoutException;
6+
import org.openqa.selenium.WebDriver;
7+
8+
import java.time.Duration;
9+
import java.util.Optional;
10+
11+
/**
12+
* This class is a wrapper for the <b>Selenium</b> {@link Alert} class, providing an interface that treats browser
13+
* modals as the application-level entities that they are instead of the pseudo-contexts presented by the standard
14+
* API.
15+
*/
16+
public abstract class AlertHandler {
17+
/** driver for this handler */
18+
protected final WebDriver driver;
19+
/** alert handler parent page */
20+
protected final Page parentPage;
21+
/** alert handler wait timeout */
22+
protected final Duration timeout;
23+
24+
/**
25+
* Constructor for an alert handler with default timeout.
26+
*
27+
* @param parentPage alert parent page
28+
*/
29+
public AlertHandler(final Page parentPage) {
30+
this(parentPage, Duration.ofSeconds(5));
31+
}
32+
33+
/**
34+
* Constructor for an alert handler with specified timeout.
35+
*
36+
* @param parentPage alert parent page
37+
* @param timeout maximum interval to wait for an alert to appear
38+
*/
39+
public AlertHandler(final Page parentPage, final Duration timeout) {
40+
this.driver = parentPage.getWrappedDriver();
41+
this.parentPage = parentPage;
42+
this.timeout = timeout;
43+
}
44+
45+
/**
46+
* Wait for browser alert up to the timeout interval.
47+
*
48+
* @return {@link Alert} object if alert appears; {@code null} if wait is interrupted
49+
* @throws TimeoutException if alert is still absent when wait interval expires
50+
*/
51+
protected Alert waitForAlert() {
52+
long start = System.currentTimeMillis();
53+
long end = start + timeout.toMillis();
54+
55+
while (System.currentTimeMillis() < end) {
56+
try {
57+
return driver.switchTo().alert();
58+
} catch (NoAlertPresentException e) {
59+
try {
60+
Thread.sleep(100);
61+
} catch (InterruptedException ignored) {
62+
Thread.currentThread().interrupt();
63+
return null;
64+
}
65+
}
66+
}
67+
throw new TimeoutException("Alert not present after waiting " + timeout.getSeconds() + " seconds.");
68+
}
69+
70+
/**
71+
* Switch driver focus to browser alert if one is present.
72+
*
73+
* @return {@link Alert} object; {@code null} if alert is absent
74+
*/
75+
protected Alert switchIfAlertExists() {
76+
try {
77+
return driver.switchTo().alert();
78+
} catch (NoAlertPresentException e) {
79+
return null;
80+
}
81+
}
82+
83+
/**
84+
* Determine if browser alert is present.
85+
*
86+
* @return {@code true} if alert exists; otherwise {@code false}
87+
*/
88+
public boolean exists() {
89+
return (null != switchIfAlertExists());
90+
}
91+
92+
/**
93+
* Get text of browser alert.
94+
*
95+
* @return {@link Alert} text; {@code null} if alert is absent
96+
*/
97+
public String getText() {
98+
return Optional.ofNullable(switchIfAlertExists()).map(alert -> alert.getText()).orElse(null);
99+
}
100+
101+
/**
102+
* Wait for browser alert and accept it.
103+
*
104+
* @return alert text; {code null} if wait is interrupted
105+
* @throws TimeoutException if alert is still absent when wait interval expires
106+
*/
107+
abstract public Page accept();
108+
109+
/**
110+
* Wait for browser alert, then send the specified keys and accept the alert.
111+
*
112+
* @param keys keys to send
113+
* @return alert text; {code null} if wait is interrupted
114+
* @throws TimeoutException if alert is still absent when wait interval expires
115+
*/
116+
abstract public Page sendKeysAndAccept(String keys);
117+
118+
/**
119+
* Wait for browser alert and dismiss it.
120+
*
121+
* @return alert text; {code null} if wait is interrupted
122+
* @throws TimeoutException if alert is still absent when wait interval expires
123+
*/
124+
abstract public Page dismiss();
125+
126+
/**
127+
* If browser alert is present, accept it.
128+
*
129+
* @return if alert is present, landing page; otherwise parent page
130+
*/
131+
public Page acceptIfPresent() {
132+
return exists() ? accept() : parentPage;
133+
}
134+
135+
/**
136+
* If browser alert is present, dismiss it.
137+
*
138+
* @return if alert is present, landing page; otherwise parent page
139+
*/
140+
public Page dismissIfPresent() {
141+
return exists() ? dismiss() : parentPage;
142+
}
143+
}

0 commit comments

Comments
 (0)