/
Control.java
364 lines (315 loc) · 12.2 KB
/
Control.java
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
package org.jenkinsci.test.acceptance.po;
import java.time.Duration;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.jenkinsci.test.acceptance.selenium.Scroller;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.Select;
import com.google.inject.Injector;
import org.jenkinsci.test.acceptance.junit.Resource;
/**
* Wraps a specific form element in {@link PageAreaImpl} to provide operations.
* <p/>
* {@link Control} is like a {@link WebElement}, but with the following key differences:
* <p/>
* <ul> <li>{@link Control} is late binding, and the underlying {@link WebElement} is resolved only when an interaction
* with control happens. This allows {@link Control}s to be instantiated earlier (typically when a {@link PageObject}
* subtype is instantiated.) <li>{@link Control} offers richer methods to interact with a form element, making the right
* code easier to write. </ul>
* <p/>
* See {@link PageAreaImpl} subtypes for typical usage.
*
* @author Kohsuke Kawaguchi
* @see PageAreaImpl#control(String...)
*/
public class Control extends CapybaraPortingLayerImpl {
private final Owner parent;
private final String[] relativePaths;
public Control(PageAreaImpl parent, String... relativePaths) {
super(parent.injector);
this.parent = parent;
this.relativePaths = relativePaths;
}
/**
* Creates a control by giving their full path in the page
*/
public Control(PageObject parent, String... paths) {
super(parent.injector);
this.parent = new Owner() {
@Override
public By path(String rel) {
return by.path(rel);
}
};
this.relativePaths = paths;
}
public Control(Injector injector, final By selector) {
super(injector);
this.relativePaths = new String[1];
this.parent = new Owner() {
@Override
public By path(String rel) {
return selector;
}
};
}
public WebElement resolve() {
NoSuchElementException problem = new NoSuchElementException("No relative path specified!");
for (String p : relativePaths) {
try {
return find(parent.path(p));
}
catch (NoSuchElementException e) {
problem = e;
}
}
throw problem;
}
public void sendKeys(String t) {
resolve().sendKeys(t);
}
public void uncheck() {
check(resolve(), false);
}
public void check() {
check(resolve(), true);
}
public void check(boolean state) {
check(resolve(), state);
}
/**
* sends a click on the underlying element.
* You should not use this on any page where the click will cause another page to be loaded as it will not
* gaurantee that the new page has been loaded.
* @see #clickAndWaitToBecomeStale()
* @see #clickAndWaitToBecomeStale(Duration)
*/
public void click() {
resolve().click();
}
/**
* like click but will block for up to 30 seconds until the underlying web element has become stale.
* see https://blog.codeship.com/get-selenium-to-wait-for-page-load/
*/
/*package*/ void clickAndWaitToBecomeStale() {
clickAndWaitToBecomeStale(Duration.ofSeconds(30));
}
/**
* like click but will block until the underlying web element has become stale.
* see https://blog.codeship.com/get-selenium-to-wait-for-page-load/
* @param timeout the amount of time to wait
*/
/*package*/ void clickAndWaitToBecomeStale(Duration timeout) {
WebElement webElement = resolve();
// webElement.submit() despite advertising it does exactly this just blows up :(
webElement.click();
waitFor(webElement).withTimeout(timeout).until(Control::isStale);
}
/**
* The existing {@link org.jenkinsci.test.acceptance.po.Control#set(String)}
* method has shortcomings regarding large strings because it utilizes
* the sendKeys mechanism to enter the string which takes a significant amount
* of time, i.e. the browser may consider the script to be unresponsive.
*
* This method method shall provide a high throughput mechanism which
* puts the whole string at once into the text field instead of char by char.
*
* This is a solution / workaround published for Selenium Issue 4496:
* https://code.google.com/p/selenium/issues/detail?id=4469
*
* @param text the large string to be entered
*/
public void setAtOnce(String text){
WebElement e = resolve();
e.clear();
((JavascriptExecutor)driver).executeScript("arguments[0].value = arguments[1];", e, text);
}
/**
* Returns the value of the input field.
*
* @return the value of the input field.
*/
public String get() {
return resolve().getAttribute("value");
}
/**
* Sets the value of the input field to the specified text.
*
* Any existing value gets cleared.
*/
public void set(@Nullable String text) {
//if the text is longer than 255 characters, use the high throughput variant
if (text!=null && text.length() > 255)
setAtOnce(text);
else {
WebElement e = resolve();
e.clear();
e.sendKeys(StringUtils.defaultString(text));
}
}
public void set(Object text) {
set(text.toString());
}
/**
* Clicks a menu button, and selects the matching item from the drop down.
* TODO using a class name as the {@link Describable#value} does not seem to work.
* @param type
* Class with {@link Describable} annotation.
*/
public void selectDropdownMenu(Class type) {
click();
findCaption(type,findDropDownMenuItem).click();
elasticSleep(1000);
}
public void selectDropdownMenu(String displayName) {
click();
elasticSleep(1000);
findDropDownMenuItem.find(displayName).click();
elasticSleep(1000);
}
/**
* Given a menu button that shows a list of build steps, select the right item from the menu
* to insert the said build step.
*/
private final Finder<WebElement> findDropDownMenuItem = new Finder<WebElement>() {
@Override
protected WebElement find(String caption) {
WebElement menuButton = resolve();
// With enough implementations registered the one we are looking for might
// require scrolling in menu to become visible. This dirty hack stretch
// yui menu so that all the items are visible.
executeScript("" +
"YAHOO.util.Dom.batch(" +
" document.querySelector('.yui-menu-body-scrolled')," +
" function (el) {" +
" el.style.height = 'auto';" +
" YAHOO.util.Dom.removeClass(el, 'yui-menu-body-scrolled');" +
" }" +
");"
);
WebElement context = findElement(menuButton, by.xpath("ancestor::*[contains(@class,'yui-menu-button')]/.."));
WebElement e = findElement(context, by.link(caption));
return e;
}
};
/**
* For alternative use when the 'yui-menu-button' doesn't exist.
* @param type
*/
public void selectDropdownMenuAlt(Class type) {
findCaption(type,findDropDownMenuItemBySelector);
elasticSleep(1000);
}
private final Finder<WebElement> findDropDownMenuItemBySelector = new Finder<WebElement>() {
@Override
protected WebElement find(String caption) {
WebElement menuButton = resolve();
// With enough implementations registered the one we are looking for might
// require scrolling in menu to become visible. This dirty hack stretch
// yui menu so that all the items are visible.
executeScript("" +
"YAHOO.util.Dom.batch(" +
" document.querySelector('.yui-menu-body-scrolled')," +
" function (el) {" +
" el.style.height = 'auto';" +
" YAHOO.util.Dom.removeClass(el, 'yui-menu-body-scrolled');" +
" }" +
");"
);
Select context = new Select(findElement(menuButton, by.xpath("ancestor-or-self::*[contains(@class,'setting-input dropdownList')]")));
context.selectByVisibleText(caption);
WebElement e = context.getFirstSelectedOption();
return e;
}
};
/**
* Select an option.
*/
public void select(String option) {
WebElement e = resolve();
// Make sure the select is scrolled into view before interacting with its options that has got special handling by scroller.
new Scroller().scrollIntoView(e, driver);
findElement(e, by.option(option)).click();
}
private WebElement findElement(WebElement context, By selector) {
try {
return context.findElement(selector);
} catch (NoSuchElementException x) {
// this is often the best place to set a breakpoint
String msg = String.format("Unable to locate %s in %s", selector, driver.getCurrentUrl());
throw new NoSuchElementException(msg, x);
}
}
public void select(Class<?> describable) {
String element = findCaption(describable, new Finder<String>() {
@Override
protected String find(String caption) {
return Control.this.getElement(by.option(caption)) != null
? caption : null
;
}
});
select(element);
}
public void choose(Class<?> describable) {
String element = findCaption(describable, new Finder<String>() {
@Override
protected String find(String caption) {
final By xpath = by.xpath("//input[@type = 'radio' and @value = '%s']", caption);
return Control.this.getElement(xpath) != null ? caption : null;
}
});
choose(element);
}
public void upload(Resource res) {
resolve().sendKeys(res.asFile().getAbsolutePath());
}
public String text() {
return resolve().getText();
}
public FormValidation getFormValidation() {
WebElement control = resolve();
// Fire validation if it was not already
control.sendKeys(Keys.TAB);
WebElement validationArea;
// Special handling for validation buttons and their markup
if (control.getTagName().equals("button")) {
WebElement spinner = control.findElement(by.xpath("./../../../following-sibling::div[1]"));
// Wait as long as there is some spinner shown on the page
waitFor().until(() -> !spinner.isDisplayed());
validationArea = control.findElement(by.xpath("./../../../following-sibling::div[2]"));
} else {
// Wait for validation area to stop being <div></div>
validationArea = waitFor().until(() -> {
WebElement va = control.findElement(by.xpath("./../../following-sibling::tr/td[2]"));
String cls = va.findElement(by.xpath("./div")).getAttribute("class");
return (cls == null || cls.isEmpty()) ? null : va;
});
}
return new FormValidation(validationArea);
}
/**
* Determines whether an object is existing on the current page
* @return TRUE if it exists
*/
public boolean exists(){
try{
this.resolve();
return true;
}
catch (NoSuchElementException e)
{
return false;
}
}
public interface Owner {
/**
* Resolves relative path into a selector.
*/
By path(String rel);
}
}