Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions java/src/org/openqa/selenium/json/InstanceCoercer.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ private Map<String, TypeAndWriter> getFieldWriters(Constructor<?> constructor) {
try {
field.set(instance, value);
} catch (IllegalAccessException e) {
throw new JsonException(e);
throw new JsonException(
String.format(
"Cannot set %s.%s = %s",
instance.getClass().getName(), field.getName(), value),
e);
}
};
return new TypeAndWriter(type, writer);
Expand All @@ -141,7 +145,11 @@ private Map<String, TypeAndWriter> getBeanWriters(Constructor<?> constructor) {
try {
method.invoke(instance, value);
} catch (ReflectiveOperationException e) {
throw new JsonException(e);
throw new JsonException(
String.format(
"Cannot call method %s.%s(%s)",
instance.getClass().getName(), method.getName(), value),
e);
}
};
return new TypeAndWriter(type, writer);
Expand All @@ -156,26 +164,21 @@ private Constructor<?> getConstructor(Type type) {
constructor.setAccessible(true);
return constructor;
} catch (ReflectiveOperationException e) {
throw new JsonException(e);
throw new JsonException("Cannot create instance of " + type, e);
}
}

private static Class<?> getClss(Type type) {
Class<?> target = null;

if (type instanceof Class) {
target = (Class<?>) type;
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) type).getRawType();
if (rawType instanceof Class) {
target = (Class<?>) rawType;
return (Class<?>) rawType;
}
}

if (target == null) {
throw new JsonException("Cannot determine base class");
}
return target;
throw new JsonException("Cannot determine base class for " + type);
}

private static class TypeAndWriter {
Expand Down
22 changes: 15 additions & 7 deletions java/src/org/openqa/selenium/json/InstantCoercer.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
package org.openqa.selenium.json;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.function.BiFunction;
import java.util.regex.Pattern;

public class InstantCoercer extends TypeCoercer<Instant> {
private static final Pattern DIGITS_ONLY = Pattern.compile("\\d+");

@Override
public boolean test(Class<?> aClass) {
return Instant.class.isAssignableFrom(aClass);
Expand All @@ -40,15 +43,20 @@ public BiFunction<JsonInput, PropertySetting, Instant> apply(Type type) {
return Instant.ofEpochMilli(jsonInput.nextNumber().longValue());
} else if (JsonType.STRING.equals(token)) {
String raw = jsonInput.nextString();
if (DIGITS_ONLY.matcher(raw).matches()) {
try {
return Instant.ofEpochMilli(new BigInteger(raw).longValueExact());
} catch (NumberFormatException | ArithmeticException invalidLong) {
throw new JsonException(
String.format("\"%s\" is not a valid timestamp", raw), invalidLong);
}
}
try {
TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(raw);
return Instant.from(parsed);
} catch (DateTimeParseException ignored) {
try {
return Instant.ofEpochMilli(new BigDecimal(raw).longValue());
} catch (NumberFormatException e) {
throw new JsonException(raw + " does not look like an Instant");
}
} catch (DateTimeParseException invalidDateTime) {
throw new JsonException(
String.format("\"%s\" does not look like an Instant", raw), invalidDateTime);
}
}

Expand Down
2 changes: 1 addition & 1 deletion java/src/org/openqa/selenium/json/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public String toJson(Object toConvert, int maxDepth) {
jsonOutput.write(toConvert, maxDepth);
return writer.toString();
} catch (IOException e) {
throw new JsonException(e);
throw new JsonException("Cannot convert " + toConvert + " to json", e);
}
}

Expand Down
2 changes: 1 addition & 1 deletion java/src/org/openqa/selenium/json/JsonInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public Number nextNumber() {

return new BigDecimal(builder.toString()).doubleValue();
} catch (NumberFormatException e) {
throw new JsonException("Unable to parse to a number: " + builder + ". " + input);
throw new JsonException("Unable to parse to a number: " + builder + ". " + input, e);
}
}

Expand Down
6 changes: 4 additions & 2 deletions java/src/org/openqa/selenium/json/NumberCoercer.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ public BiFunction<JsonInput, PropertySetting, T> apply(Type ignored) {
break;

case STRING:
String numberAsString = jsonInput.nextString();
try {
number = new BigDecimal(jsonInput.nextString());
number = new BigDecimal(numberAsString);
} catch (NumberFormatException e) {
throw new JsonException(e);
throw new JsonException(
String.format("Not a numeric value: \"%s\"", numberAsString), e);
}
break;

Expand Down
2 changes: 1 addition & 1 deletion java/src/org/openqa/selenium/json/ObjectCoercer.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import java.util.function.BiFunction;
import org.openqa.selenium.internal.Require;

class ObjectCoercer extends TypeCoercer {
class ObjectCoercer extends TypeCoercer<Object> {

private final JsonTypeCoercer coercer;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ void canMoveMouseToAnElementInAnIframeAndClick() {
wait.until(presenceOfElementLocated(By.id("ifr")));
driver.switchTo().frame("ifr");

WebElement link = driver.findElement(By.id("link"));
WebElement link = wait.until(presenceOfElementLocated(By.id("link")));

new Actions(driver).moveToElement(link).click().perform();

Expand Down Expand Up @@ -299,13 +299,6 @@ public void testClickAfterMoveToAnElementWithAnOffsetShouldUseLastMousePosition(
assertThat(y).isCloseTo(location.getY() + 10, Offset.offset(5));
}

private boolean fuzzyPositionMatching(int expectedX, int expectedY, int actualX, int actualY) {
// Everything within 5 pixels range is OK
final int ALLOWED_DEVIATION = 5;
return Math.abs(expectedX - actualX) < ALLOWED_DEVIATION
&& Math.abs(expectedY - actualY) < ALLOWED_DEVIATION;
}

/**
* This test demonstrates the following problem: When the representation of the mouse in the
* driver keeps the wrong state, mouse movement will end up at the wrong coordinates.
Expand Down Expand Up @@ -388,7 +381,7 @@ public void testHoldingDownShiftKeyWhileClicking() {

@Test
@NotYetImplemented(SAFARI)
public void canClickOnASuckerFishStyleMenu() throws InterruptedException {
public void canClickOnASuckerFishStyleMenu() {
driver.get(pages.javascriptPage);
unfocusMenu();

Expand Down
1 change: 1 addition & 0 deletions java/test/org/openqa/selenium/json/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ java_test_suite(
artifact("org.junit.jupiter:junit-jupiter-api"),
artifact("org.assertj:assertj-core"),
artifact("com.google.code.gson:gson"),
artifact("org.jspecify:jspecify"),
] + JUNIT5_DEPS,
)
135 changes: 125 additions & 10 deletions java/test/org/openqa/selenium/json/JsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.byLessThan;
import static org.assertj.core.api.InstanceOfAssertFactories.MAP;
import static org.openqa.selenium.Proxy.ProxyType.PAC;
import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.json.PropertySetting.BY_FIELD;

import com.google.common.reflect.TypeToken;
import java.io.StringReader;
Expand All @@ -32,6 +34,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.Capabilities;
Expand Down Expand Up @@ -124,7 +127,7 @@ void shouldAllowUserToPopulateFieldsDirectly() {

Json json = new Json();
String raw = json.toJson(map);
BeanWithSetter seen = json.toType(raw, BeanWithSetter.class, PropertySetting.BY_FIELD);
BeanWithSetter seen = json.toType(raw, BeanWithSetter.class, BY_FIELD);

assertThat(seen.theName).isEqualTo("fishy");
}
Expand All @@ -135,7 +138,7 @@ void settingFinalFieldsShouldWork() {

Json json = new Json();
String raw = json.toJson(map);
BeanWithFinalField seen = json.toType(raw, BeanWithFinalField.class, PropertySetting.BY_FIELD);
BeanWithFinalField seen = json.toType(raw, BeanWithFinalField.class, BY_FIELD);

assertThat(seen.theName).isEqualTo("fishy");
}
Expand Down Expand Up @@ -203,14 +206,101 @@ void shouldSetPrimitiveValuesToo() {
}

@Test
void shouldBeAbleToReadAnInstant() {
// We will lose the nanoseconds
Instant now = Instant.ofEpochMilli(System.currentTimeMillis());
String raw = String.valueOf(now.toEpochMilli());
void canReadNumericValues() {
String raw =
"{\"id\": 1234567890123456789, \"age\": 1234567890, \"fee\": \"999.888\", \"amount\":"
+ " \"999888.777666\"}";

NumericValues bean = new Json().toType(raw, NumericValues.class, BY_FIELD);

assertThat(bean.id).isEqualTo(1234567890123456789L);
assertThat(bean.age).isEqualTo(1234567890);
assertThat(bean.fee).isEqualTo(999.888f);
assertThat(bean.amount).isEqualTo(999888.777666);
assertThat(bean.remainder).isNull();
}

@Test
void reportsWhichNumericValueWasInvalid() {
String raw = "{\"id\": 123, \"age\": \"df8e0744-b1db-49b2-96df-45bd0d70dcd3\"}";

assertThatThrownBy(() -> new Json().toType(raw, NumericValues.class, BY_FIELD))
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("Unable to parse: " + raw)
.cause()
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("Not a numeric value: \"df8e0744-b1db-49b2-96df-45bd0d70dcd3\"");
}

@Test
void reportsTooLargeNumericValue() {
String raw = "{\"id\": 123456789012345678901234567890}";

assertThatThrownBy(() -> new Json().toType(raw, NumericValues.class, BY_FIELD))
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("Unable to parse: " + raw)
.cause()
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("Unable to parse to a number: 123456789012345678901234567890")
.cause()
.isInstanceOf(NumberFormatException.class)
.hasMessage("For input string: \"123456789012345678901234567890\"");
}

@Test
void canReadBooleanValues() {
String raw = "{\"hero\": true, \"gangster\": false}";

BooleanValues bean = new Json().toType(raw, BooleanValues.class, BY_FIELD);

assertThat(bean.hero).isEqualTo(true);
assertThat(bean.gangster).isEqualTo(false);
}

@Test
void reportsWhichBooleanValueWasInvalid() {
String raw = "{\"hero\": true, \"gangster\": \"not sure\"}";

Instant instant = new Json().toType(raw, Instant.class);
assertThatThrownBy(() -> new Json().toType(raw, BooleanValues.class, BY_FIELD))
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("Unable to parse: " + raw)
.cause()
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("Expected to read a BOOLEAN but instead have: STRING")
.hasMessageContaining("to read: \"not sure\"");
}

@Test
void shouldBeAbleToReadAnInstantFromTimestamp() {
long now = System.currentTimeMillis();

Dates instant = new Json().toType("{\"birth\": " + now + "}", Dates.class, BY_FIELD);

assertThat(instant).isEqualTo(now);
assertThat(instant.birth).isEqualTo(Instant.ofEpochMilli(now));
}

@Test
void shouldBeAbleToReadAnInstantFromTimestampAsString() {
long now = System.currentTimeMillis();

Dates instant = new Json().toType("{\"birth\": \"" + now + "\"}", Dates.class, BY_FIELD);

assertThat(instant.birth).isEqualTo(Instant.ofEpochMilli(now));
}

@Test
void reportsWhichInstantValueWasInvalid() {
String raw = "\"2025-13-32\"";

assertThatThrownBy(() -> new Json().toType(raw, Instant.class))
.isInstanceOf(JsonException.class)
.hasMessage("Unable to parse: \"2025-13-32\"")
.cause()
.isInstanceOf(JsonException.class)
.hasMessageStartingWith("\"2025-13-32\" does not look like an Instant")
.cause()
.isInstanceOf(java.time.format.DateTimeParseException.class)
.hasMessageContaining("Text '2025-13-32' could not be parsed");
}

@Test
Expand Down Expand Up @@ -255,18 +345,24 @@ void shouldConvertAResponseWithAnElementInIt() {
}

@Test
@SuppressWarnings("deprecation")
void shouldBeAbleToCopeWithStringsThatLookLikeBooleans() {
String json = "{\"value\":\"false\",\"context\":\"foo\",\"sessionId\":\"1210083863107\"}";
new Json().toType(json, Response.class);
Response parsed = new Json().toType(json, Response.class);
assertThat(parsed.getValue()).isEqualTo("false");
assertThat(parsed.getSessionId()).isEqualTo("1210083863107");
assertThat(parsed.getState()).isNull();
assertThat(parsed.getStatus()).isNull();
}

@Test
void shouldBeAbleToSetAnObjectToABoolean() {
String json = "{\"value\":true,\"context\":\"foo\",\"sessionId\":\"1210084658750\"}";
String json = "{\"value\":true,\"context\":\"foo\",\"sessionId\":\"true\"}";

Response response = new Json().toType(json, Response.class);

assertThat(response.getValue()).isEqualTo(true);
assertThat(response.getSessionId()).isEqualTo("true");
}

@Test
Expand Down Expand Up @@ -590,4 +686,23 @@ private static MapTakingFromJsonMethod fromJson(Map<String, Object> args) {
return toReturn;
}
}

static class NumericValues {
long id;
int age;
float fee;
double amount;
@Nullable Double remainder;
}

static class BooleanValues {
boolean hero;
Boolean gangster;
}

static class Dates {
Instant birth;
Date wedding;
Instant death;
}
}