Skip to content

Commit 5c4623f

Browse files
committed
test: add support for client callable interfaces
1 parent 92ff088 commit 5c4623f

File tree

3 files changed

+209
-17
lines changed

3 files changed

+209
-17
lines changed
Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package com.flowingcode.vaadin.addons.chipfield.integration;
22

3+
import java.lang.reflect.InvocationHandler;
4+
import java.lang.reflect.Method;
5+
import java.lang.reflect.Proxy;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Optional;
39
import java.util.concurrent.TimeoutException;
410
import java.util.stream.Collectors;
511
import java.util.stream.Stream;
612

7-
import org.junit.Before;
813
import org.openqa.selenium.By;
914
import org.openqa.selenium.JavascriptExecutor;
15+
import org.openqa.selenium.WebDriver;
1016
import org.openqa.selenium.WebElement;
11-
import org.openqa.selenium.support.ui.ExpectedConditions;
12-
import org.openqa.selenium.support.ui.WebDriverWait;
1317

1418
import com.vaadin.flow.component.ClientCallable;
1519

@@ -21,38 +25,68 @@ protected AbstractChipfieldTest() {
2125
super("it");
2226
}
2327

24-
@Before
25-
public void before() {
26-
chipfield = $(ChipFieldElement.class).first();
28+
@Override
29+
public void setup() throws Exception {
30+
super.setup();
31+
chipfield = $(ChipFieldElement.class).waitForFirst();
2732
}
33+
2834
/**
2935
* Call a {@link ClientCallable} defined on the integration view.
3036
*
3137
* @param callable the client callable name
3238
* @param arguments arguments to be passed to the callable
33-
* @throws TimeoutException if the callable doesn't complete in 2 seconds.
39+
* @throws TimeoutException if the callable times out (see
40+
* {@link WebDriver.Timeouts#setScriptTimeout(long, java.util.concurrent.TimeUnit)
41+
* WebDriver.Timeouts}).
3442
* @throws RuntimeException if the callable fails.
3543
*/
36-
protected final void call(String callable, Object... arguments) {
44+
protected final Object call(String callable, Object... arguments) {
3745
WebElement view = getDriver().findElement(By.id("view"));
38-
String result = "data-callable-result";
46+
arguments = Optional.ofNullable(arguments).orElse(new Object[0]);
3947

4048
StringBuilder script = new StringBuilder();
4149
script.append("var view = arguments[0];");
4250
script.append("var callable = arguments[1];");
43-
script.append("var result = arguments[2];");
44-
script.append("view.removeAttribute(result);");
45-
script.append("view.$server[callable](...arguments[3])");
46-
script.append(" .then(()=>view.setAttribute(result,true))");
47-
script.append(" .catch(()=>view.setAttribute(result,false));");
51+
script.append("var callback = (result,success) => arguments[3]({result, success});");
52+
script.append("view.$server[callable](...arguments[2])");
53+
script.append(" .then(result=>callback(result, true))");
54+
script.append(" .catch(()=>callback(undefined, false));");
4855

49-
((JavascriptExecutor) getDriver()).executeScript(script.toString(), view, callable, result, arguments);
56+
@SuppressWarnings("unchecked")
57+
Map<String, Object> result = (Map<String, Object>) ((JavascriptExecutor) getDriver()).executeAsyncScript(script.toString(), view, callable, arguments);
5058

51-
new WebDriverWait(getDriver(), 2, 100).until(ExpectedConditions.attributeToBeNotEmpty(view, result));
52-
if (!Boolean.parseBoolean(view.getAttribute(result))) {
59+
if (!(Boolean) result.get("success")) {
5360
throw new RuntimeException(
5461
String.format("server call failed: %s(%s)", callable, Stream.of(arguments).map(Object::toString).collect(Collectors.joining(","))));
5562
}
63+
64+
return result.get("result");
65+
}
66+
67+
/**
68+
* Create a TestBench proxy that invokes methods from the interface through a
69+
* client {@link #call}.
70+
*/
71+
protected final <T> T createCallableProxy(Class<T> intf) {
72+
return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), new Class<?>[] { intf }, new InvocationHandler() {
73+
@Override
74+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
75+
Object result = call(method.getName(), args);
76+
77+
if (result == null || method.getReturnType() == Void.TYPE) {
78+
return null;
79+
}
80+
81+
if (method.getReturnType() == JsonArrayList.class) {
82+
return JsonArrayList.wrapForTestbench((List<?>) result);
83+
}
84+
85+
// this implementation is incomplete.
86+
// other types that should be supported are: Double, Integer, Boolean, String, JsonValue
87+
throw new ClassCastException(String.format("%s as %s", result.getClass().getName(), method.getReturnType().getName()));
88+
}
89+
}));
5690
}
5791

5892
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.flowingcode.vaadin.addons.chipfield.integration;
2+
3+
import java.util.AbstractCollection;
4+
import java.util.Collection;
5+
import java.util.Iterator;
6+
import java.util.List;
7+
import java.util.Optional;
8+
import java.util.function.Function;
9+
10+
import elemental.json.Json;
11+
import elemental.json.JsonValue;
12+
import elemental.json.impl.JreJsonArray;
13+
import lombok.experimental.Delegate;
14+
15+
//Vaadin constraints:
16+
// - the result type of a @ClientCallable methods must be assignable to JsonValue.
17+
// - the runtime type of the result must be instanceof JreJsonValue.
18+
// - JreJsonValue can only be extended through intermediate classes.
19+
20+
//Testbench constraints:
21+
// - Selenium already converts JSON to a List of objects (thus we have a "T" and not a JsonValue).
22+
// - In the integration test, we are interested on the "T" and not the raw JsonValue.
23+
// - It makes sense to implement Collection<T>, in order to facilitate the use of hamcrest Matchers
24+
// - JsonArray cannot implement List it because the return type of get(int) is incompatible.
25+
// - List isn't too helpul, because most Matchers work with Iterable/Collection.
26+
// - JsonValue methods (JS type coercion, etc.) are not needed in integration tests.
27+
@SuppressWarnings("serial")
28+
public interface JsonArrayList<T> extends JsonValue, Collection<T> {
29+
30+
List<T> asList();
31+
32+
public static JsonArrayList<String> fromStringArray(List<String> list) {
33+
return createArray(list, Json::create);
34+
}
35+
36+
public static JsonArrayList<Boolean> fromBooleanArray(List<Boolean> list) {
37+
return createArray(list, Json::create);
38+
}
39+
40+
public static JsonArrayList<Double> fromDoubleArray(List<Double> list) {
41+
return createArray(list, Json::create);
42+
}
43+
44+
/**
45+
* @deprecated. This method should be private.
46+
*/
47+
public static <T> JsonArrayList<T> createArray(List<T> list, Function<? super T, JsonValue> mapper) {
48+
//this is the server-side flavor of JsonArrayList
49+
class JreJsonArrayList extends JreJsonArray implements JsonArrayList<T> {
50+
51+
@Delegate(excludes = JreJsonArray.class)
52+
private Collection<T> list = new AbstractCollection<T>() {
53+
//the delegate is only for the purpose of implementing Collection,
54+
//but the Collection interface is unsupported on instances of JreJsonArrayList
55+
@Override
56+
public Iterator<T> iterator() {
57+
throw new UnsupportedOperationException();
58+
}
59+
60+
@Override
61+
public int size() {
62+
throw new UnsupportedOperationException();
63+
}
64+
};
65+
66+
public JreJsonArrayList(List<T> list, Function<? super T, JsonValue> mapper) {
67+
super(Json.instance());
68+
for (T t : list) {
69+
set(length(), Optional.ofNullable(t).map(mapper).orElseGet(Json::createNull));
70+
}
71+
}
72+
73+
@Override
74+
public List<T> asList() {
75+
//JsonArrayList#asList is unsupported
76+
throw new UnsupportedOperationException();
77+
}
78+
}
79+
80+
return new JreJsonArrayList(list, mapper);
81+
}
82+
83+
public static <T> JsonArrayList<T> wrapForTestbench(List<T> list) {
84+
class TestbenchJsonArrayList implements JsonArrayList<T>, TestbenchValueWrapper {
85+
86+
@Delegate
87+
private List<T> list;
88+
89+
public TestbenchJsonArrayList(List<T> list) {
90+
this.list = list;
91+
}
92+
93+
@Override
94+
public List<T> asList() {
95+
return list;
96+
}
97+
98+
@Override
99+
public boolean equals(Object obj) {
100+
return list.equals(obj);
101+
}
102+
103+
@Override
104+
public String toString() {
105+
return list.toString();
106+
}
107+
}
108+
109+
return new TestbenchJsonArrayList(list);
110+
111+
}
112+
113+
}
114+
115+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.flowingcode.vaadin.addons.chipfield.integration;
2+
3+
import elemental.json.JsonType;
4+
import elemental.json.JsonValue;
5+
6+
public interface TestbenchValueWrapper extends JsonValue {
7+
8+
@Override
9+
default boolean asBoolean() {
10+
throw new UnsupportedOperationException();
11+
}
12+
13+
@Override
14+
default double asNumber() {
15+
throw new UnsupportedOperationException();
16+
}
17+
18+
@Override
19+
default String asString() {
20+
throw new UnsupportedOperationException();
21+
}
22+
23+
@Override
24+
default JsonType getType() {
25+
throw new UnsupportedOperationException();
26+
}
27+
28+
@Override
29+
default String toJson() {
30+
throw new UnsupportedOperationException();
31+
}
32+
33+
@Override
34+
default boolean jsEquals(JsonValue value) {
35+
throw new UnsupportedOperationException();
36+
}
37+
38+
@Override
39+
default Object toNative() {
40+
throw new UnsupportedOperationException();
41+
}
42+
43+
}

0 commit comments

Comments
 (0)