Skip to content

Commit 3964c58

Browse files
committed
New Functionality for Extendable/Containerized elements
This change adds an abstract extensible element which provide an 'out-of-the-box' way for users to define custom containerized element groupings, or singular elements with specific behaviors. The design uses only classes instead of interfaces with implementing classes to keep usage simplistic and allows for future annotations to enable context specific overriding. The change should be backwards compatible. Potentially addresses comment in #3680 #3680 (comment)
1 parent 9b1e83c commit 3964c58

File tree

8 files changed

+231
-19
lines changed

8 files changed

+231
-19
lines changed

java/src/org/openqa/selenium/support/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,7 @@ java_library(
5757
deps = [
5858
"//java/src/org/openqa/selenium:core",
5959
"//java/src/org/openqa/selenium/support/ui:components",
60+
"//java/src/org/openqa/selenium/support/ui:elements",
61+
"//java/src/org/openqa/selenium/remote",
6062
],
6163
)

java/src/org/openqa/selenium/support/pagefactory/DefaultFieldDecorator.java

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.openqa.selenium.support.FindBys;
3232
import org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler;
3333
import org.openqa.selenium.support.pagefactory.internal.LocatingElementListHandler;
34+
import org.openqa.selenium.support.ui.AbstractExtendedElement;
3435

3536
/**
3637
* Default decorator for use with PageFactory. Will decorate 1) all the WebElement fields and 2)
@@ -48,7 +49,8 @@ public DefaultFieldDecorator(ElementLocatorFactory factory) {
4849

4950
@Override
5051
public Object decorate(ClassLoader loader, Field field) {
51-
if (!(WebElement.class.isAssignableFrom(field.getType()) || isDecoratableList(field))) {
52+
Class<?> type = field.getType();
53+
if (!(WebElement.class.isAssignableFrom(type) || isDecoratableList(field))) {
5254
return null;
5355
}
5456

@@ -57,11 +59,20 @@ public Object decorate(ClassLoader loader, Field field) {
5759
return null;
5860
}
5961

60-
if (WebElement.class.isAssignableFrom(field.getType())) {
61-
return proxyForLocator(loader, locator);
62-
} else if (List.class.isAssignableFrom(field.getType())) {
63-
return proxyForListLocator(loader, locator);
64-
} else {
62+
if (WebElement.class.isAssignableFrom(type)) {
63+
WebElement elementProxy = proxyForLocator(loader, locator);
64+
if(AbstractExtendedElement.class.isAssignableFrom(type)) {
65+
try {
66+
return type.getConstructor(WebElement.class).newInstance(elementProxy);
67+
} catch (Exception e) {
68+
return null;
69+
}
70+
}
71+
return elementProxy;
72+
} else if (List.class.isAssignableFrom(type)) {
73+
return proxyForListLocator(loader, locator, getErasureType(field));
74+
}
75+
else {
6576
return null;
6677
}
6778
}
@@ -81,14 +92,28 @@ protected boolean isDecoratableList(Field field) {
8192
Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
8293

8394
if (!WebElement.class.equals(listType)) {
84-
return false;
95+
if (listType instanceof Class) {
96+
if (!AbstractExtendedElement.class.isAssignableFrom((Class<?>) listType)) {
97+
return false;
98+
}
99+
} else {
100+
return false;
101+
}
85102
}
86103

87104
return field.getAnnotation(FindBy.class) != null
88105
|| field.getAnnotation(FindBys.class) != null
89106
|| field.getAnnotation(FindAll.class) != null;
90107
}
91108

109+
private Class<?> getErasureType(Field field) {
110+
Type genericType = field.getGenericType();
111+
if (!(genericType instanceof ParameterizedType)) {
112+
return null;
113+
}
114+
return (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
115+
}
116+
92117
protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator) {
93118
InvocationHandler handler = new LocatingElementHandler(locator);
94119

@@ -103,11 +128,11 @@ protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator)
103128
}
104129

105130
@SuppressWarnings("unchecked")
106-
protected List<WebElement> proxyForListLocator(ClassLoader loader, ElementLocator locator) {
107-
InvocationHandler handler = new LocatingElementListHandler(locator);
131+
protected <T extends WebElement> List<T> proxyForListLocator(ClassLoader loader, ElementLocator locator, Class<?> type) {
132+
InvocationHandler handler = new LocatingElementListHandler(locator, type);
108133

109-
List<WebElement> proxy;
110-
proxy = (List<WebElement>) Proxy.newProxyInstance(loader, new Class[] {List.class}, handler);
134+
List<T> proxy;
135+
proxy = (List<T>) Proxy.newProxyInstance(loader, new Class[] {List.class}, handler);
111136
return proxy;
112137
}
113138
}

java/src/org/openqa/selenium/support/pagefactory/internal/LocatingElementListHandler.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,57 @@
1717

1818
package org.openqa.selenium.support.pagefactory.internal;
1919

20+
import java.lang.reflect.Constructor;
2021
import java.lang.reflect.InvocationHandler;
2122
import java.lang.reflect.InvocationTargetException;
2223
import java.lang.reflect.Method;
24+
import java.util.ArrayList;
2325
import java.util.List;
2426
import org.openqa.selenium.WebElement;
2527
import org.openqa.selenium.support.pagefactory.ElementLocator;
2628

2729
public class LocatingElementListHandler implements InvocationHandler {
2830
private final ElementLocator locator;
31+
private final Class<?> listType;
32+
private boolean isExtendedElement = false;
33+
private Constructor<?> cons = null;
34+
35+
public LocatingElementListHandler(ElementLocator locator, Class<?> listType) {
36+
this.locator = locator;
37+
this.listType = listType;
38+
if(!WebElement.class.equals(listType)) {
39+
this.isExtendedElement = true;
40+
try {
41+
cons = listType.getConstructor(WebElement.class);
42+
} catch (NoSuchMethodException e) {
43+
throw new RuntimeException("Constructor with WebElement argument not found for list type: "
44+
+ listType.getName());
45+
}
46+
}
47+
}
2948

3049
public LocatingElementListHandler(ElementLocator locator) {
3150
this.locator = locator;
51+
this.listType = WebElement.class;
52+
this.isExtendedElement = false;
53+
cons = null;
3254
}
3355

3456
@Override
3557
public Object invoke(Object object, Method method, Object[] objects) throws Throwable {
58+
List<Object> elementList = new ArrayList<>();
3659
List<WebElement> elements = locator.findElements();
3760

61+
if(isExtendedElement && null != cons) {
62+
for (WebElement element : elements) {
63+
Object extension = cons.newInstance(element);
64+
elementList.add(listType.cast(extension));
65+
}
66+
}
3867
try {
39-
return method.invoke(elements, objects);
68+
return method.invoke(
69+
isExtendedElement ? elementList : elements,
70+
objects);
4071
} catch (InvocationTargetException e) {
4172
// Unwrap the underlying exception
4273
throw e.getCause();
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.support.ui;
19+
20+
import org.openqa.selenium.WebElement;
21+
import org.openqa.selenium.WrapsElement;
22+
import org.openqa.selenium.remote.RemoteWebElement;
23+
24+
public abstract class AbstractExtendedElement extends RemoteWebElement implements WrapsElement {
25+
26+
private WebElement wrappedElement;
27+
28+
protected AbstractExtendedElement(WebElement element) {
29+
if (null == element) {
30+
throw new IllegalArgumentException("Wrapped element must not be null");
31+
}
32+
this.wrappedElement = element;
33+
}
34+
35+
@Override
36+
public WebElement getWrappedElement() {
37+
return this.wrappedElement;
38+
}
39+
40+
@Override
41+
public String toString() {
42+
return this.wrappedElement.toString();
43+
}
44+
45+
}

java/src/org/openqa/selenium/support/ui/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ java_library(
2828
java_library(
2929
name = "elements",
3030
srcs = [
31+
"AbstractExtendedElement.java",
3132
"ISelect.java",
3233
"Quotes.java",
3334
"Select.java",
@@ -38,6 +39,7 @@ java_library(
3839
],
3940
deps = [
4041
"//java/src/org/openqa/selenium:core",
42+
"//java/src/org/openqa/selenium/remote",
4143
],
4244
)
4345

java/src/org/openqa/selenium/support/ui/Select.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@
2222
import org.openqa.selenium.By;
2323
import org.openqa.selenium.NoSuchElementException;
2424
import org.openqa.selenium.WebElement;
25-
import org.openqa.selenium.WrapsElement;
2625

2726
/** Models a SELECT tag, providing helper methods to select and deselect options. */
28-
public class Select implements ISelect, WrapsElement {
27+
public class Select extends AbstractExtendedElement implements ISelect {
2928

3029
private final WebElement element;
3130
private final boolean isMulti;
@@ -38,6 +37,7 @@ public class Select implements ISelect, WrapsElement {
3837
* @throws UnexpectedTagNameException when element is not a SELECT
3938
*/
4039
public Select(WebElement element) {
40+
super(element);
4141
String tagName = element.getTagName();
4242

4343
if (!"select".equalsIgnoreCase(tagName)) {

java/test/org/openqa/selenium/support/PageFactoryTest.java

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.openqa.selenium.TimeoutException;
3333
import org.openqa.selenium.WebDriver;
3434
import org.openqa.selenium.WebElement;
35+
import org.openqa.selenium.support.ui.AbstractExtendedElement;
3536
import org.openqa.selenium.support.ui.ExpectedConditions;
3637
import org.openqa.selenium.support.ui.TickingClock;
3738
import org.openqa.selenium.support.ui.Wait;
@@ -55,6 +56,38 @@ void shouldProxyElementsInAnInstantiatedPage() {
5556
assertThat(page.list).isNotNull();
5657
}
5758

59+
@Test
60+
void shouldProxyExtendedElements() {
61+
PublicPageWithExtendedElements page = new PublicPageWithExtendedElements();
62+
63+
assertThat(page.q).isNull();
64+
assertThat(page.list).isNull();
65+
assertThat(page.rendered).isNull();
66+
assertThat(page.renderedList).isNull();
67+
68+
PageFactory.initElements(searchContext, page);
69+
70+
assertThat(page.q).isNotNull();
71+
assertThat(page.list).isNotNull();
72+
assertThat(page.rendered).isNotNull();
73+
assertThat(page.renderedList).isNotNull();
74+
}
75+
76+
@Test
77+
void shouldProxyNestedExtendedElements() {
78+
PublicPageWithExtendedElements page = new PublicPageWithExtendedElements();
79+
80+
assertThat(page.q).isNull();
81+
82+
PageFactory.initElements(searchContext, page);
83+
84+
assertThat(page.q).isNotNull();
85+
86+
assertThat(page.q.a).isNull();
87+
PageFactory.initElements(page.q, page.q);
88+
assertThat(page.q.a).isNotNull();
89+
}
90+
5891
@Test
5992
void shouldInsertProxiesForPublicWebElements() {
6093
PublicPage page = PageFactory.initElements(searchContext, PublicPage.class);
@@ -154,33 +187,74 @@ void shouldComplainWhenMoreThanOneFindByAttributeIsSet() {
154187
GrottyPage page = new GrottyPage();
155188

156189
assertThatExceptionOfType(IllegalArgumentException.class)
157-
.isThrownBy(() -> PageFactory.initElements((WebDriver) null, page));
190+
.isThrownBy(() -> PageFactory.initElements((WebDriver) null, page));
158191
}
159192

160193
@Test
161194
void shouldComplainWhenMoreThanOneFindByShortFormAttributeIsSet() {
162195
GrottyPage2 page = new GrottyPage2();
163196

164197
assertThatExceptionOfType(IllegalArgumentException.class)
165-
.isThrownBy(() -> PageFactory.initElements((WebDriver) null, page));
198+
.isThrownBy(() -> PageFactory.initElements((WebDriver) null, page));
166199
}
167200

168201
@Test
169202
void shouldNotThrowANoSuchElementExceptionWhenUsedWithAFluentWait() {
170203
WebDriver driver = mock(WebDriver.class);
171204
when(driver.findElement(ArgumentMatchers.any()))
172-
.thenThrow(new NoSuchElementException("because"));
205+
.thenThrow(new NoSuchElementException("because"));
173206

174207
TickingClock clock = new TickingClock();
175208
Wait<WebDriver> wait =
176-
new WebDriverWait(driver, Duration.ofSeconds(1), Duration.ofMillis(1001), clock, clock);
209+
new WebDriverWait(driver, Duration.ofSeconds(1), Duration.ofMillis(1001), clock, clock);
177210

178211
PublicPage page = new PublicPage();
179212
PageFactory.initElements(driver, page);
180213
WebElement element = page.q;
181214

182215
assertThatExceptionOfType(TimeoutException.class)
183-
.isThrownBy(() -> wait.until(ExpectedConditions.visibilityOf(element)));
216+
.isThrownBy(() -> wait.until(ExpectedConditions.visibilityOf(element)));
217+
}
218+
219+
public static class ExtendedElement extends AbstractExtendedElement {
220+
221+
public ExtendedElement(WebElement element) {
222+
super(element);
223+
}
224+
225+
@FindBy(name = "a")
226+
public OtherExtendedElement a;
227+
228+
@FindBy(name = "a")
229+
public List<OtherExtendedElement> aList;
230+
231+
@FindBy(name = "a")
232+
public WebElement internalRender;
233+
234+
@FindBy(name = "a")
235+
public List<WebElement> internalRenderList;
236+
}
237+
238+
239+
public static class OtherExtendedElement extends AbstractExtendedElement {
240+
241+
public OtherExtendedElement(WebElement element) {
242+
super(element);
243+
}
244+
}
245+
246+
public static class PublicPageWithExtendedElements {
247+
@FindBy(name = "q")
248+
public ExtendedElement q;
249+
250+
@FindBy(name = "q")
251+
public List<ExtendedElement> list;
252+
253+
@FindBy(name = "a")
254+
public WebElement rendered;
255+
256+
@FindBy(name = "a")
257+
public List<WebElement> renderedList;
184258
}
185259

186260
public static class PublicPage {

0 commit comments

Comments
 (0)