Skip to content

Commit 3901335

Browse files
committed
Adds New Functionality for Extendable/Containerized elements for reuse, backwards compatible
1 parent 9b1e83c commit 3901335

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)