From e50d774fa6f25134f65f030f695b3325d89c4538 Mon Sep 17 00:00:00 2001
From: Bauke Scholtz
Date: Thu, 30 Jul 2026 10:24:27 -0400
Subject: [PATCH] Add java.time Instant, Year, YearMonth and MonthDay support
to DateTimeConverter
The type attribute now also accepts "instant", "year", "yearMonth" and
"monthDay". None of these types can be formatted or parsed with a localized
date/time style, so dateStyle and timeStyle are ignored for them and the ISO
8601 form of the type is used when no pattern is specified: ISO_INSTANT for
instant, "uuuu" for year, "uuuu-MM" for yearMonth and "--MM-dd" for monthDay.
This follows the existing offsetTime/offsetDateTime/zonedDateTime types, which
also use a fixed ISO formatter.
An Instant carries no zone, so for that type the timeZone property is passed to
the formatter, which makes a pattern containing date or time fields resolvable.
The new types are also added to the message selection in getAsObject, which
would otherwise swallow the parse exception and silently convert bad input to
null.
Closes #2211
Co-Authored-By: Claude Opus 5 (1M context)
---
.../faces/convert/DateTimeConverter.java | 47 +++++-
api/src/main/vdldoc/faces.core.taglib.xml | 20 ++-
.../faces/convert/DateTimeConverterTest.java | 151 +++++++++++++++++-
spec/src/main/asciidoc/ChangeLog.adoc | 3 +
.../faces50/converters/Spec2211Bean.java | 67 ++++++++
.../converters/src/main/webapp/spec2211.xhtml | 62 +++++++
.../faces/faces50/converters/Spec2211IT.java | 101 ++++++++++++
7 files changed, 440 insertions(+), 11 deletions(-)
create mode 100644 tck/faces50/converters/src/main/java/ee/jakarta/tck/faces/faces50/converters/Spec2211Bean.java
create mode 100644 tck/faces50/converters/src/main/webapp/spec2211.xhtml
create mode 100644 tck/faces50/converters/src/test/java/ee/jakarta/tck/faces/faces50/converters/Spec2211IT.java
diff --git a/api/src/main/java/jakarta/faces/convert/DateTimeConverter.java b/api/src/main/java/jakarta/faces/convert/DateTimeConverter.java
index 884bcf0339..f0ca66f286 100644
--- a/api/src/main/java/jakarta/faces/convert/DateTimeConverter.java
+++ b/api/src/main/java/jakarta/faces/convert/DateTimeConverter.java
@@ -22,11 +22,15 @@
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
+import java.time.Year;
+import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
@@ -190,7 +194,11 @@ public class DateTimeConverter implements Converter
*
+ *
+ * The values {@code instant}, {@code year}, {@code yearMonth} and {@code monthDay} correspond in the same way to
+ * {@code java.time.Instant}, {@code java.time.Year}, {@code java.time.YearMonth} and {@code java.time.MonthDay}. None
+ * of these types can be formatted or parsed with a localized date/time style, hence the dateStyle and
+ * timeStyle properties must be ignored for them. When no pattern has been specified, the ISO
+ * 8601 representation of the type must be used: {@code java.time.format.DateTimeFormatter.ISO_INSTANT} for
+ * {@code instant}, the pattern {@code "uuuu"} for {@code year}, {@code "uuuu-MM"} for {@code yearMonth} and
+ * {@code "--MM-dd"} for {@code monthDay}.
+ *
+ *
+ *
+ * A {@code java.time.Instant} does not carry a time zone, hence for the type {@code instant} the timezone
+ * property must be passed to the underlying {@code DateTimeFormatter}, so that a pattern containing date
+ * or time fields can be resolved. Note that {@code ISO_INSTANT} always renders in UTC, regardless of the
+ * timezone property.
+ *
+ *
* @param type The new date style
*/
public void setType(String type) {
@@ -407,6 +433,9 @@ public Object getAsObject(FacesContext context, UIComponent component, String va
switch (type) {
case "date":
case "localDate":
+ case "year":
+ case "yearMonth":
+ case "monthDay":
throw new ConverterException(
getMessage(context, DATE_ID, value, parser.formatNow(), getLabel(context, component)),
e);
@@ -420,6 +449,7 @@ public Object getAsObject(FacesContext context, UIComponent component, String va
case "localDateTime":
case "offsetDateTime":
case "zonedDateTime":
+ case "instant":
throw new ConverterException(getMessage(context, DATETIME_ID, value, parser.formatNow(),
getLabel(context, component)), e);
}
@@ -668,6 +698,15 @@ private FormatWrapper getDateFormat(Locale locale, boolean forParsing) {
dtf = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(locale);
} else if (type.equals("zonedDateTime")) {
dtf = DateTimeFormatter.ISO_ZONED_DATE_TIME.withLocale(locale);
+ } else if (type.equals("instant")) {
+ dtf = DateTimeFormatter.ISO_INSTANT.withLocale(locale);
+ } else if (type.equals("year")) {
+ // These three patterns represent the ISO 8601 form of the type, as also used by its own parse() and toString().
+ dtfBuilder = new DateTimeFormatterBuilder().appendPattern("uuuu");
+ } else if (type.equals("yearMonth")) {
+ dtfBuilder = new DateTimeFormatterBuilder().appendPattern("uuuu-MM");
+ } else if (type.equals("monthDay")) {
+ dtfBuilder = new DateTimeFormatterBuilder().appendPattern("--MM-dd");
} else {
// PENDING(craigmcc) - i18n
throw new IllegalArgumentException("Invalid type: " + type);
@@ -686,6 +725,10 @@ private FormatWrapper getDateFormat(Locale locale, boolean forParsing) {
}
if (dtf != null) {
+ if (timeZone != null && "instant".equals(type)) {
+ dtf = dtf.withZone(timeZone.toZoneId());
+ }
+
return new FormatWrapper(dtf, fromJavaTime, forParsing);
}
}
diff --git a/api/src/main/vdldoc/faces.core.taglib.xml b/api/src/main/vdldoc/faces.core.taglib.xml
index b0e3c20055..4999638093 100644
--- a/api/src/main/vdldoc/faces.core.taglib.xml
+++ b/api/src/main/vdldoc/faces.core.taglib.xml
@@ -447,6 +447,12 @@
a java.util.TimeZone instance, or a String that is a timezone ID
as described in the javadocs for
java.util.TimeZone.getTimeZone().
+
+
A
+ java.time.Instant does not carry a time
+ zone, hence for the type "instant" this attribute is
+ also used to resolve the date and time fields of an
+ explicit pattern.
]]>
timeZonejava.lang.Object
@@ -459,13 +465,25 @@
parsed expecting. Valid values are "date", "time",
"both", "localDate",
"localDateTime", "localTime", "offsetTime",
- "offsetDateTime", and "zonedDateTime". The values starting
+ "offsetDateTime", "zonedDateTime", "instant", "year", "yearMonth",
+ and "monthDay". The values starting
with "local", "offset" and "zoned" correspond to Java SE 8
Date Time API classes in package java.time with
the name derived by upper casing the first letter. For
example, java.time.LocalDate for the value
"localDate". Default value is "date".
+
The values "instant", "year",
+ "yearMonth" and "monthDay" correspond in the same way to
+ java.time.Instant, java.time.Year,
+ java.time.YearMonth and
+ java.time.MonthDay. None of these types can be
+ formatted or parsed with a localized date/time style, hence
+ the dateStyle and timeStyle attributes are ignored for them.
+ When no pattern has been specified, the ISO 8601
+ representation of the type is used.
+
]]>
typejava.lang.String
diff --git a/api/src/test/java/jakarta/faces/convert/DateTimeConverterTest.java b/api/src/test/java/jakarta/faces/convert/DateTimeConverterTest.java
index 37a14e950e..310c698be0 100644
--- a/api/src/test/java/jakarta/faces/convert/DateTimeConverterTest.java
+++ b/api/src/test/java/jakarta/faces/convert/DateTimeConverterTest.java
@@ -19,9 +19,14 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.MonthDay;
+import java.time.Year;
+import java.time.YearMonth;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
@@ -29,6 +34,7 @@
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import java.util.TimeZone;
import jakarta.faces.application.Application;
import jakarta.faces.component.UIPanel;
@@ -52,6 +58,7 @@ public class DateTimeConverterTest {
private FacesContext facesContext;
private MockedStatic facesContextStatic;
+ private UIPanel component;
@BeforeEach
public void setUp() {
@@ -70,6 +77,8 @@ public void setUp() {
facesContextStatic = Mockito.mockStatic(FacesContext.class);
facesContextStatic.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
+
+ component = new UIPanel();
}
@AfterEach
@@ -95,8 +104,6 @@ public void testLocalTimeParsingWithRegularSpaceBeforeAmPm() {
converter.setType("localTime");
converter.setLocale(Locale.US);
- UIPanel component = new UIPanel();
-
// This is what a user would type: regular space (U+0020) before AM.
String userInput = "10:30:00 AM";
@@ -123,8 +130,6 @@ public void testLocalTimeParsingWithNnbsp() {
converter.setType("localTime");
converter.setLocale(Locale.US);
- UIPanel component = new UIPanel();
-
// Input with NNBSP — this should always work on JDK 21+.
String inputWithNnbsp = "10:30:00\u202fAM";
@@ -148,8 +153,6 @@ public void testLocalDateTimeParsingWithRegularSpaceBeforeAmPm() {
converter.setType("localDateTime");
converter.setLocale(Locale.US);
- UIPanel component = new UIPanel();
-
// Format a known value to get the expected formatted string, then replace NNBSP with regular space
// to simulate user input.
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
@@ -179,8 +182,6 @@ public void testLocalTimeRoundtripWithRegularSpace() {
converter.setType("localTime");
converter.setLocale(Locale.US);
- UIPanel component = new UIPanel();
-
LocalTime originalTime = LocalTime.of(14, 45, 30);
// getAsString produces formatted output (may contain NNBSP on JDK 21+)
@@ -197,6 +198,140 @@ public void testLocalTimeRoundtripWithRegularSpace() {
assertEquals(originalTime, parsed);
}
+ /**
+ * Test that type "instant" formats and parses using the ISO 8601 instant representation, which is what
+ * {@link Instant#toString()} and {@link Instant#parse(CharSequence)} use.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testInstant() {
+ DateTimeConverter converter = createConverter("instant");
+ Instant instant = Instant.parse("2026-07-30T10:15:30Z");
+
+ assertEquals("2026-07-30T10:15:30Z", converter.getAsString(facesContext, component, instant));
+ assertEquals(instant, converter.getAsObject(facesContext, component, "2026-07-30T10:15:30Z"));
+ }
+
+ /**
+ * Test that type "instant" resolves the date and time fields of an explicit pattern against the configured time zone,
+ * as an {@link Instant} does not carry one by itself.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testInstantWithPatternAndTimeZone() {
+ DateTimeConverter converter = createConverter("instant");
+ converter.setPattern("uuuu-MM-dd HH:mm:ss");
+ converter.setTimeZone(TimeZone.getTimeZone("Europe/Amsterdam"));
+ Instant instant = Instant.parse("2026-07-30T10:15:30Z");
+
+ assertEquals("2026-07-30 12:15:30", converter.getAsString(facesContext, component, instant));
+ assertEquals(instant, converter.getAsObject(facesContext, component, "2026-07-30 12:15:30"));
+ }
+
+ /**
+ * Test that type "year" formats and parses using the ISO 8601 year representation, which is what
+ * {@link Year#toString()} and {@link Year#parse(CharSequence)} use.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testYear() {
+ DateTimeConverter converter = createConverter("year");
+
+ assertEquals("2026", converter.getAsString(facesContext, component, Year.of(2026)));
+ assertEquals(Year.of(2026), converter.getAsObject(facesContext, component, "2026"));
+ }
+
+ /**
+ * Test that type "yearMonth" formats and parses using the ISO 8601 year-month representation, which is what
+ * {@link YearMonth#toString()} and {@link YearMonth#parse(CharSequence)} use.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testYearMonth() {
+ DateTimeConverter converter = createConverter("yearMonth");
+
+ assertEquals("2026-07", converter.getAsString(facesContext, component, YearMonth.of(2026, 7)));
+ assertEquals(YearMonth.of(2026, 7), converter.getAsObject(facesContext, component, "2026-07"));
+ }
+
+ /**
+ * Test that type "monthDay" formats and parses using the ISO 8601 month-day representation, which is what
+ * {@link MonthDay#toString()} and {@link MonthDay#parse(CharSequence)} use.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testMonthDay() {
+ DateTimeConverter converter = createConverter("monthDay");
+
+ assertEquals("--07-30", converter.getAsString(facesContext, component, MonthDay.of(7, 30)));
+ assertEquals(MonthDay.of(7, 30), converter.getAsObject(facesContext, component, "--07-30"));
+ }
+
+ /**
+ * Test that type "monthDay" accepts February 29, which is valid without a year.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testMonthDayAcceptsLeapDay() {
+ DateTimeConverter converter = createConverter("monthDay");
+
+ assertEquals(MonthDay.of(2, 29), converter.getAsObject(facesContext, component, "--02-29"));
+ }
+
+ /**
+ * Test that the types "year", "yearMonth" and "monthDay" honor an explicit pattern.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testPattern() {
+ assertPattern("year", "'FY'uuuu", Year.of(2026), "FY2026");
+ assertPattern("yearMonth", "MM/uuuu", YearMonth.of(2026, 7), "07/2026");
+ assertPattern("monthDay", "dd-MM", MonthDay.of(7, 30), "30-07");
+ }
+
+ private void assertPattern(String type, String pattern, Object value, String formatted) {
+ DateTimeConverter converter = createConverter(type);
+ converter.setPattern(pattern);
+
+ assertEquals(formatted, converter.getAsString(facesContext, component, value));
+ assertEquals(value, converter.getAsObject(facesContext, component, formatted));
+ }
+
+ /**
+ * Test that unparseable input for the new types results in a ConverterException rather than in a silent null, which
+ * is what happens when a type is not covered by the message selection in
+ * {@link DateTimeConverter#getAsObject(FacesContext, jakarta.faces.component.UIComponent, String)}.
+ *
+ * @see GitHub issue #2211
+ */
+ @Test
+ public void testUnparseableValueThrowsConverterException() {
+ assertUnparseable("instant", "2026-07-30");
+ assertUnparseable("year", "MMXXVI");
+ assertUnparseable("yearMonth", "2026-13");
+ assertUnparseable("monthDay", "--02-30");
+ }
+
+ private void assertUnparseable(String type, String value) {
+ DateTimeConverter converter = createConverter(type);
+ assertThrows(ConverterException.class, () -> converter.getAsObject(facesContext, component, value),
+ () -> "Type " + type + " must reject '" + value + '\'');
+ }
+
+ private static DateTimeConverter createConverter(String type) {
+ DateTimeConverter converter = new DateTimeConverter();
+ converter.setType(type);
+ converter.setLocale(Locale.US);
+ return converter;
+ }
+
private static void requireNnbspInPattern(FormatStyle dateStyle, FormatStyle timeStyle) {
String localizedPattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(
dateStyle, timeStyle, IsoChronology.INSTANCE, Locale.US);
diff --git a/spec/src/main/asciidoc/ChangeLog.adoc b/spec/src/main/asciidoc/ChangeLog.adoc
index 96f198c3f0..6ee04be043 100644
--- a/spec/src/main/asciidoc/ChangeLog.adoc
+++ b/spec/src/main/asciidoc/ChangeLog.adoc
@@ -12,6 +12,9 @@ pre-Jakarta Faces specification under the JCP.
This release contains the following backwards compatible changes:
+* Issue ID {issue_tracker_url}2211[2211] +
+New: DateTimeConverter support for java.time Instant, Year, YearMonth and MonthDay
+
* Issue ID {issue_tracker_url}2173[2173] +
Change: generalize Render Response resource push to allow modern mechanisms such as HTTP 103 Early Hints
diff --git a/tck/faces50/converters/src/main/java/ee/jakarta/tck/faces/faces50/converters/Spec2211Bean.java b/tck/faces50/converters/src/main/java/ee/jakarta/tck/faces/faces50/converters/Spec2211Bean.java
new file mode 100644
index 0000000000..9c44ede1b6
--- /dev/null
+++ b/tck/faces50/converters/src/main/java/ee/jakarta/tck/faces/faces50/converters/Spec2211Bean.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GPL-2.0 with Classpath-exception-2.0 which
+ * is available at https://openjdk.java.net/legal/gplv2+ce.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 or Apache-2.0
+ */
+package ee.jakarta.tck.faces.faces50.converters;
+
+import java.time.Instant;
+import java.time.MonthDay;
+import java.time.Year;
+import java.time.YearMonth;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Named;
+
+@Named
+@RequestScoped
+public class Spec2211Bean {
+
+ private Instant instant;
+ private Year year;
+ private YearMonth yearMonth;
+ private MonthDay monthDay;
+
+ public Instant getInstant() {
+ return instant;
+ }
+
+ public void setInstant(Instant instant) {
+ this.instant = instant;
+ }
+
+ public Year getYear() {
+ return year;
+ }
+
+ public void setYear(Year year) {
+ this.year = year;
+ }
+
+ public YearMonth getYearMonth() {
+ return yearMonth;
+ }
+
+ public void setYearMonth(YearMonth yearMonth) {
+ this.yearMonth = yearMonth;
+ }
+
+ public MonthDay getMonthDay() {
+ return monthDay;
+ }
+
+ public void setMonthDay(MonthDay monthDay) {
+ this.monthDay = monthDay;
+ }
+
+}
diff --git a/tck/faces50/converters/src/main/webapp/spec2211.xhtml b/tck/faces50/converters/src/main/webapp/spec2211.xhtml
new file mode 100644
index 0000000000..52a3950c8a
--- /dev/null
+++ b/tck/faces50/converters/src/main/webapp/spec2211.xhtml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+ spec2211
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tck/faces50/converters/src/test/java/ee/jakarta/tck/faces/faces50/converters/Spec2211IT.java b/tck/faces50/converters/src/test/java/ee/jakarta/tck/faces/faces50/converters/Spec2211IT.java
new file mode 100644
index 0000000000..7aad21e816
--- /dev/null
+++ b/tck/faces50/converters/src/test/java/ee/jakarta/tck/faces/faces50/converters/Spec2211IT.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GPL-2.0 with Classpath-exception-2.0 which
+ * is available at https://openjdk.java.net/legal/gplv2+ce.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 or Apache-2.0
+ */
+package ee.jakarta.tck.faces.faces50.converters;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+
+import ee.jakarta.tck.faces.util.selenium.BaseITNG;
+import ee.jakarta.tck.faces.util.selenium.WebPage;
+
+class Spec2211IT extends BaseITNG {
+
+ /**
+ * Test that the types instant, year, yearMonth and monthDay roundtrip through their ISO 8601 representation when no
+ * pattern has been specified.
+ *
+ * @see https://github.com/jakartaee/faces/issues/2211
+ */
+ @Test
+ void testDefaultFormat() {
+ Map values = new LinkedHashMap<>();
+ values.put("instant", "2026-07-30T10:15:30Z");
+ values.put("year", "2026");
+ values.put("yearMonth", "2026-07");
+ values.put("monthDay", "--07-30");
+
+ assertRoundtrip("formWithDefaultFormat", values);
+ }
+
+ /**
+ * Test that the types instant, year, yearMonth and monthDay honor an explicit pattern, whereby the instant resolves
+ * its date and time fields against the specified time zone.
+ *
+ * @see https://github.com/jakartaee/faces/issues/2211
+ */
+ @Test
+ void testPattern() {
+ Map values = new LinkedHashMap<>();
+ values.put("instant", "2026-07-30 12:15:30");
+ values.put("year", "FY2026");
+ values.put("yearMonth", "07/2026");
+ values.put("monthDay", "30-07");
+
+ assertRoundtrip("formWithPattern", values);
+ }
+
+ /**
+ * Test that unparseable input results in a conversion error message instead of in a silently converted null.
+ *
+ * @see https://github.com/jakartaee/faces/issues/2211
+ */
+ @Test
+ void testUnparseableValue() {
+ assertUnparseable("instant", "2026-07-30");
+ assertUnparseable("year", "MMXXVI");
+ assertUnparseable("yearMonth", "2026-13");
+ assertUnparseable("monthDay", "--02-30");
+ }
+
+ private void assertRoundtrip(String formId, Map values) {
+ WebPage page = getPage("spec2211.xhtml");
+ assertEquals("", page.findElement(By.id(formId + ":messages")).getText());
+
+ values.forEach((id, value) -> page.findElement(By.id(formId + ":" + id)).sendKeys(value));
+ page.guardHttp(page.findElement(By.id(formId + ":submit"))::click);
+
+ assertEquals("", page.findElement(By.id(formId + ":messages")).getText());
+ assertAll(values.entrySet().stream().map(entry ->
+ () -> assertEquals(entry.getValue(), page.findElement(By.id(formId + ":" + entry.getKey())).getDomProperty("value"))));
+ }
+
+ private void assertUnparseable(String id, String value) {
+ String formId = "formWithDefaultFormat";
+ WebPage page = getPage("spec2211.xhtml");
+ page.findElement(By.id(formId + ":" + id)).sendKeys(value);
+ page.guardHttp(page.findElement(By.id(formId + ":submit"))::click);
+
+ assertNotEquals("", page.findElement(By.id(formId + ":messages")).getText(),
+ "Type " + id + " must reject '" + value + "'");
+ }
+}